From 5213caac49d65765e0a98b45744d5d7a24cb0528 Mon Sep 17 00:00:00 2001 From: tarsin <43900799+asuka-mio@users.noreply.github.com> Date: Tue, 13 Dec 2022 15:51:27 +0800 Subject: [PATCH] Drop glgallery (#519) * Neutralize GlGallery * Enable dataBinding and viewBinding * Add tachiyomi reader layout * Cleanup Dead-code * Rename .java to .kt * Fix activity rename * Nullability changes * Add basic tachiyomi reader imageviews * Fix size increase * Let's cleanup later * Add some tachiyomi webtoon widgets * Import tachiyomi webtoon pager without chapter transition * Remove rx dependency * Rewrite GalleryProvider interface with flow * groundwork for image load * Refine webtoon pageholder * Force us hw bitmap, refine recycle * Strip tachiyomi decoder usage * Keep our own SubsamplingScaleImageView, strip tachiyomi decoder to avoid apk bloated * Import other PageViewers * Add webtoon viewer config * groundFix ReaderActivity * Add tachiyomi Preferences * Readpreference * Fix fullscreen * Move related class to tachiyomi package * Fix pageload * Add readerconfig * Add ViewerConfig hook * Add Reader bottomsheet settings * Fix reader setting apply * Refine * Fix save share image * Refine theme --- app/build.gradle | 9 + app/proguard-rules.pro | 2 + app/src/main/AndroidManifest.xml | 2 +- app/src/main/cpp/CMakeLists.txt | 2 +- app/src/main/cpp/image.c | 143 - .../labs/subscaleview/ImageSource.kt | 160 + .../labs/subscaleview/ImageViewState.kt | 15 + .../subscaleview/SubsamplingScaleImageView.kt | 2783 +++++++++++++++++ .../labs/subscaleview/decoder/Decoder.kt | 53 + .../labs/subscaleview/decoder/ImageDecoder.kt | 64 + .../provider/AssetInputProvider.kt | 11 + .../subscaleview/provider/InputProvider.kt | 10 + .../provider/OpenStreamProvider.kt | 16 + .../provider/ResourceInputProvider.kt | 11 + .../subscaleview/provider/UriInputProvider.kt | 13 + .../java/com/hippo/ehviewer/Settings.java | 21 +- .../java/com/hippo/ehviewer/UrlOpener.java | 18 +- ...ryProvider.java => ArchivePageLoader.java} | 17 +- ...alleryProvider.java => DirPageLoader.java} | 31 +- ...GalleryProvider.java => EhPageLoader.java} | 22 +- ...GalleryProvider2.java => PageLoader2.java} | 4 +- .../com/hippo/ehviewer/spider/SpiderDen.java | 12 +- .../hippo/ehviewer/spider/SpiderQueen.java | 17 +- .../hippo/ehviewer/ui/GalleryActivity.java | 1380 -------- .../ehviewer/ui/scene/DownloadsScene.java | 8 +- .../ehviewer/ui/scene/GalleryDetailScene.java | 30 +- .../ehviewer/ui/scene/GalleryListScene.java | 8 +- .../ui/scene/GalleryPreviewsScene.java | 10 +- .../hippo/ehviewer/ui/scene/HistoryScene.kt | 8 +- .../ehviewer/widget/GalleryGuideView.java | 202 -- .../hippo/ehviewer/widget/GalleryHeader.java | 210 -- .../ehviewer/widget/ReversibleSeekBar.java | 78 - .../hippo/ehviewer/widget/SeekBarPanel.java | 87 - .../com/hippo/glgallery/DownUpDetector.java | 70 - .../main/java/com/hippo/glgallery/Fling.java | 137 - .../com/hippo/glgallery/GalleryPageView.java | 197 -- .../com/hippo/glgallery/GalleryProvider.java | 263 -- .../java/com/hippo/glgallery/GalleryView.java | 1206 ------- .../hippo/glgallery/GestureRecognizer.java | 186 -- .../java/com/hippo/glgallery/ImageView.java | 597 ---- .../hippo/glgallery/PagerLayoutManager.java | 963 ------ .../hippo/glgallery/ScrollLayoutManager.java | 1134 ------- .../com/hippo/glgallery/SimpleAdapter.java | 163 - .../com/hippo/glview/anim/AlphaAnimation.java | 48 - .../java/com/hippo/glview/anim/Animation.java | 165 - .../hippo/glview/anim/CanvasAnimation.java | 26 - .../com/hippo/glview/anim/FloatAnimation.java | 40 - .../hippo/glview/annotation/RenderThread.java | 27 - .../hippo/glview/glrenderer/BasicTexture.java | 214 -- .../glview/glrenderer/BitmapTexture.java | 54 - .../glview/glrenderer/CanvasTexture.java | 51 - .../hippo/glview/glrenderer/ColorTexture.java | 74 - .../hippo/glview/glrenderer/ExtTexture.java | 60 - .../glview/glrenderer/FadeInTexture.java | 52 - .../glview/glrenderer/FadeOutTexture.java | 52 - .../hippo/glview/glrenderer/FadeTexture.java | 79 - .../com/hippo/glview/glrenderer/GLCanvas.java | 223 -- .../hippo/glview/glrenderer/GLES11Canvas.java | 1071 ------- .../hippo/glview/glrenderer/GLES11IdImpl.java | 67 - .../hippo/glview/glrenderer/GLES20Canvas.java | 1017 ------ .../hippo/glview/glrenderer/GLES20IdImpl.java | 60 - .../com/hippo/glview/glrenderer/GLId.java | 33 - .../com/hippo/glview/glrenderer/GLPaint.java | 50 - .../glview/glrenderer/MovableTextTexture.java | 202 -- .../glview/glrenderer/MultiLineTexture.java | 52 - .../glview/glrenderer/NativeTexture.java | 103 - .../glview/glrenderer/NinePatchChunk.java | 82 - .../glview/glrenderer/NinePatchTexture.java | 424 --- .../hippo/glview/glrenderer/RawTexture.java | 73 - .../glview/glrenderer/ResourceTexture.java | 53 - .../glview/glrenderer/SpriteTexture.java | 91 - .../glview/glrenderer/StringTexture.java | 87 - .../com/hippo/glview/glrenderer/Texture.java | 52 - .../glview/glrenderer/TextureUploader.java | 104 - .../hippo/glview/glrenderer/TiledTexture.java | 370 --- .../glview/glrenderer/UploadedTexture.java | 296 -- .../glview/glrenderer/XmlResourceTexture.java | 80 - .../glview/image/GLImageMovableTextView.java | 107 - .../glview/image/ImageMovableTextTexture.java | 210 -- .../glview/image/ImageSpriteTexture.java | 91 - .../com/hippo/glview/image/ImageTexture.java | 678 ---- .../com/hippo/glview/image/ImageWrapper.java | 158 - .../java/com/hippo/glview/util/ApiHelper.java | 190 -- .../com/hippo/glview/util/GalleryUtils.java | 60 - .../hippo/glview/util/MotionEventHelper.java | 118 - .../com/hippo/glview/view/AnimationTime.java | 45 - .../java/com/hippo/glview/view/GLRoot.java | 68 - .../com/hippo/glview/view/GLRootView.java | 710 ----- .../java/com/hippo/glview/view/GLView.java | 1341 -------- .../java/com/hippo/glview/view/Gravity.java | 98 - .../hippo/glview/view/OrientationSource.java | 23 - .../com/hippo/glview/view/TouchHelper.java | 240 -- .../com/hippo/glview/view/TouchOwner.java | 40 - .../hippo/glview/widget/GLFrameLayout.java | 107 - .../hippo/glview/widget/GLLinearLayout.java | 243 -- .../glview/widget/GLMovableTextView.java | 108 - .../hippo/glview/widget/GLProgressView.java | 239 -- .../hippo/glview/widget/GLTextureView.java | 69 - app/src/main/java/com/hippo/image/Image.kt | 86 +- .../java/com/hippo/widget/BatteryView.java | 181 -- .../main/java/com/hippo/widget/ColorView.java | 51 - .../main/java/com/hippo/widget/DateUtils.java | 78 - .../main/java/com/hippo/widget/TextClock.java | 268 -- .../java/com/hippo/yorozuya/ColorUtils.java | 32 - .../main/java/com/hippo/yorozuya/Pool.java | 50 - .../com/hippo/yorozuya/ResourcesUtils.java | 95 - .../thread/InfiniteThreadExecutor.java | 100 - .../core/preference/AndroidPreference.kt | 175 ++ .../core/preference/AndroidPreferenceStore.kt | 72 + .../tachiyomi/core/preference/Preference.kt | 26 + .../core/preference/PreferenceStore.kt | 41 + .../data/preference/PreferenceValues.kt | 25 + .../eu/kanade/tachiyomi/source/model/Page.kt | 21 + .../ui/reader/PageIndicatorTextView.kt | 52 + .../tachiyomi/ui/reader/ReaderActivity.kt | 1190 +++++++ .../ui/reader/ReaderColorFilterView.kt | 36 + .../ui/reader/ReaderNavigationOverlayView.kt | 112 + .../tachiyomi/ui/reader/ReaderPageSheet.kt | 43 + .../tachiyomi/ui/reader/ReaderSlider.kt | 30 + .../tachiyomi/ui/reader/loader/PageLoader.kt | 74 + .../tachiyomi/ui/reader/model/ReaderPage.kt | 8 + .../ui/reader/setting/OrientationType.kt | 25 + .../setting/ReaderColorFilterSettings.kt | 201 ++ .../reader/setting/ReaderGeneralSettings.kt | 52 + .../ui/reader/setting/ReaderPreferences.kt | 110 + .../setting/ReaderReadingModeSettings.kt | 125 + .../ui/reader/setting/ReaderSettingsSheet.kt | 92 + .../ui/reader/setting/ReadingModeType.kt | 45 + .../tachiyomi/ui/reader/viewer/BaseViewer.kt | 45 + .../viewer/GestureDetectorWithLongTap.kt | 75 + .../ui/reader/viewer/ReaderButton.kt | 27 + .../ui/reader/viewer/ReaderPageImageView.kt | 381 +++ .../reader/viewer/ReaderProgressIndicator.kt | 95 + .../ui/reader/viewer/ViewerConfig.kt | 89 + .../ui/reader/viewer/ViewerNavigation.kt | 49 + .../viewer/navigation/DisabledNavigation.kt | 18 + .../viewer/navigation/EdgeNavigation.kt | 32 + .../viewer/navigation/KindlishNavigation.kt | 28 + .../reader/viewer/navigation/LNavigation.kt | 36 + .../navigation/RightAndLeftNavigation.kt | 28 + .../tachiyomi/ui/reader/viewer/pager/Pager.kt | 111 + .../ui/reader/viewer/pager/PagerConfig.kt | 139 + .../ui/reader/viewer/pager/PagerPageHolder.kt | 202 ++ .../ui/reader/viewer/pager/PagerViewer.kt | 362 +++ .../reader/viewer/pager/PagerViewerAdapter.kt | 82 + .../ui/reader/viewer/pager/PagerViewers.kt | 53 + .../reader/viewer/webtoon/WebtoonAdapter.kt | 96 + .../viewer/webtoon/WebtoonBaseHolder.kt | 29 + .../ui/reader/viewer/webtoon/WebtoonConfig.kt | 92 + .../ui/reader/viewer/webtoon/WebtoonFrame.kt | 79 + .../viewer/webtoon/WebtoonLayoutManager.kt | 55 + .../viewer/webtoon/WebtoonPageHolder.kt | 244 ++ .../viewer/webtoon/WebtoonRecyclerView.kt | 327 ++ .../webtoon/WebtoonSubsamplingImageView.kt | 20 + .../ui/reader/viewer/webtoon/WebtoonViewer.kt | 270 ++ .../tachiyomi/util/lang/RectFExtensions.kt | 15 + .../util/preference/PreferenceExtensions.kt | 33 + .../util/system/ActivityExtensions.kt | 14 + .../util/system/AnimationExtensions.kt | 22 + .../util/system/ContextExtensions.kt | 258 ++ .../eu/kanade/tachiyomi/util/system/GLUtil.kt | 47 + .../kanade/tachiyomi/util/system/ImageUtil.kt | 248 ++ .../util/system/InternalResourceHelper.kt | 28 + .../tachiyomi/util/view/ViewExtensions.kt | 132 + .../tachiyomi/util/view/WindowExtensions.kt | 35 + .../tachiyomi/widget/MaterialSpinnerView.kt | 167 + .../eu/kanade/tachiyomi/widget/OutlineSpan.kt | 56 + .../tachiyomi/widget/ViewPagerAdapter.kt | 33 + .../listener/SimpleAnimationListener.kt | 11 + .../listener/SimpleTabSelectedListener.kt | 14 + .../widget/sheet/BaseBottomSheetDialog.kt | 52 + .../widget/sheet/BottomSheetViewPager.kt | 56 + .../widget/sheet/TabbedBottomSheetDialog.kt | 43 + app/src/main/res/anim/enter_from_bottom.xml | 8 + app/src/main/res/anim/enter_from_top.xml | 8 + app/src/main/res/anim/exit_to_bottom.xml | 8 + app/src/main/res/anim/exit_to_top.xml | 8 + app/src/main/res/drawable/ic_blank_24dp.xml | 7 + .../res/drawable/ic_brightness_5_24dp.xml | 9 + app/src/main/res/drawable/ic_check_24dp.xml | 9 + app/src/main/res/drawable/ic_crop_24dp.xml | 9 + .../main/res/drawable/ic_crop_off_24dp.xml | 18 + .../main/res/drawable/ic_expand_more_24dp.xml | 9 + .../main/res/drawable/ic_overflow_24dp.xml | 9 + .../ic_reader_continuous_vertical_24dp.xml | 15 + .../res/drawable/ic_reader_default_24dp.xml | 12 + .../main/res/drawable/ic_reader_ltr_24dp.xml | 21 + .../main/res/drawable/ic_reader_rtl_24dp.xml | 21 + .../res/drawable/ic_reader_vertical_24dp.xml | 12 + .../res/drawable/ic_reader_webtoon_24dp.xml | 12 + app/src/main/res/drawable/ic_save_24dp.xml | 9 + .../ic_screen_lock_landscape_24dp.xml | 9 + .../drawable/ic_screen_lock_portrait_24dp.xml | 9 + .../res/drawable/ic_screen_rotation_24dp.xml | 9 + .../main/res/drawable/ic_settings_24dp.xml | 9 + app/src/main/res/drawable/ic_share_24dp.xml | 9 + .../ic_stay_current_landscape_24dp.xml | 9 + .../ic_stay_current_portrait_24dp.xml | 9 + .../drawable/transparent_tabs_background.xml | 16 + app/src/main/res/layout/activity_gallery.xml | 108 - .../main/res/layout/common_tabbed_sheet.xml | 50 + .../main/res/layout/dialog_gallery_menu.xml | 202 -- app/src/main/res/layout/pref_spinner.xml | 58 + app/src/main/res/layout/reader_activity.xml | 191 ++ .../layout/reader_color_filter_settings.xml | 269 ++ app/src/main/res/layout/reader_error.xml | 23 + .../res/layout/reader_general_settings.xml | 87 + app/src/main/res/layout/reader_page_sheet.xml | 38 + .../main/res/layout/reader_pager_settings.xml | 100 + .../layout/reader_reading_mode_settings.xml | 58 + .../res/layout/reader_webtoon_settings.xml | 84 + app/src/main/res/values-sw720dp/dimens.xml | 4 + app/src/main/res/values/arrays.xml | 106 + app/src/main/res/values/attrs.xml | 6 + app/src/main/res/values/colors.xml | 9 + app/src/main/res/values/dimens.xml | 3 + app/src/main/res/values/ids.xml | 2 + app/src/main/res/values/strings.xml | 120 + app/src/main/res/values/styles.xml | 5 + 219 files changed, 11622 insertions(+), 18856 deletions(-) delete mode 100644 app/src/main/cpp/image.c create mode 100644 app/src/main/java/com/davemorrissey/labs/subscaleview/ImageSource.kt create mode 100644 app/src/main/java/com/davemorrissey/labs/subscaleview/ImageViewState.kt create mode 100644 app/src/main/java/com/davemorrissey/labs/subscaleview/SubsamplingScaleImageView.kt create mode 100644 app/src/main/java/com/davemorrissey/labs/subscaleview/decoder/Decoder.kt create mode 100644 app/src/main/java/com/davemorrissey/labs/subscaleview/decoder/ImageDecoder.kt create mode 100644 app/src/main/java/com/davemorrissey/labs/subscaleview/provider/AssetInputProvider.kt create mode 100644 app/src/main/java/com/davemorrissey/labs/subscaleview/provider/InputProvider.kt create mode 100644 app/src/main/java/com/davemorrissey/labs/subscaleview/provider/OpenStreamProvider.kt create mode 100644 app/src/main/java/com/davemorrissey/labs/subscaleview/provider/ResourceInputProvider.kt create mode 100644 app/src/main/java/com/davemorrissey/labs/subscaleview/provider/UriInputProvider.kt rename app/src/main/java/com/hippo/ehviewer/gallery/{ArchiveGalleryProvider.java => ArchivePageLoader.java} (96%) rename app/src/main/java/com/hippo/ehviewer/gallery/{DirGalleryProvider.java => DirPageLoader.java} (89%) rename app/src/main/java/com/hippo/ehviewer/gallery/{EhGalleryProvider.java => EhPageLoader.java} (92%) rename app/src/main/java/com/hippo/ehviewer/gallery/{GalleryProvider2.java => PageLoader2.java} (93%) delete mode 100644 app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java delete mode 100644 app/src/main/java/com/hippo/ehviewer/widget/GalleryGuideView.java delete mode 100644 app/src/main/java/com/hippo/ehviewer/widget/GalleryHeader.java delete mode 100644 app/src/main/java/com/hippo/ehviewer/widget/ReversibleSeekBar.java delete mode 100644 app/src/main/java/com/hippo/ehviewer/widget/SeekBarPanel.java delete mode 100644 app/src/main/java/com/hippo/glgallery/DownUpDetector.java delete mode 100644 app/src/main/java/com/hippo/glgallery/Fling.java delete mode 100644 app/src/main/java/com/hippo/glgallery/GalleryPageView.java delete mode 100644 app/src/main/java/com/hippo/glgallery/GalleryProvider.java delete mode 100644 app/src/main/java/com/hippo/glgallery/GalleryView.java delete mode 100644 app/src/main/java/com/hippo/glgallery/GestureRecognizer.java delete mode 100644 app/src/main/java/com/hippo/glgallery/ImageView.java delete mode 100644 app/src/main/java/com/hippo/glgallery/PagerLayoutManager.java delete mode 100644 app/src/main/java/com/hippo/glgallery/ScrollLayoutManager.java delete mode 100644 app/src/main/java/com/hippo/glgallery/SimpleAdapter.java delete mode 100644 app/src/main/java/com/hippo/glview/anim/AlphaAnimation.java delete mode 100644 app/src/main/java/com/hippo/glview/anim/Animation.java delete mode 100644 app/src/main/java/com/hippo/glview/anim/CanvasAnimation.java delete mode 100644 app/src/main/java/com/hippo/glview/anim/FloatAnimation.java delete mode 100644 app/src/main/java/com/hippo/glview/annotation/RenderThread.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/BasicTexture.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/BitmapTexture.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/CanvasTexture.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/ColorTexture.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/ExtTexture.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/FadeInTexture.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/FadeOutTexture.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/FadeTexture.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/GLCanvas.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/GLES11Canvas.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/GLES11IdImpl.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/GLES20Canvas.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/GLES20IdImpl.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/GLId.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/GLPaint.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/MovableTextTexture.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/MultiLineTexture.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/NativeTexture.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/NinePatchChunk.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/NinePatchTexture.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/RawTexture.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/ResourceTexture.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/SpriteTexture.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/StringTexture.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/Texture.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/TextureUploader.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/TiledTexture.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/UploadedTexture.java delete mode 100644 app/src/main/java/com/hippo/glview/glrenderer/XmlResourceTexture.java delete mode 100644 app/src/main/java/com/hippo/glview/image/GLImageMovableTextView.java delete mode 100644 app/src/main/java/com/hippo/glview/image/ImageMovableTextTexture.java delete mode 100644 app/src/main/java/com/hippo/glview/image/ImageSpriteTexture.java delete mode 100644 app/src/main/java/com/hippo/glview/image/ImageTexture.java delete mode 100644 app/src/main/java/com/hippo/glview/image/ImageWrapper.java delete mode 100644 app/src/main/java/com/hippo/glview/util/ApiHelper.java delete mode 100644 app/src/main/java/com/hippo/glview/util/GalleryUtils.java delete mode 100644 app/src/main/java/com/hippo/glview/util/MotionEventHelper.java delete mode 100644 app/src/main/java/com/hippo/glview/view/AnimationTime.java delete mode 100644 app/src/main/java/com/hippo/glview/view/GLRoot.java delete mode 100644 app/src/main/java/com/hippo/glview/view/GLRootView.java delete mode 100644 app/src/main/java/com/hippo/glview/view/GLView.java delete mode 100644 app/src/main/java/com/hippo/glview/view/Gravity.java delete mode 100644 app/src/main/java/com/hippo/glview/view/OrientationSource.java delete mode 100644 app/src/main/java/com/hippo/glview/view/TouchHelper.java delete mode 100644 app/src/main/java/com/hippo/glview/view/TouchOwner.java delete mode 100644 app/src/main/java/com/hippo/glview/widget/GLFrameLayout.java delete mode 100644 app/src/main/java/com/hippo/glview/widget/GLLinearLayout.java delete mode 100644 app/src/main/java/com/hippo/glview/widget/GLMovableTextView.java delete mode 100644 app/src/main/java/com/hippo/glview/widget/GLProgressView.java delete mode 100644 app/src/main/java/com/hippo/glview/widget/GLTextureView.java delete mode 100644 app/src/main/java/com/hippo/widget/BatteryView.java delete mode 100644 app/src/main/java/com/hippo/widget/ColorView.java delete mode 100644 app/src/main/java/com/hippo/widget/DateUtils.java delete mode 100644 app/src/main/java/com/hippo/widget/TextClock.java delete mode 100644 app/src/main/java/com/hippo/yorozuya/ColorUtils.java delete mode 100644 app/src/main/java/com/hippo/yorozuya/Pool.java delete mode 100644 app/src/main/java/com/hippo/yorozuya/ResourcesUtils.java delete mode 100644 app/src/main/java/com/hippo/yorozuya/thread/InfiniteThreadExecutor.java create mode 100644 app/src/main/java/eu/kanade/tachiyomi/core/preference/AndroidPreference.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/core/preference/AndroidPreferenceStore.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/core/preference/Preference.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/core/preference/PreferenceStore.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceValues.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/PageIndicatorTextView.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderColorFilterView.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderNavigationOverlayView.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPageSheet.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSlider.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/PageLoader.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderPage.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/OrientationType.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderColorFilterSettings.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderGeneralSettings.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderReadingModeSettings.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsSheet.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReadingModeType.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/BaseViewer.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/GestureDetectorWithLongTap.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderButton.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressIndicator.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ViewerConfig.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ViewerNavigation.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/DisabledNavigation.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/EdgeNavigation.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/KindlishNavigation.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/LNavigation.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/RightAndLeftNavigation.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/Pager.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewers.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonBaseHolder.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonConfig.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonFrame.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonLayoutManager.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonRecyclerView.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonSubsamplingImageView.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/util/lang/RectFExtensions.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/util/preference/PreferenceExtensions.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/util/system/ActivityExtensions.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/util/system/AnimationExtensions.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/util/system/GLUtil.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/util/system/InternalResourceHelper.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/util/view/WindowExtensions.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/MaterialSpinnerView.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/OutlineSpan.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/ViewPagerAdapter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/listener/SimpleAnimationListener.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/listener/SimpleTabSelectedListener.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/sheet/BaseBottomSheetDialog.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/sheet/BottomSheetViewPager.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/sheet/TabbedBottomSheetDialog.kt create mode 100644 app/src/main/res/anim/enter_from_bottom.xml create mode 100644 app/src/main/res/anim/enter_from_top.xml create mode 100644 app/src/main/res/anim/exit_to_bottom.xml create mode 100644 app/src/main/res/anim/exit_to_top.xml create mode 100644 app/src/main/res/drawable/ic_blank_24dp.xml create mode 100644 app/src/main/res/drawable/ic_brightness_5_24dp.xml create mode 100644 app/src/main/res/drawable/ic_check_24dp.xml create mode 100644 app/src/main/res/drawable/ic_crop_24dp.xml create mode 100644 app/src/main/res/drawable/ic_crop_off_24dp.xml create mode 100644 app/src/main/res/drawable/ic_expand_more_24dp.xml create mode 100644 app/src/main/res/drawable/ic_overflow_24dp.xml create mode 100644 app/src/main/res/drawable/ic_reader_continuous_vertical_24dp.xml create mode 100644 app/src/main/res/drawable/ic_reader_default_24dp.xml create mode 100644 app/src/main/res/drawable/ic_reader_ltr_24dp.xml create mode 100644 app/src/main/res/drawable/ic_reader_rtl_24dp.xml create mode 100644 app/src/main/res/drawable/ic_reader_vertical_24dp.xml create mode 100644 app/src/main/res/drawable/ic_reader_webtoon_24dp.xml create mode 100644 app/src/main/res/drawable/ic_save_24dp.xml create mode 100644 app/src/main/res/drawable/ic_screen_lock_landscape_24dp.xml create mode 100644 app/src/main/res/drawable/ic_screen_lock_portrait_24dp.xml create mode 100644 app/src/main/res/drawable/ic_screen_rotation_24dp.xml create mode 100644 app/src/main/res/drawable/ic_settings_24dp.xml create mode 100644 app/src/main/res/drawable/ic_share_24dp.xml create mode 100644 app/src/main/res/drawable/ic_stay_current_landscape_24dp.xml create mode 100644 app/src/main/res/drawable/ic_stay_current_portrait_24dp.xml create mode 100644 app/src/main/res/drawable/transparent_tabs_background.xml delete mode 100644 app/src/main/res/layout/activity_gallery.xml create mode 100644 app/src/main/res/layout/common_tabbed_sheet.xml delete mode 100644 app/src/main/res/layout/dialog_gallery_menu.xml create mode 100644 app/src/main/res/layout/pref_spinner.xml create mode 100644 app/src/main/res/layout/reader_activity.xml create mode 100644 app/src/main/res/layout/reader_color_filter_settings.xml create mode 100644 app/src/main/res/layout/reader_error.xml create mode 100644 app/src/main/res/layout/reader_general_settings.xml create mode 100644 app/src/main/res/layout/reader_page_sheet.xml create mode 100644 app/src/main/res/layout/reader_pager_settings.xml create mode 100644 app/src/main/res/layout/reader_reading_mode_settings.xml create mode 100644 app/src/main/res/layout/reader_webtoon_settings.xml create mode 100644 app/src/main/res/values-sw720dp/dimens.xml diff --git a/app/build.gradle b/app/build.gradle index 0cda361e6..d27acd634 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -94,6 +94,8 @@ android { buildFeatures { buildConfig true compose true + dataBinding true + viewBinding true } composeOptions { @@ -194,6 +196,13 @@ dependencies { implementation 'io.coil-kt:coil:2.2.2' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' implementation 'org.jsoup:jsoup:1.15.3' + + // Tachiyomi Reader + implementation 'com.github.chrisbanes:PhotoView:2.3.0' + implementation 'dev.chrisbanes.insetter:insetter:0.6.1' + implementation ('com.github.tachiyomiorg:DirectionalViewPager:1.0.0') { + exclude group: 'androidx.viewpager', module: 'viewpager' + } } configurations.all { diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index ccc265f46..40dc408b0 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -14,6 +14,8 @@ -keep,allowoptimization class com.hippo.ehviewer.ui.fragment.* extends com.hippo.ehviewer.ui.fragment.BaseFragment { } -keep,allowoptimization class com.hippo.ehviewer.ui.fragment.* extends com.hippo.ehviewer.ui.fragment.BasePreferenceFragment { } +-keep class androidx.viewpager.widget.ViewPager$LayoutParams { int position; } + -renamesourcefileattribute -repackageclasses -allowaccessmodification diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ce8aac1ab..1f469c46c 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -102,7 +102,7 @@ diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index f2c14d2d0..0099767fa 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -45,6 +45,6 @@ SET(LIBARCHIVE_CUSTOM_LIBS "liblzma" "nettle") add_subdirectory(libarchive) # Build and link our app's native lib -add_library(${PROJECT_NAME} SHARED archive.c image.c fdutils.c) +add_library(${PROJECT_NAME} SHARED archive.c fdutils.c) target_include_directories(${PROJECT_NAME} PUBLIC libarchive/libarchive) target_link_libraries(${PROJECT_NAME} archive_static log jnigraphics GLESv2 -Wl,--gc-sections) diff --git a/app/src/main/cpp/image.c b/app/src/main/cpp/image.c deleted file mode 100644 index 69627b17f..000000000 --- a/app/src/main/cpp/image.c +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2022 Tarsin Norbin - * - * This file is part of EhViewer - * - * EhViewer is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * EhViewer is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along with - * EhViewer. If not, see . - */ - -#include -#include - -#include -#include -#include - -#include -#include - -#define TAG "ImageDecoder_wrapper" - -#include "ehviewer.h" - -#define IMAGE_TILE_MAX_SIZE (512 * 512) - -static char tile_buffer[IMAGE_TILE_MAX_SIZE * 4]; - -bool copy_pixels(const void *src, int src_w, int src_h, int src_x, int src_y, - void *dst, int dst_w, int dst_h, int dst_x, int dst_y, - int width, int height) { - int left; - int line; - size_t line_stride; - int src_stride; - int src_pos; - int dst_pos; - size_t dst_blank_length; - - // Sanitize - if (src_x < 0) { - width -= src_x; - dst_x -= src_x; - src_x = 0; - } - if (dst_x < 0) { - width -= dst_x; - src_x -= dst_x; - dst_x = 0; - } - if (width <= 0) { - return false; - } - if (src_y < 0) { - height -= src_y; - dst_y -= src_y; - src_y = 0; - } - if (dst_y < 0) { - height -= dst_y; - src_y -= dst_y; - dst_y = 0; - } - if (height <= 0) { - return false; - } - left = src_x + width - src_w; - if (left > 0) { - width -= left; - } - left = dst_x + width - dst_w; - if (left > 0) { - width -= left; - } - if (width <= 0) { - return false; - } - left = src_y + height - src_h; - if (left > 0) { - height -= left; - } - left = dst_y + height - dst_h; - if (left > 0) { - height -= left; - } - if (height <= 0) { - return false; - } - - // Init - line_stride = (size_t) (width * 4); - src_stride = src_w * 4; - src_pos = src_y * src_stride + src_x * 4; - dst_pos = 0; - - dst_blank_length = (size_t) (dst_y * dst_w + dst_x) * 4; - - // First line - dst_pos += (int) dst_blank_length; - memcpy(dst + dst_pos, src + src_pos, line_stride); - dst_pos += (int) line_stride; - src_pos += src_stride; - - // Other lines - dst_blank_length = (size_t) ((dst_w - width) * 4); - for (line = 1; line < height; line++) { - dst_pos += (int) dst_blank_length; - memcpy(dst + dst_pos, src + src_pos, line_stride); - dst_pos += (int) line_stride; - src_pos += src_stride; - } - - return true; -} - -JNIEXPORT void JNICALL -Java_com_hippo_image_Image_nativeTexImage(JNIEnv *env, jclass clazz, jobject bitmap, jboolean init, - jint offset_x, jint offset_y, jint width, jint height) { - if (width * height > IMAGE_TILE_MAX_SIZE) - return; - AndroidBitmapInfo info; - void *pixels = NULL; - AndroidBitmap_lockPixels(env, bitmap, &pixels); - AndroidBitmap_getInfo(env, bitmap, &info); - copy_pixels(pixels, info.width, info.height, offset_x, offset_y, tile_buffer, width, height, 0, 0, width, height); - AndroidBitmap_unlockPixels(env, bitmap); - if (init) { - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, - tile_buffer); - } else { - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, - tile_buffer); - } -} diff --git a/app/src/main/java/com/davemorrissey/labs/subscaleview/ImageSource.kt b/app/src/main/java/com/davemorrissey/labs/subscaleview/ImageSource.kt new file mode 100644 index 000000000..8d8ce0dba --- /dev/null +++ b/app/src/main/java/com/davemorrissey/labs/subscaleview/ImageSource.kt @@ -0,0 +1,160 @@ +package com.davemorrissey.labs.subscaleview + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Rect +import android.net.Uri +import com.davemorrissey.labs.subscaleview.provider.AssetInputProvider +import com.davemorrissey.labs.subscaleview.provider.InputProvider +import com.davemorrissey.labs.subscaleview.provider.OpenStreamProvider +import com.davemorrissey.labs.subscaleview.provider.ResourceInputProvider +import com.davemorrissey.labs.subscaleview.provider.UriInputProvider +import java.io.InputStream + +/** + * Helper class used to set the source and additional attributes from a variety of sources. Supports + * use of a bitmap, asset, resource, external file or any other URI. + * + * + * When you are using a preview image, you must set the dimensions of the full size image on the + * ImageSource object for the full size image using the [.dimensions] method. + */ +class ImageSource { + val bitmap: Bitmap? + val provider: InputProvider? + + private var sWidth = 0 + private var sHeight = 0 + + var sRegion = Rect(0, 0, 0, 0) + private set + var isCached = false + private set + + private constructor(bitmap: Bitmap, cached: Boolean) { + this.bitmap = bitmap + provider = null + sWidth = bitmap.width + sHeight = bitmap.height + isCached = cached + } + + private constructor(provider: InputProvider) { + bitmap = null + this.provider = provider + } + + /** + * Use a region of the source image. Region must be set independently for the full size image and the preview if + * you are using one. + * + * @param sRegion the region of the source image to be displayed. + * @return this instance for chaining. + */ + fun region(sRegion: Rect): ImageSource { + this.sRegion = sRegion + setInvariants() + return this + } + + /** + * Declare the dimensions of the image. This is only required for a full size image, when you are specifying a URI + * and also a preview image. When displaying a bitmap object, or not using a preview, you do not need to declare + * the image dimensions. Note if the declared dimensions are found to be incorrect, the view will reset. + * + * @param sWidth width of the source image. + * @param sHeight height of the source image. + * @return this instance for chaining. + */ + fun dimensions(sWidth: Int, sHeight: Int): ImageSource { + if (bitmap == null) { + this.sWidth = sWidth + this.sHeight = sHeight + } + setInvariants() + return this + } + + private fun setInvariants() { + if (!sRegion.isEmpty) { + sWidth = sRegion.width() + sHeight = sRegion.height() + } + } + + companion object { + /** + * Create an instance from a resource. The correct resource for the device screen resolution will be used. + * + * @param resId resource ID. + * @return an [ImageSource] instance. + */ + @JvmStatic + fun resource(context: Context, resId: Int): ImageSource { + return ImageSource(ResourceInputProvider(context, resId)) + } + + /** + * Create an instance from an asset name. + * + * @param assetName asset name. + * @return an [ImageSource] instance. + */ + @JvmStatic + fun asset(context: Context, assetName: String): ImageSource { + return ImageSource(AssetInputProvider(context, assetName)) + } + + /** + * Create an instance from a URI. + * + * @param uri image URI. + * @return an [ImageSource] instance. + */ + fun uri(context: Context, uri: Uri): ImageSource { + return ImageSource(UriInputProvider(context, uri)) + } + + /** + * Create an instance from an input provider. + * + * @param provider input stream provider. + * @return an [ImageSource] instance. + */ + fun provider(provider: InputProvider): ImageSource { + return ImageSource(provider) + } + + /** + * Create an instance from an input stream. + * + * @param stream open input stream. + * @return an [ImageSource] instance. + */ + fun inputStream(stream: InputStream): ImageSource { + return ImageSource(OpenStreamProvider(stream)) + } + + /** + * Provide a loaded bitmap for display. + * + * @param bitmap bitmap to be displayed. + * @return an [ImageSource] instance. + */ + fun bitmap(bitmap: Bitmap): ImageSource { + return ImageSource(bitmap, false) + } + + /** + * Provide a loaded and cached bitmap for display. This bitmap will not be recycled when it is no + * longer needed. Use this method if you loaded the bitmap with an image loader such as Picasso + * or Volley. + * + * @param bitmap bitmap to be displayed. + * @return an [ImageSource] instance. + */ + fun cachedBitmap(bitmap: Bitmap): ImageSource { + return ImageSource(bitmap, true) + } + } +} diff --git a/app/src/main/java/com/davemorrissey/labs/subscaleview/ImageViewState.kt b/app/src/main/java/com/davemorrissey/labs/subscaleview/ImageViewState.kt new file mode 100644 index 000000000..41b285d1a --- /dev/null +++ b/app/src/main/java/com/davemorrissey/labs/subscaleview/ImageViewState.kt @@ -0,0 +1,15 @@ +package com.davemorrissey.labs.subscaleview + +import android.graphics.PointF +import java.io.Serializable + +/** + * Wraps the scale, center and orientation of a displayed image for easy restoration on screen rotate. + */ +class ImageViewState(val scale: Float, center: PointF) : Serializable { + private val centerX: Float = center.x + private val centerY: Float = center.y + + val center: PointF + get() = PointF(centerX, centerY) +} diff --git a/app/src/main/java/com/davemorrissey/labs/subscaleview/SubsamplingScaleImageView.kt b/app/src/main/java/com/davemorrissey/labs/subscaleview/SubsamplingScaleImageView.kt new file mode 100644 index 000000000..4b420f3fb --- /dev/null +++ b/app/src/main/java/com/davemorrissey/labs/subscaleview/SubsamplingScaleImageView.kt @@ -0,0 +1,2783 @@ +package com.davemorrissey.labs.subscaleview + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Point +import android.graphics.PointF +import android.graphics.Rect +import android.graphics.RectF +import android.util.AttributeSet +import android.util.Log +import android.util.TypedValue +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.View +import androidx.annotation.AnyThread +import androidx.annotation.IntDef +import com.davemorrissey.labs.subscaleview.ImageSource.Companion.asset +import com.davemorrissey.labs.subscaleview.ImageSource.Companion.resource +import com.davemorrissey.labs.subscaleview.decoder.Decoder +import com.davemorrissey.labs.subscaleview.decoder.ImageDecoder +import com.davemorrissey.labs.subscaleview.provider.InputProvider +import com.hippo.ehviewer.R +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.ArrayList +import java.util.Locale +import java.util.concurrent.locks.ReadWriteLock +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt +import kotlin.math.sqrt + +/** + * Displays an image subsampled as necessary to avoid loading too much image data into memory. After zooming in, + * a set of image tiles subsampled at higher resolution are loaded and displayed over the base layer. During pan and + * zoom, tiles off screen or higher/lower resolution than required are discarded from memory. + * + * + * Tiles are no larger than the max supported bitmap size, so with large images tiling may be used even when zoomed out. + * + * + * v prefixes - coordinates, translations and distances measured in screen (view) pixels + *

+ * s prefixes - coordinates, translations and distances measured in rotated and cropped source image pixels (scaled) + *

+ * f prefixes - coordinates, translations and distances measured in original unrotated, uncropped source file pixels + * + * + * [View project on GitHub](https://github.com/davemorrissey/subsampling-scale-image-view) + * + */ +@Suppress("unused") +open class SubsamplingScaleImageView @JvmOverloads constructor( + context: Context, + attr: AttributeSet? = null +) : View(context, attr) { + private val decoderLock: ReadWriteLock = ReentrantReadWriteLock(true) + + // Coroutines scope + private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + + // Current quickscale state + private val quickScaleThreshold: Float + + // Long click handler + private var longClickJob: Job? = null + + private val srcArray = FloatArray(8) + private val dstArray = FloatArray(8) + + //The logical density of the display + private val density: Float = resources.displayMetrics.density + + // Bitmap (preview or full image) + private var bitmap: Bitmap? = null + + // Specifies if a cache handler is also referencing the bitmap. Do not recycle if so. + private var bitmapIsCached = false + + // Provider of full size image + private var provider: InputProvider? = null + + // Sample size used to display the whole image when fully zoomed out + private var fullImageSampleSize = 0 + + // Map of zoom level to tile grid + private val tileMap: MutableMap> = mutableMapOf() + + // Overlay tile boundaries and other info + private var debug = false + + /** + * Maximum scale allowed. A value of 1 means 1:1 pixels at maximum scale. You may wish to set this according + * to screen density - on a retina screen, 1:1 may still be too small. Consider using [setMinimumDpi], + * which is density aware. + */ + // Max scale allowed (prevent infinite zoom) + var maxScale = 2F + + // Density to reach before loading higher resolution tiles + private var minimumTileDpi = -1 + + // Pan limiting style + @PanLimit private var panLimit = PAN_LIMIT_INSIDE + + // Minimum scale type + @ScaleType private var minimumScaleType = SCALE_TYPE_CENTER_INSIDE + + // Whether to crop borders. + private var cropBorders = false + private var maxTileWidth = TILE_SIZE_AUTO + private var maxTileHeight = TILE_SIZE_AUTO + + // Whether tiles should be loaded while gestures and animations are still in progress + private var eagerLoadingEnabled = true + + // Gesture detection settings + private var panEnabled = true + + /** + * Enable or disable zoom gesture detection. Disabling zoom locks the the current scale. + */ + var isZoomEnabled = true + + /** + * Enable or disable double tap & swipe to zoom. + */ + var isQuickScaleEnabled = true + + // Double tap zoom behaviour + private var doubleTapZoomScale = 1F + @ZoomStyle private var doubleTapZoomStyle = ZOOM_FOCUS_FIXED + private var doubleTapZoomDuration = 500 + + /** + * Returns the current scale value. + * + * @return the current scale as a source/view pixels ratio. + */ + // Current scale and scale at start of zoom + var scale = 0F + private set + private var scaleStart = 0F + + // Screen coordinate of top-left corner of source image + private var vTranslate: PointF? = null + private var vTranslateStart: PointF? = null + private var vTranslateBefore: PointF? = null + + /** + * Sets left extra space for the view in view pixels. + * @see setExtraSpace + */ + var extraSpaceLeft = 0F + + /** + * Sets top extra space for the view in view pixels. + * @see setExtraSpace + */ + var extraSpaceTop = 0F + + /** + * Sets right extra space for the view in view pixels. + * @see setExtraSpace + */ + var extraSpaceRight = 0F + + /** + * Sets bottom extra space for the view in view pixels. + * @see setExtraSpace + */ + var extraSpaceBottom = 0F + + // Source coordinate to center on, used when new position is set externally before view is ready + private var pendingScale = -1F + private var sPendingCenter: PointF? = null + private var sRequestedCenter: PointF? = null + + /** + * Get source width. + * + * @return the source image width in pixels. + */ + // Source image dimensions and orientation - dimensions relate to the unrotated image + var sWidth = 0 + private set + + /** + * Get source height. + * + * @return the source image height in pixels. + */ + var sHeight = 0 + private set + + // Min scale allowed (prevent infinite zoom) + private var minScale = minScale() + private val sRegion = Rect(0, 0, 0, 0) + + // Is two-finger zooming in progress + private var isZooming = false + + // Is one-finger panning in progress + private var isPanning = false + + // Is quick-scale gesture in progress + private var isQuickScaling = false + + // Max touches used in current gesture + private var maxTouchCount = 0 + + // Fling detector + private var detector: GestureDetector? = null + private var singleDetector: GestureDetector? = null + + // Tile and image decoding + private var decoder: Decoder? = null + + // Debug values + private var vCenterStart: PointF? = null + private var vDistStart = 0F + private var quickScaleLastDistance = 0F + private var quickScaleMoved = false + private var quickScaleVLastPoint: PointF? = null + private var quickScaleSCenter: PointF? = null + private var quickScaleVStart: PointF? = null + + // Scale and center animation tracking + private var anim: Anim? = null + + /** + * Call to find whether the view is initialised, has dimensions, and will display an image on + * the next draw. If a preview has been provided, it may be the preview that will be displayed + * and the full size image may still be loading. If no preview was provided, this is called once + * the base layer tiles of the full size image are loaded. + * + * @return true if the view is ready to display an image and accept touch gestures. + */ + var isReady = false + private set + + /** + * Call to find whether the main image (base layer tiles where relevant) have been loaded. Before + * this event the view is blank unless a preview was provided. + * + * @return true if the main image (not the preview) has been loaded and is ready to display. + */ + var isImageLoaded = false + private set + + // Event listener + private var onImageEventListener: OnImageEventListener? = null + + // Scale and center listener + private var onStateChangedListener: OnStateChangedListener? = null + + // Long click listener + private var onLongClickListener: OnLongClickListener? = null + + // Paint objects created once and reused for efficiency + private val bitmapPaint by lazy { + Paint().apply { + isAntiAlias = true + isFilterBitmap = true + isDither = true + } + } + private val debugTextPaint by lazy { + Paint().apply { + textSize = px(12).toFloat() + color = Color.MAGENTA + style = Paint.Style.FILL + } + } + private val debugLinePaint by lazy { + Paint().apply { + color = Color.MAGENTA + style = Paint.Style.STROKE + strokeWidth = px(1).toFloat() + } + } + private var tileBgPaint: Paint? = null + + // Volatile fields used to reduce object creation + private var satTemp: ScaleAndTranslate? = null + private val mtrx = Matrix() + private val sRect = RectF() + + init { + setMinimumDpi(160) + setDoubleTapZoomDpi(160) + setGestureDetector(context) + + // Handle XML attributes + if (attr != null) { + val typedAttr = getContext().obtainStyledAttributes(attr, R.styleable.SubsamplingScaleImageView) + if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_assetName)) { + val assetName = typedAttr.getString(R.styleable.SubsamplingScaleImageView_assetName) + if (assetName != null && assetName.isNotEmpty()) { + setImage(asset(context, assetName)) + } + } + if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_src)) { + val resId = typedAttr.getResourceId(R.styleable.SubsamplingScaleImageView_src, 0) + if (resId > 0) { + setImage(resource(context, resId)) + } + } + if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_panEnabled)) { + setPanEnabled(typedAttr.getBoolean(R.styleable.SubsamplingScaleImageView_panEnabled, true)) + } + if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_zoomEnabled)) { + isZoomEnabled = typedAttr.getBoolean(R.styleable.SubsamplingScaleImageView_zoomEnabled, true) + } + if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_quickScaleEnabled)) { + isQuickScaleEnabled = + typedAttr.getBoolean(R.styleable.SubsamplingScaleImageView_quickScaleEnabled, true) + } + if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_tileBackgroundColor)) { + setTileBackgroundColor( + typedAttr.getColor( + R.styleable.SubsamplingScaleImageView_tileBackgroundColor, + Color.argb(0, 0, 0, 0) + ) + ) + } + typedAttr.recycle() + } + quickScaleThreshold = + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20F, context.resources.displayMetrics) + } + + /** + * Set the image source from a bitmap, resource, asset, file or other URI. + * + * @param imageSource Image source. + */ + fun setImage(imageSource: ImageSource) { + setImage(imageSource, null) + } + + /** + * Set the image source from a bitmap, resource, asset, file or other URI, starting with a given orientation + * setting, scale and center. This is the best method to use when you want scale and center to be restored + * after screen orientation change; it avoids any redundant loading of tiles in the wrong orientation. + * + * @param imageSource Image source. + * @param state State to be restored. Nullable. + */ + fun setImage(imageSource: ImageSource, state: ImageViewState?) { + reset(true) + state?.let { restoreState(it) } + if (imageSource.bitmap != null && !imageSource.sRegion.isEmpty) { + onImageLoaded( + Bitmap.createBitmap( + imageSource.bitmap, + imageSource.sRegion.left, + imageSource.sRegion.top, + imageSource.sRegion.width(), + imageSource.sRegion.height() + ), false + ) + } else if (imageSource.bitmap != null) { + onImageLoaded(imageSource.bitmap, imageSource.isCached) + } else { + // Load the bitmap using tile decoding. + provider = imageSource.provider + sRegion.set(imageSource.sRegion) + initTiles() + } + } + + private fun initTiles() = coroutineScope.launch { + try { + debug("initTiles") + val imageProvider = provider ?: throw IllegalStateException("Image provider not set") + decoder = ImageDecoder(cropBorders) + val dimensions = decoder!!.init(context, imageProvider) + var sWidth = dimensions.x + var sHeight = dimensions.y + sRegion.apply { + if (!isEmpty) { + left = left.coerceAtLeast(0) + top = top.coerceAtLeast(0) + right = right.coerceAtLeast(sWidth) + bottom = bottom.coerceAtLeast(sHeight) + sWidth = width() + sHeight = height() + } + } + withContext(Dispatchers.Main) { + onTilesInited(decoder!!, sWidth, sHeight) + } + } catch (e: Exception) { + Log.e(TAG, e.message, e) + withContext(Dispatchers.Main) { + onImageEventListener?.onImageLoadError(e) + } + } + } + + /** + * Reset all state before setting/changing image or setting new rotation. + */ + private fun reset(newImage: Boolean) { + debug("reset newImage=$newImage") + scale = 0F + scaleStart = 0F + vTranslate = null + vTranslateStart = null + vTranslateBefore = null + pendingScale = -1F + sPendingCenter = null + sRequestedCenter = null + isZooming = false + isPanning = false + isQuickScaling = false + maxTouchCount = 0 + fullImageSampleSize = 0 + vCenterStart = null + vDistStart = 0F + quickScaleLastDistance = 0F + quickScaleMoved = false + quickScaleSCenter = null + quickScaleVLastPoint = null + quickScaleVStart = null + anim = null + satTemp = null + mtrx.reset() + sRect.setEmpty() + if (newImage) { + provider = null + decoderLock.writeLock().lock() + try { + decoder?.recycle() + decoder = null + } finally { + decoderLock.writeLock().unlock() + } + if (!bitmapIsCached) { + bitmap?.recycle() + } + sWidth = 0 + sHeight = 0 + sRegion.setEmpty() + isReady = false + isImageLoaded = false + bitmap = null + bitmapIsCached = false + } + if (tileMap.isNotEmpty()) { + tileMap.forEach { (_, value) -> + value.forEach { tile -> + tile.visible = false + tile.bitmap?.recycle() + tile.bitmap = null + } + } + tileMap.clear() + } + setGestureDetector(context) + } + + private fun setGestureDetector(context: Context) { + detector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() { + override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean { + if (panEnabled && isReady && vTranslate != null && (abs(e1.x - e2.x) > 50 || abs(e1.y - e2.y) > 50) && (abs(velocityX) > 500 || abs(velocityY) > 500) && !isZooming) { + val vTranslateEnd = PointF(vTranslate!!.x + velocityX * 0.25F, vTranslate!!.y + velocityY * 0.25F) + val sCenterXEnd = (width / 2 - vTranslateEnd.x) / scale + val sCenterYEnd = (height / 2 - vTranslateEnd.y) / scale + AnimationBuilder(PointF(sCenterXEnd, sCenterYEnd)) + .withEasing(EASE_OUT_QUAD) + .withPanLimited(false) + .withOrigin(ORIGIN_FLING) + .start() + return true + } + return super.onFling(e1, e2, velocityX, velocityY) + } + + override fun onSingleTapConfirmed(e: MotionEvent): Boolean { + performClick() + return true + } + + override fun onDoubleTap(e: MotionEvent): Boolean { + if (isZoomEnabled && isReady && vTranslate != null) { + // Hacky solution for #15 - after a double tap the GestureDetector gets in a state + // where the next fling is ignored, so here we replace it with a new one. + setGestureDetector(context) + return if (isQuickScaleEnabled) { + // Store quick scale params. This will become either a double tap zoom or a + // quick scale depending on whether the user swipes. + vCenterStart = PointF(e.x, e.y) + vTranslateStart = PointF(vTranslate!!.x, vTranslate!!.y) + scaleStart = scale + isQuickScaling = true + isZooming = true + quickScaleLastDistance = -1F + quickScaleSCenter = viewToSourceCoord(vCenterStart!!) + quickScaleVStart = PointF(e.x, e.y) + quickScaleVLastPoint = PointF(quickScaleSCenter!!.x, quickScaleSCenter!!.y) + quickScaleMoved = false + // We need to get events in onTouchEvent after this. + false + } else { + // Start double tap zoom animation. + viewToSourceCoord(PointF(e.x, e.y))?.let { center -> + doubleTapZoom(center, PointF(e.x, e.y)) + } + true + } + } + return super.onDoubleTapEvent(e) + } + }) + singleDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() { + override fun onSingleTapConfirmed(e: MotionEvent): Boolean { + performClick() + return true + } + }) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + coroutineScope.cancel() + } + + /** + * On resize, preserve center and scale. Various behaviours are possible, override this method to use another. + */ + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + debug("onSizeChanged %dx%d -> %dx%d", oldw, oldh, w, h) + if (isReady && center != null) { + anim = null + pendingScale = scale + sPendingCenter = center + } + } + + /** + * Measures the width and height of the view, preserving the aspect ratio of the image displayed if wrap_content is + * used. The image will scale within this box, not resizing the view as it is zoomed. + */ + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val widthSpecMode = MeasureSpec.getMode(widthMeasureSpec) + val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec) + val parentWidth = MeasureSpec.getSize(widthMeasureSpec) + val parentHeight = MeasureSpec.getSize(heightMeasureSpec) + val resizeWidth = widthSpecMode != MeasureSpec.EXACTLY + val resizeHeight = heightSpecMode != MeasureSpec.EXACTLY + var width = parentWidth + var height = parentHeight + if (sWidth > 0 && sHeight > 0) { + if (resizeWidth && resizeHeight) { + width = sWidth + height = sHeight + } else if (resizeHeight) { + height = (sHeight.toDouble() / sWidth.toDouble() * width).toInt() + } else if (resizeWidth) { + width = (sWidth.toDouble() / sHeight.toDouble() * height).toInt() + } + } + width = width.coerceAtLeast(suggestedMinimumWidth) + height = height.coerceAtLeast(suggestedMinimumHeight) + setMeasuredDimension(width, height) + } + + /** + * Handle touch events. One finger pans, and two finger pinch and zoom plus panning. + */ + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent): Boolean { + // During non-interruptible anims, ignore all touch events + if (anim?.interruptible == false) { + requestDisallowInterceptTouchEvent(true) + return true + } + + try { + anim?.listener?.onInterruptedByUser() + } catch (e: Exception) { + Log.w(TAG, "Error thrown by animation listener", e) + } finally { + anim = null + } + + // Abort if not ready + if (vTranslate == null) { + singleDetector?.onTouchEvent(event) + return true + } + // Detect flings, taps and double taps + if (!isQuickScaling && detector?.onTouchEvent(event) != false) { + isZooming = false + isPanning = false + maxTouchCount = 0 + return true + } + if (vTranslateStart == null) { + vTranslateStart = PointF(0F, 0F) + } + if (vTranslateBefore == null) { + vTranslateBefore = PointF(0F, 0F) + } + if (vCenterStart == null) { + vCenterStart = PointF(0F, 0F) + } + + // Store current values so we can send an event if they change + vTranslateBefore!!.set(vTranslate!!) + val handled = onTouchEventInternal(event) + sendStateChanged(scale, vTranslateBefore!!, ORIGIN_TOUCH) + return handled || super.onTouchEvent(event) + } + + private fun onTouchEventInternal(event: MotionEvent): Boolean { + val touchCount = event.pointerCount + when (event.action) { + MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_1_DOWN, MotionEvent.ACTION_POINTER_2_DOWN -> { + anim = null + requestDisallowInterceptTouchEvent(true) + maxTouchCount = maxTouchCount.coerceAtLeast(touchCount) + if (touchCount >= 2) { + if (isZoomEnabled) { + // Start pinch to zoom. Calculate distance between touch points and center point of the pinch. + scaleStart = scale + vDistStart = distance(event.getX(0), event.getX(1), event.getY(0), event.getY(1)) + vTranslateStart!![vTranslate!!.x] = vTranslate!!.y + vCenterStart!![(event.getX(0) + event.getX(1)) / 2] = (event.getY(0) + event.getY(1)) / 2 + } else { + // Abort all gestures on second touch + maxTouchCount = 0 + } + // Cancel long click timer + longClickJob?.cancel() + } else if (!isQuickScaling) { + // Start one-finger pan + vTranslateStart!![vTranslate!!.x] = vTranslate!!.y + vCenterStart!![event.x] = event.y + + // Start long click timer + longClickJob = coroutineScope.launch { + if (onLongClickListener != null) { + delay(600) + super@SubsamplingScaleImageView.setOnLongClickListener(onLongClickListener) + performLongClick() + super@SubsamplingScaleImageView.setOnLongClickListener(null) + } + } + } + return true + } + MotionEvent.ACTION_MOVE -> { + var consumed = false + if (maxTouchCount > 0) { + if (touchCount >= 2) { + // Calculate new distance between touch points, to scale and pan relative to start values. + val vDistEnd = distance(event.getX(0), event.getX(1), event.getY(0), event.getY(1)) + val vCenterEndX = (event.getX(0) + event.getX(1)) / 2 + val vCenterEndY = (event.getY(0) + event.getY(1)) / 2 + if (isZoomEnabled && (distance(vCenterStart!!.x, vCenterEndX, vCenterStart!!.y, vCenterEndY) > 5 || abs(vDistEnd - vDistStart) > 5 || isPanning)) { + isZooming = true + isPanning = true + consumed = true + val previousScale = scale.toDouble() + scale = (vDistEnd / vDistStart * scaleStart).coerceAtMost(maxScale) + if (scale <= minScale()) { + // Minimum scale reached so don't pan. Adjust start settings so any expand will zoom in. + vDistStart = vDistEnd + scaleStart = minScale() + vCenterStart!![vCenterEndX] = vCenterEndY + vTranslateStart!!.set(vTranslate!!) + } else if (panEnabled) { + // Translate to place the source image coordinate that was at the center of the pinch at the start + // at the center of the pinch now, to give simultaneous pan + zoom. + val vLeftStart = vCenterStart!!.x - vTranslateStart!!.x + val vTopStart = vCenterStart!!.y - vTranslateStart!!.y + val vLeftNow = vLeftStart * (scale / scaleStart) + val vTopNow = vTopStart * (scale / scaleStart) + vTranslate!!.x = vCenterEndX - vLeftNow + vTranslate!!.y = vCenterEndY - vTopNow + if (previousScale * sHeight < height && scale * sHeight >= height || previousScale * sWidth < width && scale * sWidth >= width) { + fitToBounds(true) + vCenterStart!![vCenterEndX] = vCenterEndY + vTranslateStart!!.set(vTranslate!!) + scaleStart = scale + vDistStart = vDistEnd + } + } else if (sRequestedCenter != null) { + // With a center specified from code, zoom around that point. + vTranslate!!.x = width / 2 - scale * sRequestedCenter!!.x + vTranslate!!.y = height / 2 - scale * sRequestedCenter!!.y + } else { + // With no requested center, scale around the image center. + vTranslate!!.x = width / 2 - scale * (sWidth / 2) + vTranslate!!.y = height / 2 - scale * (sHeight / 2) + } + fitToBounds(true) + refreshRequiredTiles(eagerLoadingEnabled) + } + } else if (isQuickScaling) { + // One finger zoom + // Stole Google's Magical Formula™ to make sure it feels the exact same + var dist = abs(quickScaleVStart!!.y - event.y) * 2 + quickScaleThreshold + if (quickScaleLastDistance == -1F) { + quickScaleLastDistance = dist + } + val isUpwards = event.y > quickScaleVLastPoint!!.y + quickScaleVLastPoint!![0F] = event.y + val spanDiff = abs(1 - dist / quickScaleLastDistance) * 0.5F + if (spanDiff > 0.03F || quickScaleMoved) { + quickScaleMoved = true + var multiplier = 1F + if (quickScaleLastDistance > 0) { + multiplier = if (isUpwards) 1 + spanDiff else 1 - spanDiff + } + val previousScale = scale.toDouble() + scale = (scale * multiplier).coerceIn(minScale(), maxScale) + if (panEnabled) { + val vLeftStart = vCenterStart!!.x - vTranslateStart!!.x + val vTopStart = vCenterStart!!.y - vTranslateStart!!.y + val vLeftNow = vLeftStart * (scale / scaleStart) + val vTopNow = vTopStart * (scale / scaleStart) + vTranslate!!.x = vCenterStart!!.x - vLeftNow + vTranslate!!.y = vCenterStart!!.y - vTopNow + if (previousScale * sHeight < height && scale * sHeight >= height || previousScale * sWidth < width && scale * sWidth >= width) { + fitToBounds(true) + vCenterStart!!.set(sourceToViewCoord(quickScaleSCenter)!!) + vTranslateStart!!.set(vTranslate!!) + scaleStart = scale + dist = 0F + } + } else if (sRequestedCenter != null) { + // With a center specified from code, zoom around that point. + vTranslate!!.x = width / 2 - scale * sRequestedCenter!!.x + vTranslate!!.y = height / 2 - scale * sRequestedCenter!!.y + } else { + // With no requested center, scale around the image center. + vTranslate!!.x = width / 2 - scale * (sWidth / 2) + vTranslate!!.y = height / 2 - scale * (sHeight / 2) + } + } + quickScaleLastDistance = dist + fitToBounds(true) + refreshRequiredTiles(eagerLoadingEnabled) + consumed = true + } else if (!isZooming) { + // One finger pan - translate the image. We do this calculation even with pan disabled so click + // and long click behaviour is preserved. + val dx = abs(event.x - vCenterStart!!.x) + val dy = abs(event.y - vCenterStart!!.y) + + //On the Samsung S6 long click event does not work, because the dx > 5 usually true + val offset = density * 5 + if (dx > offset || dy > offset || isPanning) { + consumed = true + vTranslate!!.x = vTranslateStart!!.x + (event.x - vCenterStart!!.x) + vTranslate!!.y = vTranslateStart!!.y + (event.y - vCenterStart!!.y) + val lastX = vTranslate!!.x + val lastY = vTranslate!!.y + fitToBounds(true) + val atXEdge = lastX != vTranslate!!.x + val atYEdge = lastY != vTranslate!!.y + val edgeXSwipe = atXEdge && dx > dy && !isPanning + val edgeYSwipe = atYEdge && dy > dx && !isPanning + val yPan = lastY == vTranslate!!.y && dy > offset * 3 + if (!edgeXSwipe && !edgeYSwipe && (!atXEdge || !atYEdge || yPan || isPanning)) { + isPanning = true + } else if (dx > offset || dy > offset) { + // Haven't panned the image, and we're at the left or right edge. Switch to page swipe. + maxTouchCount = 0 + longClickJob?.cancel() + requestDisallowInterceptTouchEvent(false) + } + if (!panEnabled) { + vTranslate!!.x = vTranslateStart!!.x + vTranslate!!.y = vTranslateStart!!.y + requestDisallowInterceptTouchEvent(false) + } + refreshRequiredTiles(eagerLoadingEnabled) + } + } + } + if (consumed) { + longClickJob?.cancel() + invalidate() + return true + } + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_POINTER_2_UP -> { + longClickJob?.cancel() + if (isQuickScaling) { + isQuickScaling = false + if (!quickScaleMoved && quickScaleSCenter != null && vCenterStart != null) { + doubleTapZoom(quickScaleSCenter!!, vCenterStart!!) + } + } + if (maxTouchCount > 0 && (isZooming || isPanning)) { + if (isZooming && touchCount == 2) { + // Convert from zoom to pan with remaining touch + isPanning = true + vTranslateStart!![vTranslate!!.x] = vTranslate!!.y + if (event.actionIndex == 1) { + vCenterStart!![event.getX(0)] = event.getY(0) + } else { + vCenterStart!![event.getX(1)] = event.getY(1) + } + } + if (touchCount < 3) { + // End zooming when only one touch point + isZooming = false + } + if (touchCount < 2) { + // End panning when no touch points + isPanning = false + maxTouchCount = 0 + } + // Trigger load of tiles now required + refreshRequiredTiles(true) + return true + } + if (touchCount == 1) { + isZooming = false + isPanning = false + maxTouchCount = 0 + } + return true + } + } + return false + } + + private fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) { + parent?.requestDisallowInterceptTouchEvent(disallowIntercept) + } + + /** + * Double tap zoom handler triggered from gesture detector or on touch, depending on whether + * quick scale is enabled. + */ + private fun doubleTapZoom(sCenter: PointF, vFocus: PointF) { + if (!panEnabled) { + if (sRequestedCenter != null) { + // With a center specified from code, zoom around that point. + sCenter.x = sRequestedCenter!!.x + sCenter.y = sRequestedCenter!!.y + } else { + // With no requested center, scale around the image center. + sCenter.x = (sWidth / 2).toFloat() + sCenter.y = (sHeight / 2).toFloat() + } + } + val doubleTapZoomScale = doubleTapZoomScale.coerceAtMost(maxScale) + val zoomIn = scale <= doubleTapZoomScale * 0.9 || scale == minScale + val targetScale = if (zoomIn) doubleTapZoomScale else minScale() + if (doubleTapZoomStyle == ZOOM_FOCUS_CENTER_IMMEDIATE) { + setScaleAndCenter(targetScale, sCenter) + } else if (doubleTapZoomStyle == ZOOM_FOCUS_CENTER || !zoomIn || !panEnabled) { + AnimationBuilder(targetScale, sCenter) + .withInterruptible(false) + .withDuration(doubleTapZoomDuration.toLong()) + .withOrigin(ORIGIN_DOUBLE_TAP_ZOOM) + .start() + } else if (doubleTapZoomStyle == ZOOM_FOCUS_FIXED) { + AnimationBuilder(targetScale, sCenter, vFocus) + .withInterruptible(false) + .withDuration(doubleTapZoomDuration.toLong()) + .withOrigin(ORIGIN_DOUBLE_TAP_ZOOM) + .start() + } + invalidate() + } + + /** + * Draw method should not be called until the view has dimensions so the first calls are used as triggers to calculate + * the scaling and tiling required. Once the view is setup, tiles are displayed as they are loaded. + */ + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + // If image or view dimensions are not known yet, abort. + if (sWidth == 0 || sHeight == 0 || width == 0 || height == 0) { + return + } + + // When using tiles, on first render with no tile map ready, initialise it and kick off async base image loading. + if (tileMap.isEmpty() && decoder != null) { + initialiseBaseLayer(getMaxBitmapDimensions(canvas)) + } + + // If image has been loaded or supplied as a bitmap, onDraw may be the first time the view has + // dimensions and therefore the first opportunity to set scale and translate. If this call returns + // false there is nothing to be drawn so return immediately. + if (!checkReady()) { + return + } + + // Set scale and translate before draw. + preDraw() + + // If animating scale, calculate current scale and center with easing equations + if (anim != null && anim!!.vFocusStart != null) { + // Store current values so we can send an event if they change + val scaleBefore = scale + if (vTranslateBefore == null) { + vTranslateBefore = PointF(0F, 0F) + } + vTranslateBefore!!.set(vTranslate!!) + var scaleElapsed = System.currentTimeMillis() - anim!!.time + val finished = scaleElapsed > anim!!.duration + scaleElapsed = scaleElapsed.coerceAtMost(anim!!.duration) + scale = ease( + anim!!.easing, + scaleElapsed, + anim!!.scaleStart, + anim!!.scaleEnd - anim!!.scaleStart, + anim!!.duration + ) + + // Apply required animation to the focal point + val vFocusNowX = ease( + anim!!.easing, + scaleElapsed, + anim!!.vFocusStart!!.x, + anim!!.vFocusEnd.x - anim!!.vFocusStart!!.x, + anim!!.duration + ) + val vFocusNowY = ease( + anim!!.easing, + scaleElapsed, + anim!!.vFocusStart!!.y, + anim!!.vFocusEnd.y - anim!!.vFocusStart!!.y, + anim!!.duration + ) + // Find out where the focal point is at this scale and adjust its position to follow the animation path + vTranslate!!.x -= sourceToViewX(anim!!.sCenterEnd.x) - vFocusNowX + vTranslate!!.y -= sourceToViewY(anim!!.sCenterEnd.y) - vFocusNowY + + // For translate anims, showing the image non-centered is never allowed, for scaling anims it is during the animation. + fitToBounds(finished || anim!!.scaleStart == anim!!.scaleEnd) + sendStateChanged(scaleBefore, vTranslateBefore!!, anim!!.origin) + refreshRequiredTiles(finished) + if (finished) { + try { + anim?.listener?.onComplete() + } catch (e: Exception) { + Log.w(TAG, "Error thrown by animation listener", e) + } + anim = null + } + invalidate() + } + if (tileMap.isNotEmpty() && isBaseLayerReady) { + // Optimum sample size for current scale + val sampleSize = calculateInSampleSize(scale).coerceAtMost(fullImageSampleSize) + + // First check for missing tiles - if there are any we need the base layer underneath to avoid gaps + var hasMissingTiles = false + tileMap.forEach { (key, value) -> + if (key == sampleSize) { + value.forEach { tile -> + if (tile.visible && (tile.loading || tile.bitmap == null)) { + hasMissingTiles = true + } + } + } + } + + // Render all loaded tiles. LinkedHashMap used for bottom up rendering - lower res tiles underneath. + tileMap.forEach { (key, value) -> + if (key == sampleSize || hasMissingTiles) { + value.forEach { tile -> + sourceToViewRect(tile.sRect, tile.vRect) + if (!tile.loading && tile.bitmap != null) { + tileBgPaint?.let { + canvas.drawRect(tile.vRect, it) + } + mtrx.reset() + val tileWidth = tile.bitmap!!.width.toFloat() + val tileHeight = tile.bitmap!!.height.toFloat() + setMatrixArray( + srcArray, + 0F, + 0F, + tileWidth, + 0F, + tileWidth, + tileHeight, + 0F, + tileHeight + ) + setMatrixArray( + dstArray, + tile.vRect.left.toFloat(), + tile.vRect.top.toFloat(), + tile.vRect.right.toFloat(), + tile.vRect.top.toFloat(), + tile.vRect.right.toFloat(), + tile.vRect.bottom.toFloat(), + tile.vRect.left.toFloat(), + tile.vRect.bottom.toFloat() + ) + mtrx.setPolyToPoly(srcArray, 0, dstArray, 0, 4) + canvas.drawBitmap(tile.bitmap!!, mtrx, bitmapPaint) + if (debug) { + canvas.drawRect(tile.vRect, debugLinePaint) + } + } else if (tile.loading && debug) { + canvas.drawText( + "LOADING", + (tile.vRect.left + px(5)).toFloat(), + (tile.vRect.top + px(35)).toFloat(), + debugTextPaint + ) + } + if (tile.visible && debug) { + canvas.drawText( + "ISS " + tile.sampleSize + " RECT " + tile.sRect.top + "," + tile.sRect.left + "," + tile.sRect.bottom + "," + tile.sRect.right, + (tile.vRect.left + px(5)).toFloat(), + (tile.vRect.top + px(15)).toFloat(), + debugTextPaint + ) + } + } + } + } + } else if (bitmap != null && !bitmap!!.isRecycled) { + val xScale = scale + val yScale = scale + mtrx.reset() + mtrx.postScale(xScale, yScale) + mtrx.postTranslate(vTranslate!!.x, vTranslate!!.y) + tileBgPaint?.let { + sRect.setEmpty() + sRect[0F, 0F, sWidth.toFloat()] = sHeight.toFloat() + mtrx.mapRect(sRect) + canvas.drawRect(sRect, it) + } + canvas.drawBitmap(bitmap!!, mtrx, bitmapPaint) + } + if (debug) { + canvas.drawText( + "Scale: " + String.format(Locale.ENGLISH, "%.2f", scale) + " (" + String.format( + Locale.ENGLISH, + "%.2f", + minScale() + ) + " - " + String.format( + Locale.ENGLISH, "%.2f", maxScale + ) + ")", px(5).toFloat(), px(15).toFloat(), debugTextPaint + ) + canvas.drawText( + "Translate: " + String.format(Locale.ENGLISH, "%.2f", vTranslate!!.x) + ":" + String.format( + Locale.ENGLISH, "%.2f", vTranslate!!.y + ), px(5).toFloat(), px(30).toFloat(), debugTextPaint + ) + val center = center + canvas.drawText( + "Source center: " + String.format(Locale.ENGLISH, "%.2f", center!!.x) + ":" + String.format( + Locale.ENGLISH, "%.2f", center.y + ), px(5).toFloat(), px(45).toFloat(), debugTextPaint + ) + if (anim != null) { + val vCenterStart = sourceToViewCoord(anim!!.sCenterStart) + val vCenterEndRequested = sourceToViewCoord(anim!!.sCenterEndRequested) + val vCenterEnd = sourceToViewCoord(anim!!.sCenterEnd) + canvas.drawCircle(vCenterStart!!.x, vCenterStart.y, px(10).toFloat(), debugLinePaint) + debugLinePaint.color = Color.RED + canvas.drawCircle(vCenterEndRequested!!.x, vCenterEndRequested.y, px(20).toFloat(), debugLinePaint) + debugLinePaint.color = Color.BLUE + canvas.drawCircle(vCenterEnd!!.x, vCenterEnd.y, px(25).toFloat(), debugLinePaint) + debugLinePaint.color = Color.CYAN + canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), px(30).toFloat(), debugLinePaint) + } + if (vCenterStart != null) { + debugLinePaint.color = Color.RED + canvas.drawCircle(vCenterStart!!.x, vCenterStart!!.y, px(20).toFloat(), debugLinePaint) + } + if (quickScaleSCenter != null) { + debugLinePaint.color = Color.BLUE + canvas.drawCircle( + sourceToViewX(quickScaleSCenter!!.x), + sourceToViewY(quickScaleSCenter!!.y), + px(35).toFloat(), + debugLinePaint + ) + } + if (quickScaleVStart != null && isQuickScaling) { + debugLinePaint.color = Color.CYAN + canvas.drawCircle(quickScaleVStart!!.x, quickScaleVStart!!.y, px(30).toFloat(), debugLinePaint) + } + debugLinePaint.color = Color.MAGENTA + } + } + + /** + * Helper method for setting the values of a tile matrix array. + */ + private fun setMatrixArray( + array: FloatArray, + f0: Float, + f1: Float, + f2: Float, + f3: Float, + f4: Float, + f5: Float, + f6: Float, + f7: Float + ) { + array[0] = f0 + array[1] = f1 + array[2] = f2 + array[3] = f3 + array[4] = f4 + array[5] = f5 + array[6] = f6 + array[7] = f7 + } + + /** + * Checks whether the base layer of tiles or full size bitmap is ready. + */ + private val isBaseLayerReady: Boolean + get() { + if (bitmap != null) { + return true + } else if (tileMap.isNotEmpty()) { + var baseLayerReady = true + tileMap.forEach { (key, value) -> + if (key == fullImageSampleSize) { + value.forEach { tile -> + if (tile.loading || tile.bitmap == null) { + baseLayerReady = false + } + } + } + } + return baseLayerReady + } + return false + } + + /** + * Check whether view and image dimensions are known and either a preview, full size image or + * base layer tiles are loaded. First time, send ready event to listener. The next draw will + * display an image. + */ + private fun checkReady(): Boolean { + val ready = width > 0 && height > 0 && sWidth > 0 && sHeight > 0 && (bitmap != null || isBaseLayerReady) + if (!isReady && ready) { + preDraw() + isReady = true + onReady() + onImageEventListener?.onReady() + } + return ready + } + + /** + * Check whether either the full size bitmap or base layer tiles are loaded. First time, send image + * loaded event to listener. + */ + private fun checkImageLoaded(): Boolean { + val imageLoaded = isBaseLayerReady + if (!isImageLoaded && imageLoaded) { + preDraw() + isImageLoaded = true + onImageLoaded() + onImageEventListener?.onImageLoaded() + } + return imageLoaded + } + + private fun loadTile(tile: Tile) = coroutineScope.launch { + try { + val imageDecoder = decoder ?: return@launch + if (!imageDecoder.isReady || !tile.visible) return@launch + + debug( + "loadTile, tile.sRect=%s, tile.sampleSize=%d", + tile.sRect, + tile.sampleSize + ) + decoderLock.readLock().lock() + try { + if (imageDecoder.isReady) { + tile.bitmap = imageDecoder.decodeRegion(tile.fileSRect, tile.sampleSize) + onTileLoaded() + } + } finally { + decoderLock.readLock().unlock() + } + } catch (e: Exception) { + Log.e(TAG, "Failed to decode tile", e) + onImageEventListener?.onTileLoadError(e) + } catch (e: OutOfMemoryError) { + Log.e(TAG, "Failed to decode tile - OutOfMemoryError", e) + onImageEventListener?.onTileLoadError(Exception(e)) + } finally { + tile.loading = false + } + } + + /** + * Called on first draw when the view has dimensions. Calculates the initial sample size and starts async loading of + * the base layer image - the whole source subsampled as necessary. + */ + @Synchronized + private fun initialiseBaseLayer(maxTileDimensions: Point) { + debug("initialiseBaseLayer maxTileDimensions=%dx%d", maxTileDimensions.x, maxTileDimensions.y) + satTemp = ScaleAndTranslate(0F, PointF(0F, 0F)) + fitToBounds(true, satTemp!!) + + // Load double resolution - next level will be split into four tiles and at the center all four are required, + // so don't bother with tiling until the next level 16 tiles are needed. + fullImageSampleSize = calculateInSampleSize(satTemp!!.scale) + if (fullImageSampleSize > 1) { + fullImageSampleSize /= 2 + } + initialiseTileMap(maxTileDimensions) + tileMap[fullImageSampleSize]?.forEach { + loadTile(it) + } + refreshRequiredTiles(true) + } + + /** + * Loads the optimum tiles for display at the current scale and translate, so the screen can be filled with tiles + * that are at least as high resolution as the screen. Frees up bitmaps that are now off the screen. + * + * @param load Whether to load the new tiles needed. Use false while scrolling/panning for performance. + */ + private fun refreshRequiredTiles(load: Boolean) { + if (decoder == null || tileMap.isEmpty()) { + return + } + val sampleSize = calculateInSampleSize(scale).coerceAtMost(fullImageSampleSize) + + // Load tiles of the correct sample size that are on screen. Discard tiles off screen, and those that are higher + // resolution than required, or lower res than required but not the base layer, so the base layer is always present. + tileMap.forEach { (_, value) -> + value.forEach { tile -> + if (tile.sampleSize < sampleSize || tile.sampleSize > sampleSize && tile.sampleSize != fullImageSampleSize) { + tile.visible = false + tile.bitmap?.recycle() + tile.bitmap = null + } + if (tile.sampleSize == sampleSize) { + if (tileVisible(tile)) { + tile.visible = true + if (!tile.loading && tile.bitmap == null && load) { + loadTile(tile) + } + } else if (tile.sampleSize != fullImageSampleSize) { + tile.visible = false + tile.bitmap?.recycle() + tile.bitmap = null + } + } else if (tile.sampleSize == fullImageSampleSize) { + tile.visible = true + } + } + } + } + + /** + * Determine whether tile is visible. + */ + private fun tileVisible(tile: Tile): Boolean { + val sVisLeft = viewToSourceX(0F) + val sVisRight = viewToSourceX(width.toFloat()) + val sVisTop = viewToSourceY(0F) + val sVisBottom = viewToSourceY(height.toFloat()) + return !(sVisLeft > tile.sRect.right || tile.sRect.left > sVisRight || sVisTop > tile.sRect.bottom || tile.sRect.top > sVisBottom) + } + + /** + * Sets scale and translate ready for the next draw. + */ + private fun preDraw() { + if (width == 0 || height == 0 || sWidth <= 0 || sHeight <= 0) { + return + } + + // If waiting to translate to new center position, set translate now + if (sPendingCenter != null && pendingScale >= 0) { + scale = pendingScale + if (vTranslate == null) { + vTranslate = PointF() + } + vTranslate!!.x = width / 2 - scale * sPendingCenter!!.x + vTranslate!!.y = height / 2 - scale * sPendingCenter!!.y + sPendingCenter = null + pendingScale = -1F + fitToBounds(true) + refreshRequiredTiles(true) + } + + // On first display of base image set up position, and in other cases make sure scale is correct. + fitToBounds(false) + } + + /** + * Calculates sample size to fit the source image in given bounds. + */ + private fun calculateInSampleSize(_scale: Float): Int { + var scale = _scale + if (minimumTileDpi > 0) { + val metrics = resources.displayMetrics + val averageDpi = (metrics.xdpi + metrics.ydpi) / 2 + scale *= minimumTileDpi / averageDpi + } + val reqWidth = (sWidth * scale).toInt() + val reqHeight = (sHeight * scale).toInt() + + // Raw height and width of image + var inSampleSize = 1 + if (reqWidth == 0 || reqHeight == 0) { + return 32 + } + if (sHeight > reqHeight || sWidth > reqWidth) { + // Calculate ratios of height and width to requested height and width + val heightRatio = (sHeight.toFloat() / reqHeight.toFloat()).roundToInt() + val widthRatio = (sWidth.toFloat() / reqWidth.toFloat()).roundToInt() + + // Choose the smallest ratio as inSampleSize value, this will guarantee + // a final image with both dimensions larger than or equal to the + // requested height and width. + inSampleSize = if (heightRatio < widthRatio) heightRatio else widthRatio + } + + // We want the actual sample size that will be used, so round down to nearest power of 2. + var power = 1 + while (power * 2 < inSampleSize) { + power *= 2 + } + return power + } + + /** + * Adjusts hypothetical future scale and translate values to keep scale within the allowed range and the image on screen. Minimum scale + * is set so one dimension fills the view and the image is centered on the other dimension. Used to calculate what the target of an + * animation should be. + * + * @param _center Whether the image should be centered in the dimension it's too small to fill. While animating this can be false to avoid changes in direction as bounds are reached. + * @param sat The scale we want and the translation we're aiming for. The values are adjusted to be valid. + */ + private fun fitToBounds(_center: Boolean, sat: ScaleAndTranslate) { + var center = _center + if (panLimit == PAN_LIMIT_OUTSIDE && isReady) { + center = false + } + val vTranslate = sat.vTranslate + val scale = limitedScale(sat.scale) + val scaleWidth = scale * sWidth + val scaleHeight = scale * sHeight + val extra = panLimit == PAN_LIMIT_INSIDE + val extraLeft = if (extra) extraSpaceLeft else 0F + val extraRight = if (extra) extraSpaceRight else 0F + val extraTop = if (extra) extraSpaceTop else 0F + val extraBottom = if (extra) extraSpaceBottom else 0F + if (panLimit == PAN_LIMIT_CENTER && isReady) { + vTranslate.x = vTranslate.x.coerceAtLeast(width / 2 - scaleWidth) + vTranslate.y = vTranslate.y.coerceAtLeast(height / 2 - scaleHeight) + } else if (center) { + vTranslate.x = vTranslate.x.coerceAtLeast(width - scaleWidth - extraRight) + vTranslate.y = vTranslate.y.coerceAtLeast(height - scaleHeight - extraBottom) + } else { + vTranslate.x = vTranslate.x.coerceAtLeast(-scaleWidth - extraRight) + vTranslate.y = vTranslate.y.coerceAtLeast(-scaleHeight - extraBottom) + } + + // Asymmetric padding adjustments + val xPaddingRatio = + if (paddingLeft > 0 || paddingRight > 0) paddingLeft / (paddingLeft + paddingRight).toFloat() else 0.5F + val yPaddingRatio = + if (paddingTop > 0 || paddingBottom > 0) paddingTop / (paddingTop + paddingBottom).toFloat() else 0.5F + + val maxTx: Float + val maxTy: Float + if (panLimit == PAN_LIMIT_CENTER && isReady) { + maxTx = (width / 2F).coerceAtLeast(0F) + maxTy = (height / 2F).coerceAtLeast(0F) + } else if (center) { + maxTx = ((width - scaleWidth) * xPaddingRatio).coerceAtLeast(extraLeft) + maxTy = ((height - scaleHeight) * yPaddingRatio).coerceAtLeast(extraTop) + } else { + maxTx = width.toFloat().coerceAtLeast(extraLeft) + maxTy = height.toFloat().coerceAtLeast(extraTop) + } + + vTranslate.x = vTranslate.x.coerceAtMost(maxTx) + vTranslate.y = vTranslate.y.coerceAtMost(maxTy) + sat.scale = scale + } + + /** + * Adjusts current scale and translate values to keep scale within the allowed range and the image on screen. Minimum scale + * is set so one dimension fills the view and the image is centered on the other dimension. + * + * @param center Whether the image should be centered in the dimension it's too small to fill. While animating this can be false to avoid changes in direction as bounds are reached. + */ + private fun fitToBounds(center: Boolean) { + var init = false + if (vTranslate == null) { + init = true + vTranslate = PointF(0F, 0F) + } + if (satTemp == null) { + satTemp = ScaleAndTranslate(0F, PointF(0F, 0F)) + } + satTemp!!.scale = scale + satTemp!!.vTranslate.set(vTranslate!!) + fitToBounds(center, satTemp!!) + scale = satTemp!!.scale + vTranslate!!.set(satTemp!!.vTranslate) + if (init) { + vTranslate!!.set(vTranslateForSCenter((sWidth / 2).toFloat(), (sHeight / 2).toFloat(), scale)) + } + } + + /** + * Once source image and view dimensions are known, creates a map of sample size to tile grid. + */ + private fun initialiseTileMap(maxTileDimensions: Point) { + debug("initialiseTileMap maxTileDimensions=%dx%d", maxTileDimensions.x, maxTileDimensions.y) + tileMap.clear() + var sampleSize = fullImageSampleSize + var xTiles = 1 + var yTiles = 1 + while (true) { + var sTileWidth = sWidth / xTiles + var sTileHeight = sHeight / yTiles + var subTileWidth = sTileWidth / sampleSize + var subTileHeight = sTileHeight / sampleSize + while (subTileWidth + xTiles + 1 > maxTileDimensions.x || subTileWidth > width * 1.25 && sampleSize < fullImageSampleSize) { + xTiles += 1 + sTileWidth = sWidth / xTiles + subTileWidth = sTileWidth / sampleSize + } + while (subTileHeight + yTiles + 1 > maxTileDimensions.y || subTileHeight > height * 1.25 && sampleSize < fullImageSampleSize) { + yTiles += 1 + sTileHeight = sHeight / yTiles + subTileHeight = sTileHeight / sampleSize + } + val tileGrid: MutableList = ArrayList(xTiles * yTiles) + for (x in 0 until xTiles) { + for (y in 0 until yTiles) { + val sRect = Rect( + x * sTileWidth, + y * sTileHeight, + if (x == xTiles - 1) sWidth else (x + 1) * sTileWidth, + if (y == yTiles - 1) sHeight else (y + 1) * sTileHeight + ) + val tile = Tile( + sRect = sRect, + sampleSize = sampleSize, + vRect = Rect(0, 0, 0, 0), + fileSRect = Rect(sRect) + ) + tileGrid.add(tile) + } + } + tileMap[sampleSize] = tileGrid + sampleSize /= if (sampleSize == 1) break else 2 + } + } + + /** + * Called by worker task when decoder is ready and image size and EXIF orientation is known. + */ + @Synchronized + private fun onTilesInited(decoder: Decoder, sWidth: Int, sHeight: Int) { + debug("onTilesInited sWidth=%d, sHeight=%d", sWidth, sHeight) + // If actual dimensions don't match the declared size, reset everything. + if (this.sWidth > 0 && this.sHeight > 0 && (this.sWidth != sWidth || this.sHeight != sHeight)) { + reset(false) + if (bitmap != null) { + if (!bitmapIsCached) { + bitmap!!.recycle() + } + bitmap = null + bitmapIsCached = false + } + } + this.decoder = decoder + this.sWidth = sWidth + this.sHeight = sHeight + checkReady() + debug("maxTile $maxTileWidth, $maxTileHeight view size: $width, $height") + if (!checkImageLoaded() && maxTileWidth > 0 && maxTileWidth != TILE_SIZE_AUTO && maxTileHeight > 0 && maxTileHeight != TILE_SIZE_AUTO && width > 0 && height > 0) { + initialiseBaseLayer(Point(maxTileWidth, maxTileHeight)) + } + invalidate() + requestLayout() + } + + /** + * Called by worker task when a tile has loaded. Redraws the view. + */ + @Synchronized + private fun onTileLoaded() { + debug("onTileLoaded") + checkReady() + checkImageLoaded() + if (isBaseLayerReady && bitmap != null) { + if (!bitmapIsCached) { + bitmap!!.recycle() + } + bitmap = null + bitmapIsCached = false + } + invalidate() + } + + /** + * Called by worker task when full size image bitmap is ready (tiling is disabled). + */ + @Synchronized + private fun onImageLoaded(bitmap: Bitmap, bitmapIsCached: Boolean) { + debug("onImageLoaded") + // If actual dimensions don't match the declared size, reset everything. + if (sWidth > 0 && sHeight > 0 && (sWidth != bitmap.width || sHeight != bitmap.height)) { + reset(false) + } + if (!this.bitmapIsCached) { + this.bitmap?.recycle() + } + this.bitmapIsCached = bitmapIsCached + this.bitmap = bitmap + sWidth = bitmap.width + sHeight = bitmap.height + val ready = checkReady() + val imageLoaded = checkImageLoaded() + if (ready || imageLoaded) { + invalidate() + requestLayout() + } + } + + /** + * Set scale, center and orientation from saved state. + */ + private fun restoreState(state: ImageViewState?) { + if (state != null) { + pendingScale = state.scale + sPendingCenter = state.center + invalidate() + } + } + + /** + * Set border crop of non-filled (white or black) content. + * + * @param cropBorders Whether to crop image borders. + */ + fun setCropBorders(cropBorders: Boolean) { + this.cropBorders = cropBorders + } + + /** + * By default the View automatically calculates the optimal tile size. Set this to override this, and force an upper limit to the dimensions of the generated tiles. Passing [.TILE_SIZE_AUTO] will re-enable the default behaviour. + * + * @param maxPixels Maximum tile size X and Y in pixels. + */ + fun setMaxTileSize(maxPixels: Int) { + maxTileWidth = maxPixels + maxTileHeight = maxPixels + } + + /** + * By default the View automatically calculates the optimal tile size. Set this to override this, and force an upper limit to the dimensions of the generated tiles. Passing [.TILE_SIZE_AUTO] will re-enable the default behaviour. + * + * @param maxPixelsX Maximum tile width. + * @param maxPixelsY Maximum tile height. + */ + fun setMaxTileSize(maxPixelsX: Int, maxPixelsY: Int) { + maxTileWidth = maxPixelsX + maxTileHeight = maxPixelsY + } + + /** + * Use canvas max bitmap width and height instead of the default 2048, to avoid redundant tiling. + */ + private fun getMaxBitmapDimensions(canvas: Canvas): Point { + return Point( + canvas.maximumBitmapWidth.coerceAtMost(maxTileWidth), + canvas.maximumBitmapHeight.coerceAtMost(maxTileHeight) + ) + } + + /** + * Converts source rectangle from tile, which treats the image file as if it were in the correct orientation already, + * to the rectangle of the image that needs to be loaded. + */ + @AnyThread + private fun fileSRect(sRect: Rect, target: Rect) { + target.set(sRect) + } + + /** + * Pythagoras distance between two points. + */ + private fun distance(x0: Float, x1: Float, y0: Float, y1: Float): Float { + val x = x0 - x1 + val y = y0 - y1 + return sqrt((x * x + y * y).toDouble()).toFloat() + } + + /** + * Releases all resources the view is using and resets the state, nulling any fields that use significant memory. + * After you have called this method, the view can be re-used by setting a new image. Settings are remembered + * but state (scale and center) is forgotten. You can restore these yourself if required. + */ + fun recycle() { + reset(true) + tileBgPaint = null + } + + /** + * Convert screen to source x coordinate. + */ + private fun viewToSourceX(vx: Float): Float { + return if (vTranslate == null) Float.NaN else (vx - vTranslate!!.x) / scale + } + + /** + * Convert screen to source y coordinate. + */ + private fun viewToSourceY(vy: Float): Float { + return if (vTranslate == null) Float.NaN else (vy - vTranslate!!.y) / scale + } + + /** + * Converts a rectangle within the view to the corresponding rectangle from the source file, taking + * into account the current scale, translation, orientation and clipped region. This can be used + * to decode a bitmap from the source file. + * + * + * This method will only work when the image has fully initialised, after [.isReady] returns + * true. It is not guaranteed to work with preloaded bitmaps. + * + * + * The result is written to the fRect argument. Re-use a single instance for efficiency. + * + * @param vRect rectangle representing the view area to interpret. + * @param fRect rectangle instance to which the result will be written. Re-use for efficiency. + */ + fun viewToFileRect(vRect: Rect, fRect: Rect) { + if (vTranslate == null || !isReady) return + + fRect[ + viewToSourceX(vRect.left.toFloat()).toInt(), + viewToSourceY(vRect.top.toFloat()).toInt(), + viewToSourceX(vRect.right.toFloat()).toInt() + ] = viewToSourceY(vRect.bottom.toFloat()).toInt() + fileSRect(fRect, fRect) + fRect[ + fRect.left.coerceAtLeast(0), + fRect.top.coerceAtLeast(0), + fRect.right.coerceAtLeast(sWidth) + ] = fRect.bottom.coerceAtLeast(sHeight) + if (!sRegion.isEmpty) { + fRect.offset(sRegion.left, sRegion.top) + } + } + + /** + * Find the area of the source file that is currently visible on screen, taking into account the + * current scale, translation, orientation and clipped region. This is a convenience method; see + * [.viewToFileRect]. + * + * @param fRect rectangle instance to which the result will be written. Re-use for efficiency. + */ + fun visibleFileRect(fRect: Rect) { + if (vTranslate == null || !isReady) { + return + } + fRect[0, 0, width] = height + viewToFileRect(fRect, fRect) + } + + /** + * Convert screen coordinate to source coordinate. + * + * @param vxy view X/Y coordinate. + * @return a coordinate representing the corresponding source coordinate. + */ + fun viewToSourceCoord(vxy: PointF): PointF? { + return viewToSourceCoord(vxy.x, vxy.y, PointF()) + } + + /** + * Convert screen coordinate to source coordinate. + * + * @param vxy view coordinates to convert. + * @param sTarget target object for result. The same instance is also returned. + * @return source coordinates. This is the same instance passed to the sTarget param. + */ + fun viewToSourceCoord(vxy: PointF, sTarget: PointF): PointF? { + return viewToSourceCoord(vxy.x, vxy.y, sTarget) + } + + /** + * Convert screen coordinate to source coordinate. + * + * @param vx view X coordinate. + * @param vy view Y coordinate. + * @param sTarget target object for result. The same instance is also returned. + * @return a coordinate representing the corresponding source coordinate. This is the same instance passed to the sTarget param. + */ + @JvmOverloads + fun viewToSourceCoord(vx: Float, vy: Float, sTarget: PointF = PointF()): PointF? { + if (vTranslate == null) { + return null + } + sTarget[viewToSourceX(vx)] = viewToSourceY(vy) + return sTarget + } + + /** + * Convert source to view x coordinate. + */ + private fun sourceToViewX(sx: Float): Float { + return if (vTranslate == null) Float.NaN else sx * scale + vTranslate!!.x + } + + /** + * Convert source to view y coordinate. + */ + private fun sourceToViewY(sy: Float): Float { + return if (vTranslate == null) Float.NaN else sy * scale + vTranslate!!.y + } + + /** + * Convert source coordinate to view coordinate. + * + * @param sxy source coordinates to convert. + * @return view coordinates. + */ + fun sourceToViewCoord(sxy: PointF?): PointF? { + return sourceToViewCoord(sxy!!.x, sxy.y, PointF()) + } + + /** + * Convert source coordinate to view coordinate. + * + * @param sxy source coordinates to convert. + * @param vTarget target object for result. The same instance is also returned. + * @return view coordinates. This is the same instance passed to the vTarget param. + */ + fun sourceToViewCoord(sxy: PointF, vTarget: PointF): PointF? { + return sourceToViewCoord(sxy.x, sxy.y, vTarget) + } + + /** + * Convert source coordinate to view coordinate. + * + * @param sx source X coordinate. + * @param sy source Y coordinate. + * @param vTarget target object for result. The same instance is also returned. + * @return view coordinates. This is the same instance passed to the vTarget param. + */ + @JvmOverloads + fun sourceToViewCoord(sx: Float, sy: Float, vTarget: PointF = PointF()): PointF? { + if (vTranslate == null) { + return null + } + vTarget[sourceToViewX(sx)] = sourceToViewY(sy) + return vTarget + } + + /** + * Convert source rect to screen rect, integer values. + */ + private fun sourceToViewRect(sRect: Rect, vTarget: Rect) { + vTarget[ + sourceToViewX(sRect.left.toFloat()).toInt(), + sourceToViewY(sRect.top.toFloat()).toInt(), + sourceToViewX(sRect.right.toFloat()).toInt() + ] = sourceToViewY(sRect.bottom.toFloat()).toInt() + } + + /** + * Get the translation required to place a given source coordinate at the center of the screen, with the center + * adjusted for asymmetric padding. Accepts the desired scale as an argument, so this is independent of current + * translate and scale. The result is fitted to bounds, putting the image point as near to the screen center as permitted. + */ + private fun vTranslateForSCenter(sCenterX: Float, sCenterY: Float, scale: Float): PointF { + val vxCenter = paddingLeft + (width - paddingRight - paddingLeft) / 2 + val vyCenter = paddingTop + (height - paddingBottom - paddingTop) / 2 + if (satTemp == null) { + satTemp = ScaleAndTranslate(0F, PointF(0F, 0F)) + } + satTemp!!.scale = scale + satTemp!!.vTranslate[vxCenter - sCenterX * scale] = vyCenter - sCenterY * scale + fitToBounds(true, satTemp!!) + return satTemp!!.vTranslate + } + + /** + * Given a requested source center and scale, calculate what the actual center will have to be to keep the image in + * pan limits, keeping the requested center as near to the middle of the screen as allowed. + */ + private fun limitedSCenter(sCenterX: Float, sCenterY: Float, scale: Float, sTarget: PointF): PointF { + val vTranslate = vTranslateForSCenter(sCenterX, sCenterY, scale) + val vxCenter = paddingLeft + (width - paddingRight - paddingLeft) / 2 + val vyCenter = paddingTop + (height - paddingBottom - paddingTop) / 2 + val sx = (vxCenter - vTranslate.x) / scale + val sy = (vyCenter - vTranslate.y) / scale + sTarget[sx] = sy + return sTarget + } + + /** + * Returns the minimum allowed scale. + */ + private fun minScale(): Float { + val extra = panLimit == PAN_LIMIT_INSIDE + val vExtra = if (extra) (extraSpaceBottom + extraSpaceTop).roundToInt() else 0 + val hExtra = if (extra) (extraSpaceLeft + extraSpaceRight).roundToInt() else 0 + val vPadding = paddingBottom + paddingTop + vExtra + val hPadding = paddingLeft + paddingRight + hExtra + return when (minimumScaleType) { + SCALE_TYPE_CENTER_INSIDE -> min( + (width - hPadding) / sWidth.toFloat(), + (height - vPadding) / sHeight.toFloat() + ) + SCALE_TYPE_CENTER_CROP -> max( + (width - hPadding) / sWidth.toFloat(), + (height - vPadding) / sHeight.toFloat() + ) + SCALE_TYPE_FIT_WIDTH -> (width - hPadding) / sWidth.toFloat() + SCALE_TYPE_FIT_HEIGHT -> (height - vPadding) / sHeight.toFloat() + SCALE_TYPE_ORIGINAL_SIZE -> 1F + SCALE_TYPE_SMART_FIT -> if (sHeight > sWidth) { + // Fit to width + (width - hPadding) / sWidth.toFloat() + } else { + // Fit to height + (height - vPadding) / sHeight.toFloat() + } + SCALE_TYPE_CUSTOM -> minScale + else -> min((width - hPadding) / sWidth.toFloat(), (height - vPadding) / sHeight.toFloat()) + } + } + + /** + * Adjust a requested scale to be within the allowed limits. + */ + private fun limitedScale(targetScale: Float): Float { + return targetScale.coerceIn(minScale(), maxScale) + } + + /** + * Apply a selected type of easing. + * + * @param type Easing type, from static fields + * @param time Elapsed time + * @param from Start value + * @param change Target value + * @param duration Anm duration + * @return Current value + */ + private fun ease(type: Int, time: Long, from: Float, change: Float, duration: Long): Float { + return when (type) { + EASE_IN_OUT_QUAD -> easeInOutQuad(time, from, change, duration) + EASE_OUT_QUAD -> easeOutQuad(time, from, change, duration) + else -> throw IllegalStateException("Unexpected easing type: $type") + } + } + + /** + * Quadratic easing for fling. With thanks to Robert Penner - http://gizma.com/easing/ + * + * @param time Elapsed time + * @param from Start value + * @param change Target value + * @param duration Anm duration + * @return Current value + */ + private fun easeOutQuad(time: Long, from: Float, change: Float, duration: Long): Float { + val progress = time.toFloat() / duration.toFloat() + return -change * progress * (progress - 2) + from + } + + /** + * Quadratic easing for scale and center animations. With thanks to Robert Penner - http://gizma.com/easing/ + * + * @param time Elapsed time + * @param from Start value + * @param change Target value + * @param duration Anm duration + * @return Current value + */ + private fun easeInOutQuad(time: Long, from: Float, change: Float, duration: Long): Float { + var timeF = time / (duration / 2F) + return if (timeF < 1) { + change / 2f * timeF * timeF + from + } else { + timeF-- + -change / 2f * (timeF * (timeF - 2) - 1) + from + } + } + + /** + * Debug logger + */ + @AnyThread + private fun debug(message: String, vararg args: Any) { + if (debug) { + Log.d(TAG, String.format(message, *args)) + } + } + + /** + * For debug overlays. Scale pixel value according to screen density. + */ + private fun px(px: Int): Int { + return (density * px).toInt() + } + + /** + * Calculate how much further the image can be panned in each direction. The results are set on + * the supplied [RectF] and expressed as screen pixels. For example, if the image cannot be + * panned any further towards the left, the value of [RectF.left] will be set to 0. + * + * @param vTarget target object for results. Re-use for efficiency. + */ + fun getPanRemaining(vTarget: RectF) { + if (!isReady) { + return + } + val scaleWidth = scale * sWidth + val scaleHeight = scale * sHeight + when (panLimit) { + PAN_LIMIT_CENTER -> { + vTarget.top = -(vTranslate!!.y - height / 2).coerceAtLeast(0F) + vTarget.left = -(vTranslate!!.x - width / 2).coerceAtLeast(0F) + vTarget.bottom = (vTranslate!!.y - (height / 2 - scaleHeight)).coerceAtLeast(0F) + vTarget.right = (vTranslate!!.x - (width / 2 - scaleWidth)).coerceAtLeast(0F) + } + PAN_LIMIT_OUTSIDE -> { + vTarget.top = -(vTranslate!!.y - height).coerceAtLeast(0F) + vTarget.left = -(vTranslate!!.x - width).coerceAtLeast(0F) + vTarget.bottom = (vTranslate!!.y + scaleHeight).coerceAtLeast(0F) + vTarget.right = (vTranslate!!.x + scaleWidth).coerceAtLeast(0F) + } + else -> { + vTarget.top = -vTranslate!!.y.coerceAtLeast(0F) + vTarget.left = -vTranslate!!.x.coerceAtLeast(0F) + vTarget.bottom = (scaleHeight + vTranslate!!.y - height).coerceAtLeast(0F) + vTarget.right = (scaleWidth + vTranslate!!.x - width).coerceAtLeast(0F) + } + } + } + + /** + * Set the pan limiting style. See static fields. Normally [.PAN_LIMIT_INSIDE] is best, for image galleries. + * + * @param panLimit a pan limit constant. See static fields. + */ + fun setPanLimit(@PanLimit panLimit: Int) { + this.panLimit = panLimit + if (isReady) { + fitToBounds(true) + invalidate() + } + } + + /** + * Set the minimum scale type. See static fields. Normally [.SCALE_TYPE_CENTER_INSIDE] is best, for image galleries. + * + * @param scaleType a scale type constant. See static fields. + */ + fun setMinimumScaleType(@ScaleType scaleType: Int) { + minimumScaleType = scaleType + if (isReady) { + fitToBounds(true) + invalidate() + } + } + + /** + * This is a screen density aware alternative to [.setMaxScale]; it allows you to express the maximum + * allowed scale in terms of the minimum pixel density. This avoids the problem of 1:1 scale still being + * too small on a high density screen. A sensible starting point is 160 - the default used by this view. + * + * @param dpi Source image pixel density at maximum zoom. + */ + fun setMinimumDpi(dpi: Int) { + val metrics = resources.displayMetrics + val averageDpi = (metrics.xdpi + metrics.ydpi) / 2 + maxScale = averageDpi / dpi + } + + /** + * This is a screen density aware alternative to [.setMinScale]; it allows you to express the minimum + * allowed scale in terms of the maximum pixel density. + * + * @param dpi Source image pixel density at minimum zoom. + */ + fun setMaximumDpi(dpi: Int) { + val metrics = resources.displayMetrics + val averageDpi = (metrics.xdpi + metrics.ydpi) / 2 + setMinScale(averageDpi / dpi) + } + + /** + * Returns the minimum allowed scale. + * + * @return the minimum scale as a source/view pixels ratio. + */ + fun getMinScale(): Float { + return minScale() + } + + /** + * Set the minimum scale allowed. A value of 1 means 1:1 pixels at minimum scale. You may wish to set this according + * to screen density. Consider using [.setMaximumDpi], which is density aware. + * + * @param minScale minimum scale expressed as a source/view pixels ratio. + */ + fun setMinScale(minScale: Float) { + this.minScale = minScale + } + + /** + * By default, image tiles are at least as high resolution as the screen. For a retina screen this may not be + * necessary, and may increase the likelihood of an OutOfMemoryError. This method sets a DPI at which higher + * resolution tiles should be loaded. Using a lower number will on average use less memory but result in a lower + * quality image. 160-240dpi will usually be enough. This should be called before setting the image source, + * because it affects which tiles get loaded. When using an untiled source image this method has no effect. + * + * @param minimumTileDpi Tile loading threshold. + */ + fun setMinimumTileDpi(minimumTileDpi: Int) { + val metrics = resources.displayMetrics + val averageDpi = (metrics.xdpi + metrics.ydpi) / 2 + this.minimumTileDpi = minimumTileDpi.coerceAtMost(averageDpi.roundToInt()) + if (isReady) { + reset(false) + invalidate() + } + } + + /** + * Sets extra space for the view. + * Extra space artificially increases image size, allowing to pan further from the side. + * Extra space also lowers minScale. + * Extra space is zoom-agnostic thus constant in terms of view pixels not source pixels. + * Extra space is respected only in [.PAN_LIMIT_INSIDE] pan mode and ignored in others. + * @param vLeft extra space on the left + * @param vTop extra space on the top + * @param vRight extra space on the right + * @param vBottom extra space on the bottom + */ + fun setExtraSpace(vLeft: Float, vTop: Float, vRight: Float, vBottom: Float) { + extraSpaceLeft = vLeft + extraSpaceTop = vTop + extraSpaceRight = vRight + extraSpaceBottom = vBottom + } + + /** + * Returns extra space for the view contained in [RectF]. + * @return extra space for all sides of view in view pixels + */ + val extraSpace: RectF + get() = RectF(extraSpaceLeft, extraSpaceTop, extraSpaceRight, extraSpaceBottom) + + /** + * Returns the source point at the center of the view. + * + * @return the source coordinates current at the center of the view. + */ + val center: PointF? + get() { + val mX = width / 2F + val mY = height / 2F + return viewToSourceCoord(mX, mY) + } + + /** + * Externally change the scale and translation of the source image. This may be used with getCenter() and getScale() + * to restore the scale and zoom after a screen rotate. + * + * @param scale New scale to set. + * @param sCenter New source image coordinate to center on the screen, subject to boundaries. + */ + fun setScaleAndCenter(scale: Float, sCenter: PointF?) { + anim = null + pendingScale = scale + sPendingCenter = sCenter + sRequestedCenter = sCenter + invalidate() + } + + /** + * Fully zoom out and return the image to the middle of the screen. This might be useful if you have a view pager + * and want images to be reset when the user has moved to another page. + */ + fun resetScaleAndCenter() { + anim = null + pendingScale = limitedScale(0F) + sPendingCenter = if (isReady) { + PointF((sWidth / 2F), (sHeight / 2F)) + } else { + PointF(0F, 0F) + } + invalidate() + } + + /** + * Called once when the view is initialised, has dimensions, and will display an image on the + * next draw. This is triggered at the same time as [OnImageEventListener.onReady] but + * allows a subclass to receive this event without using a listener. + */ + protected fun onReady() {} + + /** + * Called once when the full size image or its base layer tiles have been loaded. + */ + protected fun onImageLoaded() {} + + /** + * Get the current state of the view (scale, center, orientation) for restoration after rotate. Will return null if + * the view is not ready. + * + * @return an [ImageViewState] instance representing the current position of the image. null if the view isn't ready. + */ + val state: ImageViewState? + get() = if (vTranslate != null && sWidth > 0 && sHeight > 0) { + ImageViewState(scale, center!!) + } else null + + /** + * Returns true if pan gesture detection is enabled. + * + * @return true if pan gesture detection is enabled. + */ + fun isPanEnabled(): Boolean { + return panEnabled + } + + /** + * Enable or disable pan gesture detection. Disabling pan causes the image to be centered. Pan + * can still be changed from code. + * + * @param panEnabled true to enable panning, false to disable. + */ + fun setPanEnabled(panEnabled: Boolean) { + this.panEnabled = panEnabled + if (!panEnabled && vTranslate != null) { + vTranslate!!.x = width / 2 - scale * (sWidth / 2) + vTranslate!!.y = height / 2 - scale * (sHeight / 2) + if (isReady) { + refreshRequiredTiles(true) + invalidate() + } + } + } + + /** + * Set a solid color to render behind tiles, useful for displaying transparent PNGs. + * + * @param tileBgColor Background color for tiles. + */ + fun setTileBackgroundColor(tileBgColor: Int) { + if (Color.alpha(tileBgColor) == 0) { + tileBgPaint = null + } else { + tileBgPaint = Paint().apply { + style = Paint.Style.FILL + color = tileBgColor + } + } + invalidate() + } + + /** + * Set the scale the image will zoom in to when double tapped. This also the scale point where a double tap is interpreted + * as a zoom out gesture - if the scale is greater than 90% of this value, a double tap zooms out. Avoid using values + * greater than the max zoom. + * + * @param doubleTapZoomScale New value for double tap gesture zoom scale. + */ + fun setDoubleTapZoomScale(doubleTapZoomScale: Float) { + this.doubleTapZoomScale = doubleTapZoomScale + } + + /** + * A density aware alternative to [.setDoubleTapZoomScale]; this allows you to express the scale the + * image will zoom in to when double tapped in terms of the image pixel density. Values lower than the max scale will + * be ignored. A sensible starting point is 160 - the default used by this view. + * + * @param dpi New value for double tap gesture zoom scale. + */ + fun setDoubleTapZoomDpi(dpi: Int) { + val metrics = resources.displayMetrics + val averageDpi = (metrics.xdpi + metrics.ydpi) / 2 + setDoubleTapZoomScale(averageDpi / dpi) + } + + /** + * Set the type of zoom animation to be used for double taps. See static fields. + * + * @param doubleTapZoomStyle New value for zoom style. + */ + fun setDoubleTapZoomStyle(@ZoomStyle doubleTapZoomStyle: Int) { + this.doubleTapZoomStyle = doubleTapZoomStyle + } + + /** + * Set the duration of the double tap zoom animation. + * + * @param durationMs Duration in milliseconds. + */ + fun setDoubleTapZoomDuration(durationMs: Int) { + doubleTapZoomDuration = durationMs.coerceAtLeast(0) + } + + /** + * Enable or disable eager loading of tiles that appear on screen during gestures or animations, + * while the gesture or animation is still in progress. By default this is enabled to improve + * responsiveness, but it can result in tiles being loaded and discarded more rapidly than + * necessary and reduce the animation frame rate on old/cheap devices. Disable this on older + * devices if you see poor performance. Tiles will then be loaded only when gestures and animations + * are completed. + * + * @param eagerLoadingEnabled true to enable loading during gestures, false to delay loading until gestures end + */ + fun setEagerLoadingEnabled(eagerLoadingEnabled: Boolean) { + this.eagerLoadingEnabled = eagerLoadingEnabled + } + + /** + * Enables visual debugging, showing tile boundaries and sizes. + * + * @param debug true to enable debugging, false to disable. + */ + fun setDebug(debug: Boolean) { + this.debug = debug + } + + /** + * Check if an image has been set. The image may not have been loaded and displayed yet. + * + * @return If an image is currently set. + */ + fun hasImage(): Boolean { + return provider != null || bitmap != null + } + + /** + * {@inheritDoc} + */ + override fun setOnLongClickListener(onLongClickListener: OnLongClickListener?) { + this.onLongClickListener = onLongClickListener + } + + /** + * Add a listener allowing notification of load and error events. Extend [DefaultOnImageEventListener] + * to simplify implementation. + * + * @param onImageEventListener an [OnImageEventListener] instance. + */ + fun setOnImageEventListener(onImageEventListener: OnImageEventListener?) { + this.onImageEventListener = onImageEventListener + } + + /** + * Add a listener for pan and zoom events. Extend [DefaultOnStateChangedListener] to simplify + * implementation. + * + * @param onStateChangedListener an [OnStateChangedListener] instance. + */ + fun setOnStateChangedListener(onStateChangedListener: OnStateChangedListener?) { + this.onStateChangedListener = onStateChangedListener + } + + private fun sendStateChanged(oldScale: Float, oldVTranslate: PointF, origin: Int) { + if (scale != oldScale) { + onStateChangedListener?.onScaleChanged(scale, origin) + } + if (vTranslate != oldVTranslate) { + onStateChangedListener?.onCenterChanged(center, origin) + } + } + + /** + * Creates a panning animation builder, that when started will animate the image to place the given coordinates of + * the image in the center of the screen. If doing this would move the image beyond the edges of the screen, the + * image is instead animated to move the center point as near to the center of the screen as is allowed - it's + * guaranteed to be on screen. + * + * @param sCenter Target center point + * @return [AnimationBuilder] instance. Call [SubsamplingScaleImageView.AnimationBuilder.start] to start the anim. + */ + fun animateCenter(sCenter: PointF): AnimationBuilder? { + return if (isReady) AnimationBuilder(sCenter) else null + } + + /** + * Creates a scale animation builder, that when started will animate a zoom in or out. If this would move the image + * beyond the panning limits, the image is automatically panned during the animation. + * + * @param scale Target scale. + * @return [AnimationBuilder] instance. Call [SubsamplingScaleImageView.AnimationBuilder.start] to start the anim. + */ + fun animateScale(scale: Float): AnimationBuilder? { + return if (isReady) AnimationBuilder(scale) else null + } + + /** + * Creates a scale animation builder, that when started will animate a zoom in or out. If this would move the image + * beyond the panning limits, the image is automatically panned during the animation. + * + * @param scale Target scale. + * @param sCenter Target source center. + * @return [AnimationBuilder] instance. Call [SubsamplingScaleImageView.AnimationBuilder.start] to start the anim. + */ + fun animateScaleAndCenter(scale: Float, sCenter: PointF?): AnimationBuilder? { + return if (isReady) AnimationBuilder(scale, sCenter) else null + } + + /** + * An event listener for animations, allows events to be triggered when an animation completes, + * is aborted by another animation starting, or is aborted by a touch event. Note that none of + * these events are triggered if the activity is paused, the image is swapped, or in other cases + * where the view's internal state gets wiped or draw events stop. + */ + interface OnAnimationEventListener { + /** + * The animation has completed, having reached its endpoint. + */ + fun onComplete() + + /** + * The animation has been aborted before reaching its endpoint because the user touched the screen. + */ + fun onInterruptedByUser() + + /** + * The animation has been aborted before reaching its endpoint because a new animation has been started. + */ + fun onInterruptedByNewAnim() + } + + /** + * An event listener, allowing subclasses and activities to be notified of significant events. + */ + interface OnImageEventListener { + /** + * Called when the dimensions of the image and view are known, and either a preview image, + * the full size image, or base layer tiles are loaded. This indicates the scale and translate + * are known and the next draw will display an image. This event can be used to hide a loading + * graphic, or inform a subclass that it is safe to draw overlays. + */ + fun onReady() + + /** + * Called when the full size image is ready. When using tiling, this means the lowest resolution + * base layer of tiles are loaded, and when tiling is disabled, the image bitmap is loaded. + * This event could be used as a trigger to enable gestures if you wanted interaction disabled + * while only a preview is displayed, otherwise for most cases [.onReady] is the best + * event to listen to. + */ + fun onImageLoaded() + + /** + * Indicates an error initiliasing the decoder when using a tiling, or when loading the full + * size bitmap when tiling is disabled. This method cannot be relied upon; certain encoding + * types of supported image formats can result in corrupt or blank images being loaded and + * displayed with no detectable error. + * + * @param e The exception thrown. This error is also logged by the view. + */ + fun onImageLoadError(e: Exception) + + /** + * Called when an image tile could not be loaded. This method cannot be relied upon; certain + * encoding types of supported image formats can result in corrupt or blank images being loaded + * and displayed with no detectable error. Most cases where an unsupported file is used will + * result in an error caught by [.onImageLoadError]. + * + * @param e The exception thrown. This error is logged by the view. + */ + fun onTileLoadError(e: Exception) + } + + /** + * An event listener, allowing activities to be notified of pan and zoom events. Initialisation + * and calls made by your code do not trigger events; touch events and animations do. Methods in + * this listener will be called on the UI thread and may be called very frequently - your + * implementation should return quickly. + */ + interface OnStateChangedListener { + /** + * The scale has changed. Use with [.getMaxScale] and [.getMinScale] to determine + * whether the image is fully zoomed in or out. + * + * @param newScale The new scale. + * @param origin Where the event originated from - one of [.ORIGIN_ANIM], [.ORIGIN_TOUCH]. + */ + fun onScaleChanged(newScale: Float, origin: Int) + + /** + * The source center has been changed. This can be a result of panning or zooming. + * + * @param newCenter The new source center point. + * @param origin Where the event originated from - one of [.ORIGIN_ANIM], [.ORIGIN_TOUCH]. + */ + fun onCenterChanged(newCenter: PointF?, origin: Int) + } + + private data class Tile( + val sRect: Rect, + val sampleSize: Int, + val vRect: Rect, + val fileSRect: Rect + ) { + var bitmap: Bitmap? = null + var loading = false + var visible = false + } + + /** + * @param scaleStart Scale at start of anim + * @param scaleEnd Scale at end of anim (target) + * @param sCenterStart Source center point at start + * @param sCenterEnd Source center point at end, adjusted for pan limits + * @param sCenterEndRequested Source center point that was requested, without adjustments + * @param vFocusStart View point that was double tapped + * @param vFocusEnd Where the view focal point should be moved to during the anim + * @param duration How long the anim takes + * @param interruptible Whether the anim can be interrupted by a touch + * @param easing Easing style + * @param origin Animation origin (API, double tap or fling) + * @param time Start time + * @param listener Event listener + */ + private data class Anim( + val scaleStart: Float, + val scaleEnd: Float, + val sCenterStart: PointF?, + val sCenterEnd: PointF, + val sCenterEndRequested: PointF, + val vFocusStart: PointF?, + val vFocusEnd: PointF, + val duration: Long, + val interruptible: Boolean, + val easing: Int, + val origin: Int, + val time: Long = System.currentTimeMillis(), + val listener: OnAnimationEventListener? + ) + + private data class ScaleAndTranslate(var scale: Float, val vTranslate: PointF) + + /** + * Default implementation of [OnAnimationEventListener] for extension. This does nothing in any method. + */ + open class DefaultOnAnimationEventListener : OnAnimationEventListener { + override fun onComplete() {} + override fun onInterruptedByUser() {} + override fun onInterruptedByNewAnim() {} + } + + /** + * Default implementation of [OnImageEventListener] for extension. This does nothing in any method. + */ + open class DefaultOnImageEventListener : OnImageEventListener { + override fun onReady() {} + override fun onImageLoaded() {} + override fun onImageLoadError(e: Exception) {} + override fun onTileLoadError(e: Exception) {} + } + + /** + * Default implementation of [OnStateChangedListener]. This does nothing in any method. + */ + open class DefaultOnStateChangedListener : OnStateChangedListener { + override fun onCenterChanged(newCenter: PointF?, origin: Int) {} + override fun onScaleChanged(newScale: Float, origin: Int) {} + } + + /** + * Builder class used to set additional options for a scale animation. Create an instance using [.animateScale], + * then set your options and call [.start]. + */ + inner class AnimationBuilder { + private val targetScale: Float + private val targetSCenter: PointF? + private val vFocus: PointF? + private var duration: Long = 500 + private var easing = EASE_IN_OUT_QUAD + private var origin = ORIGIN_ANIM + private var interruptible = true + private var panLimited = true + private var listener: OnAnimationEventListener? = null + + constructor(sCenter: PointF) { + targetScale = scale + targetSCenter = sCenter + vFocus = null + } + + constructor(scale: Float) { + targetScale = scale + targetSCenter = center + vFocus = null + } + + constructor(scale: Float, sCenter: PointF?) { + targetScale = scale + targetSCenter = sCenter + vFocus = null + } + + constructor(scale: Float, sCenter: PointF?, vFocus: PointF?) { + targetScale = scale + targetSCenter = sCenter + this.vFocus = vFocus + } + + /** + * Desired duration of the anim in milliseconds. Default is 500. + * + * @param duration duration in milliseconds. + * @return this builder for method chaining. + */ + fun withDuration(duration: Long): AnimationBuilder { + this.duration = duration + return this + } + + /** + * Whether the animation can be interrupted with a touch. Default is true. + * + * @param interruptible interruptible flag. + * @return this builder for method chaining. + */ + fun withInterruptible(interruptible: Boolean): AnimationBuilder { + this.interruptible = interruptible + return this + } + + /** + * Set the easing style. See static fields. [.EASE_IN_OUT_QUAD] is recommended, and the default. + * + * @param easing easing style. + * @return this builder for method chaining. + */ + fun withEasing(@EasingStyle easing: Int): AnimationBuilder { + this.easing = easing + return this + } + + /** + * Add an animation event listener. + * + * @param listener The listener. + * @return this builder for method chaining. + */ + fun withOnAnimationEventListener(listener: OnAnimationEventListener?): AnimationBuilder { + this.listener = listener + return this + } + + /** + * Only for internal use. When set to true, the animation proceeds towards the actual end point - the nearest + * point to the center allowed by pan limits. When false, animation is in the direction of the requested end + * point and is stopped when the limit for each axis is reached. The latter behaviour is used for flings but + * nothing else. + */ + fun withPanLimited(panLimited: Boolean): AnimationBuilder { + this.panLimited = panLimited + return this + } + + /** + * Only for internal use. Indicates what caused the animation. + */ + fun withOrigin(origin: Int): AnimationBuilder { + this.origin = origin + return this + } + + /** + * Starts the animation. + */ + fun start() { + try { + anim?.listener?.onInterruptedByNewAnim() + } catch (e: Exception) { + Log.w(TAG, "Error thrown by animation listener", e) + } + val vxCenter = paddingLeft + (width - paddingRight - paddingLeft) / 2 + val vyCenter = paddingTop + (height - paddingBottom - paddingTop) / 2 + val targetScale = limitedScale(targetScale) + val targetSCenter = if (panLimited) limitedSCenter( + targetSCenter!!.x, + targetSCenter.y, + targetScale, + PointF() + ) else targetSCenter!! + anim = Anim( + scaleStart = scale, + scaleEnd = targetScale, + sCenterEndRequested = targetSCenter, + sCenterStart = center, + sCenterEnd = targetSCenter, + vFocusStart = sourceToViewCoord(targetSCenter), + vFocusEnd = PointF( + vxCenter.toFloat(), + vyCenter.toFloat() + ), + duration = duration, + interruptible = interruptible, + easing = easing, + origin = origin, + listener = listener + ) + if (vFocus != null) { + // Calculate where translation will be at the end of the anim + val vTranslateXEnd = vFocus.x - targetScale * anim!!.sCenterStart!!.x + val vTranslateYEnd = vFocus.y - targetScale * anim!!.sCenterStart!!.y + val satEnd = ScaleAndTranslate(targetScale, PointF(vTranslateXEnd, vTranslateYEnd)) + // Fit the end translation into bounds + fitToBounds(true, satEnd) + // Adjust the position of the focus point at end so image will be in bounds + anim!!.vFocusEnd.set( + vFocus.x + (satEnd.vTranslate.x - vTranslateXEnd), + vFocus.y + (satEnd.vTranslate.y - vTranslateYEnd) + ) + } + invalidate() + } + } + + companion object { + @Retention(AnnotationRetention.SOURCE) + @IntDef( + ZOOM_FOCUS_FIXED, + ZOOM_FOCUS_CENTER, + ZOOM_FOCUS_CENTER_IMMEDIATE + ) + annotation class ZoomStyle + + /** + * During zoom animation, keep the point of the image that was tapped in the same place, and scale the image around it. + */ + const val ZOOM_FOCUS_FIXED = 1 + + /** + * During zoom animation, move the point of the image that was tapped to the center of the screen. + */ + const val ZOOM_FOCUS_CENTER = 2 + + /** + * Zoom in to and center the tapped point immediately without animating. + */ + const val ZOOM_FOCUS_CENTER_IMMEDIATE = 3 + + @Retention(AnnotationRetention.SOURCE) + @IntDef( + EASE_IN_OUT_QUAD, + EASE_OUT_QUAD + ) + annotation class EasingStyle + + /** + * Quadratic ease out. Not recommended for scale animation, but good for panning. + */ + const val EASE_OUT_QUAD = 1 + + /** + * Quadratic ease in and out. + */ + const val EASE_IN_OUT_QUAD = 2 + + @Retention(AnnotationRetention.SOURCE) + @IntDef( + PAN_LIMIT_INSIDE, + PAN_LIMIT_OUTSIDE, + PAN_LIMIT_CENTER + ) + annotation class PanLimit + + /** + * Don't allow the image to be panned off screen. As much of the image as possible is always displayed, centered in the view when it is smaller. This is the best option for galleries. + */ + const val PAN_LIMIT_INSIDE = 1 + + /** + * Allows the image to be panned until it is just off screen, but no further. The edge of the image will stop when it is flush with the screen edge. + */ + const val PAN_LIMIT_OUTSIDE = 2 + + /** + * Allows the image to be panned until a corner reaches the center of the screen but no further. Useful when you want to pan any spot on the image to the exact center of the screen. + */ + const val PAN_LIMIT_CENTER = 3 + + @Retention(AnnotationRetention.SOURCE) + @IntDef( + SCALE_TYPE_CENTER_INSIDE, + SCALE_TYPE_CENTER_CROP, + SCALE_TYPE_FIT_WIDTH, + SCALE_TYPE_FIT_HEIGHT, + SCALE_TYPE_ORIGINAL_SIZE, + SCALE_TYPE_SMART_FIT, + SCALE_TYPE_CUSTOM + ) + annotation class ScaleType + + /** + * Scale the image so that both dimensions of the image will be equal to or less than the corresponding dimension of the view. The image is then centered in the view. This is the default behaviour and best for galleries. + */ + const val SCALE_TYPE_CENTER_INSIDE = 1 + + /** + * Scale the image uniformly so that both dimensions of the image will be equal to or larger than the corresponding dimension of the view. The image is then centered in the view. + */ + const val SCALE_TYPE_CENTER_CROP = 2 + const val SCALE_TYPE_FIT_WIDTH = 3 + const val SCALE_TYPE_FIT_HEIGHT = 4 + const val SCALE_TYPE_ORIGINAL_SIZE = 5 + const val SCALE_TYPE_SMART_FIT = 6 + + /** + * Scale the image so that both dimensions of the image will be equal to or less than the maxScale and equal to or larger than minScale. The image is then centered in the view. + */ + const val SCALE_TYPE_CUSTOM = 7 + + /** + * State change originated from animation. + */ + const val ORIGIN_ANIM = 1 + + /** + * State change originated from touch gesture. + */ + const val ORIGIN_TOUCH = 2 + + /** + * State change originated from a fling momentum anim. + */ + const val ORIGIN_FLING = 3 + + /** + * State change originated from a double tap zoom anim. + */ + const val ORIGIN_DOUBLE_TAP_ZOOM = 4 + + // overrides for the dimensions of the generated tiles + const val TILE_SIZE_AUTO = Int.MAX_VALUE + private val TAG = SubsamplingScaleImageView::class.java.simpleName + + /** + * Global preferred bitmap config shared by all view instances and applied to new instances + * initialised after the call is made. This is a hint only; the bundled [Decoder] + * classes all respect this (except when they were constructed with + * an instance-specific config) but custom decoder classes will not. + */ + var preferredBitmapConfig: Bitmap.Config? = null + } +} diff --git a/app/src/main/java/com/davemorrissey/labs/subscaleview/decoder/Decoder.kt b/app/src/main/java/com/davemorrissey/labs/subscaleview/decoder/Decoder.kt new file mode 100644 index 000000000..97122c689 --- /dev/null +++ b/app/src/main/java/com/davemorrissey/labs/subscaleview/decoder/Decoder.kt @@ -0,0 +1,53 @@ +package com.davemorrissey.labs.subscaleview.decoder + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Point +import android.graphics.Rect +import kotlin.Throws +import com.davemorrissey.labs.subscaleview.provider.InputProvider +import java.lang.Exception + +/** + * Interface for image decoding classes. + */ +interface Decoder { + /** + * Initialise the decoder. When possible, perform initial setup work once in this method. The + * dimensions of the image must be returned. + * + * @param context Application context. A reference may be held, but must be cleared on recycle. + * @param provider Provider of the image. + * @return Dimensions of the image. + * @throws Exception if initialisation fails. + */ + @Throws(Exception::class) + fun init(context: Context, provider: InputProvider): Point + + /** + * Decode a region of the image with the given sample size. This method is called off the UI + * thread so it can safely load the image on the current thread. It is called from + * [android.os.AsyncTask]s running in an executor that may have multiple threads, so + * implementations must be thread safe. Adding `synchronized` to the method signature + * is the simplest way to achieve this, but bear in mind the [.recycle] method can be + * called concurrently. + * + * @param sRect Source image rectangle to decode. + * @param sampleSize Sample size. + * @return The decoded region. It is safe to return null if decoding fails. + */ + fun decodeRegion(sRect: Rect, sampleSize: Int): Bitmap + + /** + * Status check. Should return false before initialisation and after recycle. + * + * @return true if the decoder is ready to be used. + */ + val isReady: Boolean + + /** + * This method will be called when the decoder is no longer required. It should clean up any + * resources still in use. + */ + fun recycle() +} \ No newline at end of file diff --git a/app/src/main/java/com/davemorrissey/labs/subscaleview/decoder/ImageDecoder.kt b/app/src/main/java/com/davemorrissey/labs/subscaleview/decoder/ImageDecoder.kt new file mode 100644 index 000000000..8acbc4241 --- /dev/null +++ b/app/src/main/java/com/davemorrissey/labs/subscaleview/decoder/ImageDecoder.kt @@ -0,0 +1,64 @@ +package com.davemorrissey.labs.subscaleview.decoder + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Point +import android.graphics.Rect +import kotlin.Throws +import com.davemorrissey.labs.subscaleview.provider.InputProvider +import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView +import java.lang.Exception + +class ImageDecoder @JvmOverloads constructor( + private val cropBorders: Boolean, + bitmapConfig: Bitmap.Config? = null +) : Decoder { + init { + val globalBitmapConfig = SubsamplingScaleImageView.preferredBitmapConfig + } + + /** + * Initialise the decoder. When possible, perform initial setup work once in this method. The + * dimensions of the image must be returned. + * + * @param context Application context. A reference may be held, but must be cleared on recycle. + * @param provider Provider of the image. + * @return Dimensions of the image. + * @throws Exception if initialisation fails. + */ + @Throws(Exception::class) + override fun init(context: Context, provider: InputProvider): Point { + TODO() + } + + /** + * Decode a region of the image with the given sample size. This method is called off the UI + * thread so it can safely load the image on the current thread. It is called from + * [android.os.AsyncTask]s running in an executor that may have multiple threads, so + * implementations must be thread safe. Adding `synchronized` to the method signature + * is the simplest way to achieve this, but bear in mind the [.recycle] method can be + * called concurrently. + * + * @param sRect Source image rectangle to decode. + * @param sampleSize Sample size. + * @return The decoded region. It is safe to return null if decoding fails. + */ + override fun decodeRegion(sRect: Rect, sampleSize: Int): Bitmap { + TODO() + } + + /** + * Status check. Should return false before initialisation and after recycle. + * + * @return true if the decoder is ready to be used. + */ + override val isReady: Boolean = TODO() + + /** + * This method will be called when the decoder is no longer required. It should clean up any + * resources still in use. + */ + override fun recycle() { + TODO() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/davemorrissey/labs/subscaleview/provider/AssetInputProvider.kt b/app/src/main/java/com/davemorrissey/labs/subscaleview/provider/AssetInputProvider.kt new file mode 100644 index 000000000..eba85fc58 --- /dev/null +++ b/app/src/main/java/com/davemorrissey/labs/subscaleview/provider/AssetInputProvider.kt @@ -0,0 +1,11 @@ +package com.davemorrissey.labs.subscaleview.provider + +import android.content.Context +import java.io.IOException + +class AssetInputProvider(context: Context, val assetName: String) : InputProvider { + private val assets = context.assets + + @Throws(IOException::class) + override fun openStream() = assets.open(assetName) +} diff --git a/app/src/main/java/com/davemorrissey/labs/subscaleview/provider/InputProvider.kt b/app/src/main/java/com/davemorrissey/labs/subscaleview/provider/InputProvider.kt new file mode 100644 index 000000000..95359e291 --- /dev/null +++ b/app/src/main/java/com/davemorrissey/labs/subscaleview/provider/InputProvider.kt @@ -0,0 +1,10 @@ +package com.davemorrissey.labs.subscaleview.provider + +import java.io.IOException +import java.io.InputStream +import kotlin.Throws + +interface InputProvider { + @Throws(IOException::class) + fun openStream(): InputStream? +} diff --git a/app/src/main/java/com/davemorrissey/labs/subscaleview/provider/OpenStreamProvider.kt b/app/src/main/java/com/davemorrissey/labs/subscaleview/provider/OpenStreamProvider.kt new file mode 100644 index 000000000..0ee6a5c97 --- /dev/null +++ b/app/src/main/java/com/davemorrissey/labs/subscaleview/provider/OpenStreamProvider.kt @@ -0,0 +1,16 @@ +package com.davemorrissey.labs.subscaleview.provider + +import java.io.IOException +import java.io.InputStream +import kotlin.Throws + +/** + * Provider from an already open input stream. + * + * + * Please keep in mind this provider is not reusable. + */ +class OpenStreamProvider(private val stream: InputStream) : InputProvider { + @Throws(IOException::class) + override fun openStream() = stream +} diff --git a/app/src/main/java/com/davemorrissey/labs/subscaleview/provider/ResourceInputProvider.kt b/app/src/main/java/com/davemorrissey/labs/subscaleview/provider/ResourceInputProvider.kt new file mode 100644 index 000000000..6e7197ed7 --- /dev/null +++ b/app/src/main/java/com/davemorrissey/labs/subscaleview/provider/ResourceInputProvider.kt @@ -0,0 +1,11 @@ +package com.davemorrissey.labs.subscaleview.provider + +import android.content.Context +import java.io.IOException + +class ResourceInputProvider(context: Context, val resource: Int) : InputProvider { + private val resources = context.resources + + @Throws(IOException::class) + override fun openStream() = resources.openRawResource(resource) +} diff --git a/app/src/main/java/com/davemorrissey/labs/subscaleview/provider/UriInputProvider.kt b/app/src/main/java/com/davemorrissey/labs/subscaleview/provider/UriInputProvider.kt new file mode 100644 index 000000000..f3745ce14 --- /dev/null +++ b/app/src/main/java/com/davemorrissey/labs/subscaleview/provider/UriInputProvider.kt @@ -0,0 +1,13 @@ +package com.davemorrissey.labs.subscaleview.provider + +import android.content.Context +import android.net.Uri +import java.io.IOException +import kotlin.Throws + +class UriInputProvider(context: Context, val uri: Uri) : InputProvider { + private val resolver = context.contentResolver + + @Throws(IOException::class) + override fun openStream() = resolver.openInputStream(uri) +} diff --git a/app/src/main/java/com/hippo/ehviewer/Settings.java b/app/src/main/java/com/hippo/ehviewer/Settings.java index 581d8eca6..17011c2b7 100755 --- a/app/src/main/java/com/hippo/ehviewer/Settings.java +++ b/app/src/main/java/com/hippo/ehviewer/Settings.java @@ -30,7 +30,6 @@ import com.hippo.ehviewer.client.EhConfig; import com.hippo.ehviewer.client.data.FavListUrlBuilder; import com.hippo.ehviewer.ui.CommonOperations; import com.hippo.ehviewer.ui.scene.GalleryListScene; -import com.hippo.glgallery.GalleryView; import com.hippo.unifile.UniFile; import com.hippo.yorozuya.AssertUtils; import com.hippo.yorozuya.NumberUtils; @@ -131,11 +130,8 @@ public class Settings { private static final String KEY_SCREEN_ROTATION = "screen_rotation"; private static final int DEFAULT_SCREEN_ROTATION = 0; private static final String KEY_READING_DIRECTION = "reading_direction"; - private static final int DEFAULT_READING_DIRECTION = GalleryView.LAYOUT_RIGHT_TO_LEFT; private static final String KEY_PAGE_SCALING = "page_scaling"; - private static final int DEFAULT_PAGE_SCALING = GalleryView.SCALE_FIT; private static final String KEY_START_POSITION = "start_position"; - private static final int DEFAULT_START_POSITION = GalleryView.START_POSITION_TOP_RIGHT; private static final String KEY_KEEP_SCREEN_ON = "keep_screen_on"; private static final boolean DEFAULT_KEEP_SCREEN_ON = false; private static final String KEY_SHOW_CLOCK = "gallery_show_clock"; @@ -236,7 +232,7 @@ public class Settings { public static boolean LIST_THUMB_SIZE_INITED = false; private static int LIST_THUMB_SIZE = 40; private static Context sContext; - private static SharedPreferences sSettingsPre; + public static SharedPreferences sSettingsPre; private static EhConfig sEhConfig; public static void initialize(Context context) { @@ -507,29 +503,14 @@ public class Settings { putIntToStr(KEY_SCREEN_ROTATION, value); } - @GalleryView.LayoutMode - public static int getReadingDirection() { - return GalleryView.sanitizeLayoutMode(getIntFromStr(KEY_READING_DIRECTION, DEFAULT_READING_DIRECTION)); - } - public static void putReadingDirection(int value) { putIntToStr(KEY_READING_DIRECTION, value); } - @GalleryView.ScaleMode - public static int getPageScaling() { - return GalleryView.sanitizeScaleMode(getIntFromStr(KEY_PAGE_SCALING, DEFAULT_PAGE_SCALING)); - } - public static void putPageScaling(int value) { putIntToStr(KEY_PAGE_SCALING, value); } - @GalleryView.StartPosition - public static int getStartPosition() { - return GalleryView.sanitizeStartPosition(getIntFromStr(KEY_START_POSITION, DEFAULT_START_POSITION)); - } - public static void putStartPosition(int value) { putIntToStr(KEY_START_POSITION, value); } diff --git a/app/src/main/java/com/hippo/ehviewer/UrlOpener.java b/app/src/main/java/com/hippo/ehviewer/UrlOpener.java index 5c1ec0851..92c35a54e 100644 --- a/app/src/main/java/com/hippo/ehviewer/UrlOpener.java +++ b/app/src/main/java/com/hippo/ehviewer/UrlOpener.java @@ -29,7 +29,7 @@ import androidx.browser.customtabs.CustomTabsIntent; import com.hippo.ehviewer.client.EhUrlOpener; import com.hippo.ehviewer.client.data.GalleryDetail; import com.hippo.ehviewer.client.parser.GalleryPageUrlParser; -import com.hippo.ehviewer.ui.GalleryActivity; +import eu.kanade.tachiyomi.ui.reader.ReaderActivity; import com.hippo.ehviewer.ui.MainActivity; import com.hippo.scene.Announcer; import com.hippo.scene.StageActivity; @@ -56,19 +56,19 @@ public final class UrlOpener { GalleryPageUrlParser.Result result = GalleryPageUrlParser.parse(url); if (result != null) { if (result.gid == galleryDetail.gid) { - intent = new Intent(context, GalleryActivity.class); - intent.setAction(GalleryActivity.ACTION_EH); - intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, galleryDetail); - intent.putExtra(GalleryActivity.KEY_PAGE, result.page); + intent = new Intent(context, ReaderActivity.class); + intent.setAction(ReaderActivity.ACTION_EH); + intent.putExtra(ReaderActivity.KEY_GALLERY_INFO, galleryDetail); + intent.putExtra(ReaderActivity.KEY_PAGE, result.page); context.startActivity(intent); return; } } else if (url.startsWith("#c")) { try { - intent = new Intent(context, GalleryActivity.class); - intent.setAction(GalleryActivity.ACTION_EH); - intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, galleryDetail); - intent.putExtra(GalleryActivity.KEY_PAGE, Integer.parseInt(url.replace("#c", "")) - 1); + intent = new Intent(context, ReaderActivity.class); + intent.setAction(ReaderActivity.ACTION_EH); + intent.putExtra(ReaderActivity.KEY_GALLERY_INFO, galleryDetail); + intent.putExtra(ReaderActivity.KEY_PAGE, Integer.parseInt(url.replace("#c", "")) - 1); context.startActivity(intent); return; } catch (NumberFormatException e) { diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ArchiveGalleryProvider.java b/app/src/main/java/com/hippo/ehviewer/gallery/ArchivePageLoader.java similarity index 96% rename from app/src/main/java/com/hippo/ehviewer/gallery/ArchiveGalleryProvider.java rename to app/src/main/java/com/hippo/ehviewer/gallery/ArchivePageLoader.java index 7156214e7..87164955b 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/ArchiveGalleryProvider.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/ArchivePageLoader.java @@ -46,7 +46,9 @@ import java.util.Set; import java.util.Stack; import java.util.concurrent.atomic.AtomicInteger; -public class ArchiveGalleryProvider extends GalleryProvider2 { +import eu.kanade.tachiyomi.ui.reader.loader.PageLoader; + +public class ArchivePageLoader extends PageLoader2 { private static final AtomicInteger sIdGenerator = new AtomicInteger(); public static Handler showPasswd; public static String passwd; @@ -61,10 +63,10 @@ public class ArchiveGalleryProvider extends GalleryProvider2 { new Thread(new DecodeTask()) }; private Thread archiveThread; - private volatile int size = STATE_WAIT; + private volatile int size = PageLoader.STATE_WAIT; private String error; - public ArchiveGalleryProvider(Context context, Uri uri) { + public ArchivePageLoader(Context context, Uri uri) { UriArchiveAccessor archiveAccessor1; try { archiveAccessor1 = new UriArchiveAccessor(context, uri); @@ -77,8 +79,6 @@ public class ArchiveGalleryProvider extends GalleryProvider2 { @Override public void start() { - super.start(); - int id = sIdGenerator.incrementAndGet(); archiveThread = new PriorityThread( @@ -91,7 +91,6 @@ public class ArchiveGalleryProvider extends GalleryProvider2 { @Override public void stop() { - super.stop(); if (archiveThread != null) { archiveThread.interrupt(); archiveThread = null; @@ -217,7 +216,7 @@ public class ArchiveGalleryProvider extends GalleryProvider2 { size = 0; } if (size <= 0) { - size = STATE_ERROR; + size = PageLoader.STATE_ERROR; error = GetText.getString(R.string.error_reading_failed); notifyDataChanged(); return; @@ -289,7 +288,7 @@ public class ArchiveGalleryProvider extends GalleryProvider2 { // Check index valid if (index < 0 || index >= size) { - notifyPageFailed(index, GetText.getString(R.string.error_out_of_range)); + notifyPageFailed(index); continue; } @@ -347,7 +346,7 @@ public class ArchiveGalleryProvider extends GalleryProvider2 { if (image != null) { notifyPageSucceed(index, image); } else { - notifyPageFailed(index, GetText.getString(R.string.error_decoding_failed)); + notifyPageFailed(index); } } } diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/DirGalleryProvider.java b/app/src/main/java/com/hippo/ehviewer/gallery/DirPageLoader.java similarity index 89% rename from app/src/main/java/com/hippo/ehviewer/gallery/DirGalleryProvider.java rename to app/src/main/java/com/hippo/ehviewer/gallery/DirPageLoader.java index 08ac607a1..f20a14694 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/DirGalleryProvider.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/DirPageLoader.java @@ -24,7 +24,6 @@ import androidx.annotation.Nullable; import com.hippo.ehviewer.GetText; import com.hippo.ehviewer.R; -import com.hippo.glgallery.GalleryPageView; import com.hippo.image.Image; import com.hippo.unifile.FilenameFilter; import com.hippo.unifile.UniFile; @@ -44,9 +43,11 @@ import java.util.Stack; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -public class DirGalleryProvider extends GalleryProvider2 implements Runnable { +import eu.kanade.tachiyomi.ui.reader.loader.PageLoader; - private static final String TAG = DirGalleryProvider.class.getSimpleName(); +public class DirPageLoader extends PageLoader2 implements Runnable { + + private static final String TAG = DirPageLoader.class.getSimpleName(); private static final AtomicInteger sIdGenerator = new AtomicInteger(); private static final FilenameFilter imageFilter = (dir, name) -> StringUtils.endsWith(name.toLowerCase(), SUPPORT_IMAGE_EXTENSIONS); @@ -60,21 +61,19 @@ public class DirGalleryProvider extends GalleryProvider2 implements Runnable { }; private final UniFile mDir; private final Stack mRequests = new Stack<>(); - private final AtomicInteger mDecodingIndex = new AtomicInteger(GalleryPageView.INVALID_INDEX); + private final AtomicInteger mDecodingIndex = new AtomicInteger(-1); private final AtomicReference mFileList = new AtomicReference<>(); @Nullable private Thread mBgThread; - private volatile int mSize = STATE_WAIT; + private volatile int mSize = PageLoader.STATE_WAIT; private String mError; - public DirGalleryProvider(@NonNull UniFile dir) { + public DirPageLoader(@NonNull UniFile dir) { mDir = dir; } @Override public void start() { - super.start(); - mBgThread = new PriorityThread(this, TAG + '-' + sIdGenerator.incrementAndGet(), Process.THREAD_PRIORITY_BACKGROUND); mBgThread.start(); @@ -82,8 +81,6 @@ public class DirGalleryProvider extends GalleryProvider2 implements Runnable { @Override public void stop() { - super.stop(); - if (mBgThread != null) { mBgThread.interrupt(); mBgThread = null; @@ -213,7 +210,7 @@ public class DirGalleryProvider extends GalleryProvider2 implements Runnable { UniFile[] files = mDir.listFiles(imageFilter); if (files == null) { - mSize = STATE_ERROR; + mSize = PageLoader.STATE_ERROR; mError = GetText.getString(R.string.error_not_folder_path); // Notify to to show error @@ -251,8 +248,8 @@ public class DirGalleryProvider extends GalleryProvider2 implements Runnable { // Check index valid if (index < 0 || index >= files.length) { - mDecodingIndex.lazySet(GalleryPageView.INVALID_INDEX); - notifyPageFailed(index, GetText.getString(R.string.error_out_of_range)); + mDecodingIndex.lazySet(-1); + notifyPageFailed(index); continue; } @@ -260,15 +257,15 @@ public class DirGalleryProvider extends GalleryProvider2 implements Runnable { try { is = files[index].openInputStream(); Image image = Image.decode((FileInputStream) is); - mDecodingIndex.lazySet(GalleryPageView.INVALID_INDEX); + mDecodingIndex.lazySet(-1); notifyPageSucceed(index, image); } catch (IOException e) { - mDecodingIndex.lazySet(GalleryPageView.INVALID_INDEX); - notifyPageFailed(index, GetText.getString(R.string.error_reading_failed)); + mDecodingIndex.lazySet(-1); + notifyPageFailed(index); } finally { IOUtils.closeQuietly(is); } - mDecodingIndex.lazySet(GalleryPageView.INVALID_INDEX); + mDecodingIndex.lazySet(-1); } // Clear file list diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/EhGalleryProvider.java b/app/src/main/java/com/hippo/ehviewer/gallery/EhPageLoader.java similarity index 92% rename from app/src/main/java/com/hippo/ehviewer/gallery/EhGalleryProvider.java rename to app/src/main/java/com/hippo/ehviewer/gallery/EhPageLoader.java index 553598c26..9c57fefd2 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/EhGalleryProvider.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/EhPageLoader.java @@ -23,37 +23,35 @@ import androidx.annotation.Nullable; import com.hippo.ehviewer.client.data.GalleryInfo; import com.hippo.ehviewer.spider.SpiderQueen; -import com.hippo.glgallery.GalleryProvider; +import eu.kanade.tachiyomi.ui.reader.loader.PageLoader; import com.hippo.image.Image; import com.hippo.unifile.UniFile; import com.hippo.yorozuya.SimpleHandler; import java.util.Locale; -public class EhGalleryProvider extends GalleryProvider2 implements SpiderQueen.OnSpiderListener { +public class EhPageLoader extends PageLoader2 implements SpiderQueen.OnSpiderListener { private final Context mContext; private final GalleryInfo mGalleryInfo; @Nullable private SpiderQueen mSpiderQueen; - public EhGalleryProvider(Context context, GalleryInfo galleryInfo) { + public EhPageLoader(Context context, GalleryInfo galleryInfo) { mContext = context; mGalleryInfo = galleryInfo; } @Override public void start() { - super.start(); - mSpiderQueen = SpiderQueen.obtainSpiderQueen(mContext, mGalleryInfo, SpiderQueen.MODE_READ); mSpiderQueen.addOnSpiderListener(this); + if (mSpiderQueen.size() > 0) + notifyDataChanged(); } @Override public void stop() { - super.stop(); - if (mSpiderQueen != null) { mSpiderQueen.removeOnSpiderListener(this); // Activity recreate may called, so wait 3000s @@ -120,7 +118,7 @@ public class EhGalleryProvider extends GalleryProvider2 implements SpiderQueen.O if (mSpiderQueen != null) { return mSpiderQueen.size(); } else { - return GalleryProvider.STATE_ERROR; + return PageLoader.STATE_ERROR; } } @@ -131,7 +129,7 @@ public class EhGalleryProvider extends GalleryProvider2 implements SpiderQueen.O if (object instanceof Float) { notifyPagePercent(index, (Float) object); } else if (object instanceof String) { - notifyPageFailed(index, (String) object); + notifyPageFailed(index); } else if (object == null) { notifyPageWait(index); } @@ -145,7 +143,7 @@ public class EhGalleryProvider extends GalleryProvider2 implements SpiderQueen.O if (object instanceof Float) { notifyPagePercent(index, (Float) object); } else if (object instanceof String) { - notifyPageFailed(index, (String) object); + notifyPageFailed(index); } else if (object == null) { notifyPageWait(index); } @@ -192,7 +190,7 @@ public class EhGalleryProvider extends GalleryProvider2 implements SpiderQueen.O @Override public void onPageFailure(int index, String error, int finished, int downloaded, int total) { - notifyPageFailed(index, error); + notifyPageFailed(index); } @Override @@ -206,7 +204,7 @@ public class EhGalleryProvider extends GalleryProvider2 implements SpiderQueen.O @Override public void onGetImageFailure(int index, String error) { - notifyPageFailed(index, error); + notifyPageFailed(index); } private static class ReleaseTask implements Runnable { diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/GalleryProvider2.java b/app/src/main/java/com/hippo/ehviewer/gallery/PageLoader2.java similarity index 93% rename from app/src/main/java/com/hippo/ehviewer/gallery/GalleryProvider2.java rename to app/src/main/java/com/hippo/ehviewer/gallery/PageLoader2.java index 13be518cc..1436f7d42 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/GalleryProvider2.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/PageLoader2.java @@ -19,10 +19,10 @@ package com.hippo.ehviewer.gallery; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.hippo.glgallery.GalleryProvider; +import eu.kanade.tachiyomi.ui.reader.loader.PageLoader; import com.hippo.unifile.UniFile; -public abstract class GalleryProvider2 extends GalleryProvider { +public abstract class PageLoader2 extends PageLoader { // With dot public static final String[] SUPPORT_IMAGE_EXTENSIONS = { diff --git a/app/src/main/java/com/hippo/ehviewer/spider/SpiderDen.java b/app/src/main/java/com/hippo/ehviewer/spider/SpiderDen.java index 307ffbe28..d17205abe 100644 --- a/app/src/main/java/com/hippo/ehviewer/spider/SpiderDen.java +++ b/app/src/main/java/com/hippo/ehviewer/spider/SpiderDen.java @@ -28,7 +28,7 @@ import com.hippo.ehviewer.Settings; import com.hippo.ehviewer.client.EhCacheKeyFactory; import com.hippo.ehviewer.client.EhUtils; import com.hippo.ehviewer.client.data.GalleryInfo; -import com.hippo.ehviewer.gallery.GalleryProvider2; +import com.hippo.ehviewer.gallery.PageLoader2; import com.hippo.io.UniFileInputStreamPipe; import com.hippo.io.UniFileOutputStreamPipe; import com.hippo.streampipe.InputStreamPipe; @@ -118,7 +118,7 @@ public final class SpiderDen { @Nullable private static UniFile findImageFile(UniFile dir, int index) { - for (String extension : GalleryProvider2.SUPPORT_IMAGE_EXTENSIONS) { + for (String extension : PageLoader2.SUPPORT_IMAGE_EXTENSIONS) { String filename = generateImageFilename(index, extension); UniFile file = dir.findFile(filename); if (file != null) { @@ -179,10 +179,10 @@ public final class SpiderDen { * @param extension with dot */ private String fixExtension(String extension) { - if (Utilities.contain(GalleryProvider2.SUPPORT_IMAGE_EXTENSIONS, extension)) { + if (Utilities.contain(PageLoader2.SUPPORT_IMAGE_EXTENSIONS, extension)) { return extension; } else { - return GalleryProvider2.SUPPORT_IMAGE_EXTENSIONS[0]; + return PageLoader2.SUPPORT_IMAGE_EXTENSIONS[0]; } } @@ -261,8 +261,8 @@ public final class SpiderDen { } boolean result = false; - for (int i = 0, n = GalleryProvider2.SUPPORT_IMAGE_EXTENSIONS.length; i < n; i++) { - String filename = generateImageFilename(index, GalleryProvider2.SUPPORT_IMAGE_EXTENSIONS[i]); + for (int i = 0, n = PageLoader2.SUPPORT_IMAGE_EXTENSIONS.length; i < n; i++) { + String filename = generateImageFilename(index, PageLoader2.SUPPORT_IMAGE_EXTENSIONS[i]); UniFile file = dir.subFile(filename); if (file != null) { result |= file.delete(); diff --git a/app/src/main/java/com/hippo/ehviewer/spider/SpiderQueen.java b/app/src/main/java/com/hippo/ehviewer/spider/SpiderQueen.java index 7efb8acb6..529350d68 100644 --- a/app/src/main/java/com/hippo/ehviewer/spider/SpiderQueen.java +++ b/app/src/main/java/com/hippo/ehviewer/spider/SpiderQueen.java @@ -48,9 +48,8 @@ import com.hippo.ehviewer.client.parser.GalleryMultiPageViewerPTokenParser; import com.hippo.ehviewer.client.parser.GalleryPageApiParser; import com.hippo.ehviewer.client.parser.GalleryPageParser; import com.hippo.ehviewer.client.parser.GalleryPageUrlParser; -import com.hippo.ehviewer.gallery.GalleryProvider2; -import com.hippo.glgallery.GalleryPageView; -import com.hippo.glgallery.GalleryProvider; +import com.hippo.ehviewer.gallery.PageLoader2; +import eu.kanade.tachiyomi.ui.reader.loader.PageLoader; import com.hippo.image.Image; import com.hippo.streampipe.InputStreamPipe; import com.hippo.streampipe.OutputStreamPipe; @@ -167,7 +166,7 @@ public final class SpiderQueen implements Runnable { mPreloadNumber = MathUtils.clamp(Settings.getPreloadImage(), 0, 100); for (int i = 0; i < DECODE_THREAD_NUM; i++) { - mDecodeIndexArray[i] = GalleryPageView.INVALID_INDEX; + mDecodeIndexArray[i] = -1; } mWorkerPoolExecutor = new ThreadPoolExecutor(mWorkerMaxCount, mWorkerMaxCount, @@ -399,9 +398,9 @@ public final class SpiderQueen implements Runnable { public int size() { if (mQueenThread == null) { - return GalleryProvider.STATE_ERROR; + return PageLoader.STATE_ERROR; } else if (mPageStateArray == null) { - return GalleryProvider.STATE_WAIT; + return PageLoader.STATE_WAIT; } else { return mPageStateArray.length; } @@ -1274,8 +1273,8 @@ public final class SpiderQueen implements Runnable { extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mediaType.toString()); } // Ensure extension - if (!Utilities.contain(GalleryProvider2.SUPPORT_IMAGE_EXTENSIONS, extension)) { - extension = GalleryProvider2.SUPPORT_IMAGE_EXTENSIONS[0]; + if (!Utilities.contain(PageLoader2.SUPPORT_IMAGE_EXTENSIONS, extension)) { + extension = PageLoader2.SUPPORT_IMAGE_EXTENSIONS[0]; } OutputStreamPipe osPipe = null; @@ -1596,7 +1595,7 @@ public final class SpiderQueen implements Runnable { private void resetDecodeIndex() { synchronized (mDecodeRequestQueue) { - mDecodeIndexArray[mThreadIndex] = GalleryPageView.INVALID_INDEX; + mDecodeIndexArray[mThreadIndex] = -1; } } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java deleted file mode 100644 index 39ba35fdf..000000000 --- a/app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java +++ /dev/null @@ -1,1380 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.ehviewer.ui; - -import android.Manifest; -import android.animation.Animator; -import android.animation.ObjectAnimator; -import android.animation.ValueAnimator; -import android.annotation.SuppressLint; -import android.app.assist.AssistContent; -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.PackageManager; -import android.content.res.Resources; -import android.graphics.Typeface; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Environment; -import android.os.Handler; -import android.os.Message; -import android.provider.MediaStore; -import android.text.TextUtils; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.Window; -import android.view.WindowManager; -import android.webkit.MimeTypeMap; -import android.widget.FrameLayout; -import android.widget.SeekBar; -import android.widget.Spinner; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatDelegate; -import androidx.core.content.ContextCompat; -import androidx.core.content.FileProvider; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowCompat; -import androidx.core.view.WindowInsetsCompat; -import androidx.core.view.WindowInsetsControllerCompat; - -import com.google.android.material.materialswitch.MaterialSwitch; -import com.hippo.app.BaseDialogBuilder; -import com.hippo.app.EditTextDialogBuilder; -import com.hippo.ehviewer.AppConfig; -import com.hippo.ehviewer.BuildConfig; -import com.hippo.ehviewer.R; -import com.hippo.ehviewer.Settings; -import com.hippo.ehviewer.client.EhUrl; -import com.hippo.ehviewer.client.data.GalleryInfo; -import com.hippo.ehviewer.gallery.ArchiveGalleryProvider; -import com.hippo.ehviewer.gallery.DirGalleryProvider; -import com.hippo.ehviewer.gallery.EhGalleryProvider; -import com.hippo.ehviewer.gallery.GalleryProvider2; -import com.hippo.ehviewer.widget.GalleryGuideView; -import com.hippo.ehviewer.widget.GalleryHeader; -import com.hippo.ehviewer.widget.ReversibleSeekBar; -import com.hippo.glgallery.GalleryProvider; -import com.hippo.glgallery.GalleryView; -import com.hippo.glgallery.SimpleAdapter; -import com.hippo.glview.view.GLRootView; -import com.hippo.unifile.UniFile; -import com.hippo.util.ExceptionUtils; -import com.hippo.util.IoThreadPoolExecutor; -import com.hippo.widget.ColorView; -import com.hippo.yorozuya.AnimationUtils; -import com.hippo.yorozuya.ConcurrentPool; -import com.hippo.yorozuya.FileUtils; -import com.hippo.yorozuya.IOUtils; -import com.hippo.yorozuya.MathUtils; -import com.hippo.yorozuya.ResourcesUtils; -import com.hippo.yorozuya.SimpleAnimatorListener; -import com.hippo.yorozuya.SimpleHandler; -import com.hippo.yorozuya.ViewUtils; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.lang.ref.WeakReference; - -import rikka.core.res.ConfigurationKt; -import rikka.core.res.ResourcesKt; - -public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChangeListener, - GalleryView.Listener { - - public static final String ACTION_DIR = "dir"; - public static final String ACTION_EH = "eh"; - - public static final String KEY_ACTION = "action"; - public static final String KEY_FILENAME = "filename"; - public static final String KEY_URI = "uri"; - public static final String KEY_GALLERY_INFO = "gallery_info"; - public static final String KEY_PAGE = "page"; - public static final String KEY_CURRENT_INDEX = "current_index"; - - private static final long SLIDER_ANIMATION_DURING = 150; - private static final long HIDE_SLIDER_DELAY = 3000; - - private final ConcurrentPool mNotifyTaskPool = new ConcurrentPool<>(3); - private String mAction; - private String mFilename; - private Uri mUri; - private GalleryInfo mGalleryInfo; - private int mPage; - private String mCacheFileName; - ActivityResultLauncher saveImageToLauncher = registerForActivityResult( - new ActivityResultContracts.CreateDocument(), - uri -> { - if (uri != null) { - String filepath = AppConfig.getExternalTempDir() + File.separator + mCacheFileName; - File cachefile = new File(filepath); - - ContentResolver resolver = getContentResolver(); - - IoThreadPoolExecutor.getInstance().execute(() -> { - InputStream is = null; - OutputStream os = null; - try { - is = new FileInputStream(cachefile); - os = resolver.openOutputStream(uri); - IOUtils.copy(is, os); - } catch (IOException e) { - e.printStackTrace(); - } finally { - IOUtils.closeQuietly(is); - IOUtils.closeQuietly(os); - runOnUiThread(() -> Toast.makeText(GalleryActivity.this, getString(R.string.image_saved, uri.getPath()), Toast.LENGTH_SHORT).show()); - } - //noinspection ResultOfMethodCallIgnored - cachefile.delete(); - }); - } - - - }); - @Nullable - private GLRootView mGLRootView; - @Nullable - private GalleryView mGalleryView; - @Nullable - private GalleryProvider2 mGalleryProvider; - @Nullable - private GalleryAdapter mGalleryAdapter; - @Nullable - private WindowInsetsControllerCompat insetsController; - @Nullable - private ColorView mMaskView; - @Nullable - private View mClock; - @Nullable - private TextView mProgress; - @Nullable - private View mBattery; - @Nullable - private View mSeekBarPanel; - private final ValueAnimator.AnimatorUpdateListener mUpdateSliderListener = new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - if (null != mSeekBarPanel) { - mSeekBarPanel.requestLayout(); - } - } - }; - @Nullable - private TextView mLeftText; - @Nullable - private TextView mRightText; - @Nullable - private ReversibleSeekBar mSeekBar; - private ObjectAnimator mSeekBarPanelAnimator; - private final SimpleAnimatorListener mShowSliderListener = new SimpleAnimatorListener() { - @Override - public void onAnimationEnd(Animator animation) { - mSeekBarPanelAnimator = null; - } - }; - private final SimpleAnimatorListener mHideSliderListener = new SimpleAnimatorListener() { - @Override - public void onAnimationEnd(Animator animation) { - mSeekBarPanelAnimator = null; - if (mSeekBarPanel != null) { - mSeekBarPanel.setVisibility(View.INVISIBLE); - } - } - }; - private final Runnable mHideSliderRunnable = new Runnable() { - @Override - public void run() { - if (mSeekBarPanel != null) { - hideSlider(mSeekBarPanel); - } - } - }; - private int mLayoutMode; - private int mSize; - private int mCurrentIndex; - private int mSavingPage = -1; - private EditTextDialogBuilder builder; - private boolean dialogShown = false; - private AlertDialog dialog; - - private void buildProvider() { - if (mGalleryProvider != null) { - return; - } - - if (ACTION_DIR.equals(mAction)) { - if (mFilename != null) { - mGalleryProvider = new DirGalleryProvider(UniFile.fromFile(new File(mFilename))); - } - } else if (ACTION_EH.equals(mAction)) { - if (mGalleryInfo != null) { - mGalleryProvider = new EhGalleryProvider(this, mGalleryInfo); - } - } else if (Intent.ACTION_VIEW.equals(mAction)) { - if (mUri != null) { - try { - grantUriPermission(BuildConfig.APPLICATION_ID, mUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); - } catch (Exception e) { - Toast.makeText(this, R.string.error_reading_failed, Toast.LENGTH_SHORT).show(); - } - mGalleryProvider = new ArchiveGalleryProvider(this, mUri); - } - } - } - - private void onInit() { - Intent intent = getIntent(); - if (intent == null) { - return; - } - - mAction = intent.getAction(); - mFilename = intent.getStringExtra(KEY_FILENAME); - mUri = intent.getData(); - mGalleryInfo = intent.getParcelableExtra(KEY_GALLERY_INFO); - mPage = intent.getIntExtra(KEY_PAGE, -1); - buildProvider(); - } - - private void onRestore(@NonNull Bundle savedInstanceState) { - mAction = savedInstanceState.getString(KEY_ACTION); - mFilename = savedInstanceState.getString(KEY_FILENAME); - mUri = savedInstanceState.getParcelable(KEY_URI); - mGalleryInfo = savedInstanceState.getParcelable(KEY_GALLERY_INFO); - mPage = savedInstanceState.getInt(KEY_PAGE, -1); - mCurrentIndex = savedInstanceState.getInt(KEY_CURRENT_INDEX); - buildProvider(); - } - - @Override - protected void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - outState.putString(KEY_ACTION, mAction); - outState.putString(KEY_FILENAME, mFilename); - outState.putParcelable(KEY_URI, mUri); - if (mGalleryInfo != null) { - outState.putParcelable(KEY_GALLERY_INFO, mGalleryInfo); - } - outState.putInt(KEY_PAGE, mPage); - outState.putInt(KEY_CURRENT_INDEX, mCurrentIndex); - } - - @Override - protected void attachBaseContext(@NonNull Context newBase) { - switch (Settings.getReadTheme()) { - case 1: - getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES); - break; - case 2: - getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_NO); - break; - } - super.attachBaseContext(newBase); - } - - @Override - @SuppressWarnings({"WrongConstant"}) - protected void onCreate(@Nullable Bundle savedInstanceState) { - if (Settings.getReadingFullscreen()) { - Window w = getWindow(); - w.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, - WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); - } - - super.onCreate(savedInstanceState); - if (savedInstanceState == null) { - onInit(); - } else { - onRestore(savedInstanceState); - } - - builder = new EditTextDialogBuilder(this, null, getString(R.string.archive_passwd)); - builder.setTitle(getString(R.string.archive_need_passwd)); - builder.setPositiveButton(getString(android.R.string.ok), null); - dialog = builder.create(); - dialog.setCanceledOnTouchOutside(false); - - if (mGalleryProvider == null) { - finish(); - return; - } - - ArchiveGalleryProvider.showPasswd = new ShowPasswdDialogHandler(this); - - mGalleryProvider.start(); - - // Get start page - int startPage; - if (savedInstanceState == null) { - startPage = mPage >= 0 ? mPage : mGalleryProvider.getStartPage(); - } else { - startPage = mCurrentIndex; - } - - setContentView(R.layout.activity_gallery); - mGLRootView = (GLRootView) ViewUtils.$$(this, R.id.gl_root_view); - mGalleryAdapter = new GalleryAdapter(mGLRootView, mGalleryProvider); - Resources resources = getResources(); - mGalleryView = new GalleryView.Builder(this, mGalleryAdapter) - .setListener(this) - .setLayoutMode(Settings.getReadingDirection()) - .setScaleMode(Settings.getPageScaling()) - .setStartPosition(Settings.getStartPosition()) - .setStartPage(startPage) - .setBackgroundColor(ResourcesKt.resolveColor(getTheme(), android.R.attr.colorBackground)) - .setPagerInterval(Settings.getShowPageInterval() ? resources.getDimensionPixelOffset(R.dimen.gallery_pager_interval) : 0) - .setScrollInterval(Settings.getShowPageInterval() ? resources.getDimensionPixelOffset(R.dimen.gallery_scroll_interval) : 0) - .setPageMinHeight(resources.getDimensionPixelOffset(R.dimen.gallery_page_min_height)) - .setPageInfoInterval(resources.getDimensionPixelOffset(R.dimen.gallery_page_info_interval)) - .setProgressColor(ResourcesUtils.getAttrColor(this, androidx.appcompat.R.attr.colorPrimary)) - .setProgressSize(resources.getDimensionPixelOffset(R.dimen.gallery_progress_size)) - .setPageTextColor(ResourcesKt.resolveColor(getTheme(), android.R.attr.textColorSecondary)) - .setPageTextSize(resources.getDimensionPixelOffset(R.dimen.gallery_page_text_size)) - .setPageTextTypeface(Typeface.DEFAULT) - .setErrorTextColor(resources.getColor(R.color.red_500)) - .setErrorTextSize(resources.getDimensionPixelOffset(R.dimen.gallery_error_text_size)) - .setDefaultErrorString(resources.getString(R.string.error_unknown)) - .setEmptyString(resources.getString(R.string.error_empty)) - .build(); - - mGLRootView.setContentPane(mGalleryView); - mGalleryProvider.setListener(mGalleryAdapter); - mGalleryProvider.setGLRoot(mGLRootView); - - WindowCompat.setDecorFitsSystemWindows(getWindow(), false); - insetsController = WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView()); - if (insetsController != null) { - if (Settings.getReadingFullscreen()) { - insetsController.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); - // This does not hide navigation bar sometimes on 31, framework bug??? - insetsController.hide(WindowInsetsCompat.Type.systemBars()); - } else { - insetsController.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_TOUCH); - insetsController.show(WindowInsetsCompat.Type.systemBars()); - } - boolean night = ConfigurationKt.isNight(getResources().getConfiguration()); - insetsController.setAppearanceLightStatusBars(!night); - insetsController.setAppearanceLightNavigationBars(!night); - } - - mMaskView = (ColorView) ViewUtils.$$(this, R.id.mask); - mMaskView.setOnGenericMotionListener((view, event) -> { - if (mGalleryView == null) { - return false; - } - if (event.getAction() == MotionEvent.ACTION_SCROLL) { - float scroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL) * 300; - if (scroll < 0.0f) { - if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) { - mGalleryView.pageLeft(); - } else if (mLayoutMode == GalleryView.LAYOUT_LEFT_TO_RIGHT) { - mGalleryView.pageRight(); - } else if (mLayoutMode == GalleryView.LAYOUT_TOP_TO_BOTTOM) { - mGalleryView.onScroll(0, -scroll, 0, -scroll, 0, -scroll); - } - } else { - if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) { - mGalleryView.pageRight(); - } else if (mLayoutMode == GalleryView.LAYOUT_LEFT_TO_RIGHT) { - mGalleryView.pageLeft(); - } else if (mLayoutMode == GalleryView.LAYOUT_TOP_TO_BOTTOM) { - mGalleryView.onScroll(0, -scroll, 0, -scroll, 0, -scroll); - } - } - } - return false; - }); - mClock = ViewUtils.$$(this, R.id.clock); - mProgress = (TextView) ViewUtils.$$(this, R.id.progress); - mBattery = ViewUtils.$$(this, R.id.battery); - mClock.setVisibility(Settings.getShowClock() ? View.VISIBLE : View.GONE); - mProgress.setVisibility(Settings.getShowProgress() ? View.VISIBLE : View.GONE); - mBattery.setVisibility(Settings.getShowBattery() ? View.VISIBLE : View.GONE); - - mSeekBarPanel = ViewUtils.$$(this, R.id.seek_bar_panel); - mLeftText = (TextView) ViewUtils.$$(mSeekBarPanel, R.id.left); - mRightText = (TextView) ViewUtils.$$(mSeekBarPanel, R.id.right); - mSeekBar = (ReversibleSeekBar) ViewUtils.$$(mSeekBarPanel, R.id.seek_bar); - mSeekBar.setOnSeekBarChangeListener(this); - - mSize = mGalleryProvider.size(); - mCurrentIndex = startPage; - if (mGalleryView != null) { - mLayoutMode = mGalleryView.getLayoutMode(); - } - updateSlider(); - - // Update keep screen on - if (Settings.getKeepScreenOn()) { - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } else { - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - - // Orientation - int orientation; - switch (Settings.getScreenRotation()) { - default: - case 0: - orientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; - break; - case 1: - orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT; - break; - case 2: - orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; - break; - case 3: - orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR; - break; - } - setRequestedOrientation(orientation); - - // Screen lightness - setScreenLightness(Settings.getCustomScreenLightness(), Settings.getScreenLightness()); - - // Cutout - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; - } - GalleryHeader galleryHeader = findViewById(R.id.gallery_header); - ViewCompat.setOnApplyWindowInsetsListener(galleryHeader, (v, insets) -> { - if (!Settings.getReadingFullscreen()) { - galleryHeader.setTopInsets(insets.getInsets(WindowInsetsCompat.Type.statusBars()).top); - } else { - galleryHeader.setDisplayCutout(insets.getDisplayCutout()); - } - return WindowInsetsCompat.CONSUMED; - }); - - if (Settings.getGuideGallery()) { - FrameLayout mainLayout = (FrameLayout) ViewUtils.$$(this, R.id.main); - mainLayout.addView(new GalleryGuideView(this)); - } - } - - @Override - protected void onDestroy() { - super.onDestroy(); - mGLRootView = null; - mGalleryView = null; - if (mGalleryAdapter != null) { - mGalleryAdapter.clearUploader(); - mGalleryAdapter = null; - } - if (mGalleryProvider != null) { - mGalleryProvider.setListener(null); - mGalleryProvider.stop(); - mGalleryProvider = null; - } - - mMaskView = null; - mClock = null; - mProgress = null; - mBattery = null; - mSeekBarPanel = null; - mLeftText = null; - mRightText = null; - mSeekBar = null; - - SimpleHandler.getInstance().removeCallbacks(mHideSliderRunnable); - } - - @Override - protected void onPause() { - super.onPause(); - - if (mGLRootView != null) { - mGLRootView.onPause(); - } - } - - @Override - protected void onResume() { - super.onResume(); - - if (mGLRootView != null) { - mGLRootView.onResume(); - } - } ActivityResultLauncher requestStoragePermissionLauncher = registerForActivityResult( - new ActivityResultContracts.RequestPermission(), - result -> { - if (result && mSavingPage != -1) { - saveImage(mSavingPage); - } else { - Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show(); - } - mSavingPage = -1; - }); - - @Override - public void onWindowFocusChanged(boolean hasFocus) { - super.onWindowFocusChanged(hasFocus); - - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - if (mGalleryView == null) { - return super.onKeyDown(keyCode, event); - } - - // Check volume - if (Settings.getVolumePage()) { - if (Settings.getReverseVolumePage()) { - if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) { - keyCode = KeyEvent.KEYCODE_VOLUME_DOWN; - } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { - keyCode = KeyEvent.KEYCODE_VOLUME_UP; - } - } - if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) { - if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) { - mGalleryView.pageRight(); - } else { - mGalleryView.pageLeft(); - } - return true; - } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { - if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) { - mGalleryView.pageLeft(); - } else { - mGalleryView.pageRight(); - } - return true; - } - } - - // Check keyboard and Dpad - switch (keyCode) { - case KeyEvent.KEYCODE_PAGE_UP: - case KeyEvent.KEYCODE_DPAD_UP: - if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) { - mGalleryView.pageRight(); - } else { - mGalleryView.pageLeft(); - } - return true; - case KeyEvent.KEYCODE_DPAD_LEFT: - mGalleryView.pageLeft(); - return true; - case KeyEvent.KEYCODE_PAGE_DOWN: - case KeyEvent.KEYCODE_DPAD_DOWN: - if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) { - mGalleryView.pageLeft(); - } else { - mGalleryView.pageRight(); - } - return true; - case KeyEvent.KEYCODE_DPAD_RIGHT: - mGalleryView.pageRight(); - return true; - case KeyEvent.KEYCODE_DPAD_CENTER: - case KeyEvent.KEYCODE_SPACE: - case KeyEvent.KEYCODE_MENU: - onTapMenuArea(); - return true; - } - - return super.onKeyDown(keyCode, event); - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - // Check volume - if (Settings.getVolumePage()) { - if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || - keyCode == KeyEvent.KEYCODE_VOLUME_UP) { - return true; - } - } - - // Check keyboard and Dpad - if (keyCode == KeyEvent.KEYCODE_PAGE_UP || - keyCode == KeyEvent.KEYCODE_PAGE_DOWN || - keyCode == KeyEvent.KEYCODE_DPAD_LEFT || - keyCode == KeyEvent.KEYCODE_DPAD_UP || - keyCode == KeyEvent.KEYCODE_DPAD_RIGHT || - keyCode == KeyEvent.KEYCODE_DPAD_DOWN || - keyCode == KeyEvent.KEYCODE_DPAD_CENTER || - keyCode == KeyEvent.KEYCODE_SPACE || - keyCode == KeyEvent.KEYCODE_MENU) { - return true; - } - - return super.onKeyUp(keyCode, event); - } - - @SuppressLint("SetTextI18n") - private void updateProgress() { - if (mProgress == null) { - return; - } - if (mSize <= 0 || mCurrentIndex < 0) { - mProgress.setText(null); - } else { - mProgress.setText((mCurrentIndex + 1) + "/" + mSize); - } - } - - @SuppressLint("SetTextI18n") - private void updateSlider() { - if (mSeekBar == null || mRightText == null || mLeftText == null || mSize <= 0 || mCurrentIndex < 0) { - return; - } - - TextView start; - TextView end; - if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) { - start = mRightText; - end = mLeftText; - mSeekBar.setReverse(true); - } else { - start = mLeftText; - end = mRightText; - mSeekBar.setReverse(false); - } - start.setText(Integer.toString(mCurrentIndex + 1)); - end.setText(Integer.toString(mSize)); - mSeekBar.setMax(mSize - 1); - mSeekBar.setProgress(mCurrentIndex); - } - - @Override - @SuppressLint("SetTextI18n") - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - TextView start; - if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) { - start = mRightText; - } else { - start = mLeftText; - } - if (fromUser && null != start) { - start.setText(Integer.toString(progress + 1)); - } - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - SimpleHandler.getInstance().removeCallbacks(mHideSliderRunnable); - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - SimpleHandler.getInstance().postDelayed(mHideSliderRunnable, HIDE_SLIDER_DELAY); - int progress = seekBar.getProgress(); - if (progress != mCurrentIndex && null != mGalleryView) { - mGalleryView.setCurrentPage(progress); - } - } - - @Override - public void onUpdateCurrentIndex(int index) { - if (null != mGalleryProvider) { - mGalleryProvider.putStartPage(index); - } - - NotifyTask task = mNotifyTaskPool.pop(); - if (task == null) { - task = new NotifyTask(); - } - task.setData(NotifyTask.KEY_CURRENT_INDEX, index); - SimpleHandler.getInstance().post(task); - } - - @Override - public void onTapSliderArea() { - NotifyTask task = mNotifyTaskPool.pop(); - if (task == null) { - task = new NotifyTask(); - } - task.setData(NotifyTask.KEY_TAP_SLIDER_AREA, 0); - SimpleHandler.getInstance().post(task); - } - - @Override - public void onTapMenuArea() { - NotifyTask task = mNotifyTaskPool.pop(); - if (task == null) { - task = new NotifyTask(); - } - task.setData(NotifyTask.KEY_TAP_MENU_AREA, 0); - SimpleHandler.getInstance().post(task); - } - - @Override - public void onTapErrorText(int index) { - NotifyTask task = mNotifyTaskPool.pop(); - if (task == null) { - task = new NotifyTask(); - } - task.setData(NotifyTask.KEY_TAP_ERROR_TEXT, index); - SimpleHandler.getInstance().post(task); - } - - @Override - public void onLongPressPage(int index) { - NotifyTask task = mNotifyTaskPool.pop(); - if (task == null) { - task = new NotifyTask(); - } - task.setData(NotifyTask.KEY_LONG_PRESS_PAGE, index); - SimpleHandler.getInstance().post(task); - } - - private void showSlider(View sliderPanel) { - if (null != mSeekBarPanelAnimator) { - mSeekBarPanelAnimator.cancel(); - mSeekBarPanelAnimator = null; - } - - sliderPanel.setTranslationY(sliderPanel.getHeight()); - sliderPanel.setVisibility(View.VISIBLE); - - mSeekBarPanelAnimator = ObjectAnimator.ofFloat(sliderPanel, "translationY", 0.0f); - mSeekBarPanelAnimator.setDuration(SLIDER_ANIMATION_DURING); - mSeekBarPanelAnimator.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR); - mSeekBarPanelAnimator.addUpdateListener(mUpdateSliderListener); - mSeekBarPanelAnimator.addListener(mShowSliderListener); - mSeekBarPanelAnimator.start(); - - if (insetsController != null) { - if (Settings.getReadingFullscreen()) - insetsController.show(WindowInsetsCompat.Type.systemBars()); - } - } - - private void hideSlider(View sliderPanel) { - if (null != mSeekBarPanelAnimator) { - mSeekBarPanelAnimator.cancel(); - mSeekBarPanelAnimator = null; - } - - mSeekBarPanelAnimator = ObjectAnimator.ofFloat(sliderPanel, "translationY", sliderPanel.getHeight()); - mSeekBarPanelAnimator.setDuration(SLIDER_ANIMATION_DURING); - mSeekBarPanelAnimator.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR); - mSeekBarPanelAnimator.addUpdateListener(mUpdateSliderListener); - mSeekBarPanelAnimator.addListener(mHideSliderListener); - mSeekBarPanelAnimator.start(); - - if (insetsController != null) { - if (Settings.getReadingFullscreen()) - insetsController.hide(WindowInsetsCompat.Type.systemBars()); - } - } - - /** - * @param lightness 0 - 200 - */ - private void setScreenLightness(boolean enable, int lightness) { - if (null == mMaskView) { - return; - } - - Window w = getWindow(); - WindowManager.LayoutParams lp = w.getAttributes(); - if (enable) { - lightness = MathUtils.clamp(lightness, 0, 200); - if (lightness > 100) { - mMaskView.setColor(0); - // Avoid BRIGHTNESS_OVERRIDE_OFF, - // screen may be off when lp.screenBrightness is 0.0f - lp.screenBrightness = Math.max((lightness - 100) / 100.0f, 0.01f); - } else { - mMaskView.setColor(MathUtils.lerp(0xde, 0x00, lightness / 100.0f) << 24); - lp.screenBrightness = 0.01f; - } - } else { - mMaskView.setColor(0); - lp.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE; - } - w.setAttributes(lp); - } - - private void shareImage(int page) { - if (null == mGalleryProvider) { - return; - } - - File dir = AppConfig.getExternalTempDir(); - if (null == dir) { - Toast.makeText(this, R.string.error_cant_create_temp_file, Toast.LENGTH_SHORT).show(); - return; - } - UniFile file; - if (null == (file = mGalleryProvider.save(page, UniFile.fromFile(dir), mGalleryProvider.getImageFilename(page)))) { - Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show(); - return; - } - String filename = file.getName(); - if (filename == null) { - Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show(); - return; - } - - String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension( - MimeTypeMap.getFileExtensionFromUrl(filename)); - if (TextUtils.isEmpty(mimeType)) { - mimeType = "image/jpeg"; - } - - Uri uri = FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".fileprovider", new File(dir, filename)); - - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_SEND); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - intent.putExtra(Intent.EXTRA_STREAM, uri); - if (mGalleryInfo != null) - intent.putExtra(Intent.EXTRA_TEXT, EhUrl.getGalleryDetailUrl(mGalleryInfo.gid, mGalleryInfo.token)); - intent.setDataAndType(uri, mimeType); - - try { - startActivity(Intent.createChooser(intent, getString(R.string.share_image))); - } catch (Throwable e) { - ExceptionUtils.throwIfFatal(e); - Toast.makeText(this, R.string.error_cant_find_activity, Toast.LENGTH_SHORT).show(); - } - } - - private void copyImage(int page) { - if (null == mGalleryProvider) { - return; - } - - File dir = AppConfig.getExternalCopyTempDir(); - if (null == dir) { - Toast.makeText(this, R.string.error_cant_create_temp_file, Toast.LENGTH_SHORT).show(); - return; - } - UniFile file; - if (null == (file = mGalleryProvider.save(page, UniFile.fromFile(dir), mGalleryProvider.getImageFilename(page)))) { - Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show(); - return; - } - String filename = file.getName(); - if (filename == null) { - Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show(); - return; - } - - Uri uri = FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".fileprovider", new File(dir, filename)); - - var clipboardManager = getSystemService(ClipboardManager.class); - if (clipboardManager != null) { - var clipData = ClipData.newUri(getContentResolver(), "ehviewer", uri); - clipboardManager.setPrimaryClip(clipData); - Toast.makeText(this, getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT).show(); - } - } - - private void saveImage(int page) { - if (null == mGalleryProvider) { - return; - } - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED) { - mSavingPage = page; - requestStoragePermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE); - return; - } - - String filename = mGalleryProvider.getImageFilenameWithExtension(page); - String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension( - MimeTypeMap.getFileExtensionFromUrl(filename)); - if (TextUtils.isEmpty(mimeType)) { - mimeType = "image/jpeg"; - } - - String realPath; - ContentResolver resolver = getContentResolver(); - ContentValues values = new ContentValues(); - values.put(MediaStore.MediaColumns.DISPLAY_NAME, filename); - values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis()); - values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + File.separator + AppConfig.APP_DIRNAME); - values.put(MediaStore.MediaColumns.IS_PENDING, 1); - realPath = Environment.DIRECTORY_PICTURES + File.separator + AppConfig.APP_DIRNAME; - } else { - File path = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), AppConfig.APP_DIRNAME); - realPath = path.toString(); - if (!FileUtils.ensureDirectory(path)) { - Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show(); - return; - } - values.put(MediaStore.MediaColumns.DATA, path + File.separator + filename); - } - Uri imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); - if (imageUri == null) { - Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show(); - return; - } - if (!mGalleryProvider.save(page, UniFile.fromMediaUri(this, imageUri))) { - try { - resolver.delete(imageUri, null, null); - } catch (Exception e) { - e.printStackTrace(); - } - Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show(); - return; - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - ContentValues contentValues = new ContentValues(); - contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0); - resolver.update(imageUri, contentValues, null, null); - } - - Toast.makeText(this, getString(R.string.image_saved, realPath + File.separator + filename), Toast.LENGTH_SHORT).show(); - } - - private void saveImageTo(int page) { - if (null == mGalleryProvider) { - return; - } - File dir = AppConfig.getExternalTempDir(); - if (null == dir) { - Toast.makeText(this, R.string.error_cant_create_temp_file, Toast.LENGTH_SHORT).show(); - return; - } - UniFile file; - if (null == (file = mGalleryProvider.save(page, UniFile.fromFile(dir), mGalleryProvider.getImageFilename(page)))) { - Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show(); - return; - } - String filename = file.getName(); - if (filename == null) { - Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show(); - return; - } - mCacheFileName = filename; - try { - saveImageToLauncher.launch(filename); - } catch (Throwable e) { - ExceptionUtils.throwIfFatal(e); - Toast.makeText(this, R.string.error_cant_find_activity, Toast.LENGTH_SHORT).show(); - } - } - - private void showPageDialog(final int page) { - Resources resources = GalleryActivity.this.getResources(); - BaseDialogBuilder builder = new BaseDialogBuilder(GalleryActivity.this); - builder.setTitle(resources.getString(R.string.page_menu_title, page + 1)); - - final CharSequence[] items; - items = new CharSequence[]{ - getString(R.string.page_menu_refresh), - getString(R.string.page_menu_share), - getString(android.R.string.copy), - getString(R.string.page_menu_save), - getString(R.string.page_menu_save_to)}; - pageDialogListener(builder, items, page); - builder.show(); - } - - private void pageDialogListener(AlertDialog.Builder builder, CharSequence[] items, int page) { - builder.setItems(items, (dialog, which) -> { - if (mGalleryProvider == null) { - return; - } - - switch (which) { - case 0: // Refresh - mGalleryProvider.removeCache(page); - mGalleryProvider.forceRequest(page); - break; - case 1: // Share - shareImage(page); - break; - case 2: // Copy - copyImage(page); - break; - case 3: // Save - saveImage(page); - break; - case 4: // Save to - saveImageTo(page); - break; - } - }); - } - - @Nullable - private String getGalleryDetailUrl() { - long gid; - String token; - if (mGalleryInfo != null) { - gid = mGalleryInfo.gid; - token = mGalleryInfo.token; - } else { - return null; - } - return EhUrl.getGalleryDetailUrl(gid, token, 0, false); - } - - @Override - public void onProvideAssistContent(AssistContent outContent) { - super.onProvideAssistContent(outContent); - - String url = getGalleryDetailUrl(); - if (url != null) { - outContent.setWebUri(Uri.parse(url)); - } - } - - private void showPasswdDialog() { - if (!dialogShown) { - dialogShown = true; - dialog.show(); - if (dialog.getButton(AlertDialog.BUTTON_POSITIVE) != null) { - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> { - GalleryActivity.this.onProvidePasswd(); - }); - } - dialog.setOnCancelListener(v -> finish()); - } - } - - private void onProvidePasswd() { - String passwd = builder.getText(); - if (passwd.isEmpty()) - builder.setError(getString(R.string.passwd_cannot_be_empty)); - else { - ArchiveGalleryProvider.passwd = passwd; - ArchiveGalleryProvider.pv.v(); - } - } - - private void onPasswdWrong() { - builder.setError(getString(R.string.passwd_wrong)); - } - - private void onPasswdCorrect() { - dialog.dismiss(); - } - - private static class ShowPasswdDialogHandler extends Handler { - - //弱引用持有HandlerActivity , GC 回收时会被回收掉 - private final WeakReference weakReference; - - public ShowPasswdDialogHandler(GalleryActivity activity) { - this.weakReference = new WeakReference(activity); - } - - @Override - public void handleMessage(Message msg) { - GalleryActivity activity = weakReference.get(); - super.handleMessage(msg); - if (null != activity) { - switch (msg.what) { - case 0: - activity.showPasswdDialog(); - break; - case 1: - activity.onPasswdWrong(); - break; - case 2: - activity.onPasswdCorrect(); - break; - } - } - } - } - - private class GalleryMenuHelper implements DialogInterface.OnClickListener { - - private final View mView; - private final Spinner mScreenRotation; - private final Spinner mReadingDirection; - private final Spinner mScaleMode; - private final Spinner mStartPosition; - private final Spinner mReadTheme; - private final MaterialSwitch mKeepScreenOn; - private final MaterialSwitch mShowClock; - private final MaterialSwitch mShowProgress; - private final MaterialSwitch mShowBattery; - private final MaterialSwitch mShowPageInterval; - private final MaterialSwitch mVolumePage; - private final MaterialSwitch mReverseVolumePage; - private final MaterialSwitch mReadingFullscreen; - private final MaterialSwitch mCustomScreenLightness; - private final SeekBar mScreenLightness; - - @SuppressLint("InflateParams") - public GalleryMenuHelper(Context context) { - mView = LayoutInflater.from(context).inflate(R.layout.dialog_gallery_menu, null); - mScreenRotation = mView.findViewById(R.id.screen_rotation); - mReadingDirection = mView.findViewById(R.id.reading_direction); - mScaleMode = mView.findViewById(R.id.page_scaling); - mStartPosition = mView.findViewById(R.id.start_position); - mReadTheme = mView.findViewById(R.id.read_theme); - mKeepScreenOn = mView.findViewById(R.id.keep_screen_on); - mShowClock = mView.findViewById(R.id.show_clock); - mShowProgress = mView.findViewById(R.id.show_progress); - mShowBattery = mView.findViewById(R.id.show_battery); - mShowPageInterval = mView.findViewById(R.id.show_page_interval); - mVolumePage = mView.findViewById(R.id.volume_page); - mReverseVolumePage = mView.findViewById(R.id.reverse_volume_page); - mReadingFullscreen = mView.findViewById(R.id.reading_fullscreen); - mCustomScreenLightness = mView.findViewById(R.id.custom_screen_lightness); - mScreenLightness = mView.findViewById(R.id.screen_lightness); - - mScreenRotation.setSelection(Settings.getScreenRotation()); - mReadingDirection.setSelection(Settings.getReadingDirection()); - mScaleMode.setSelection(Settings.getPageScaling()); - mStartPosition.setSelection(Settings.getStartPosition()); - mReadTheme.setSelection(Settings.getReadTheme()); - mKeepScreenOn.setChecked(Settings.getKeepScreenOn()); - mShowClock.setChecked(Settings.getShowClock()); - mShowProgress.setChecked(Settings.getShowProgress()); - mShowBattery.setChecked(Settings.getShowBattery()); - mShowPageInterval.setChecked(Settings.getShowPageInterval()); - mVolumePage.setChecked(Settings.getVolumePage()); - mVolumePage.setOnCheckedChangeListener((buttonView, isChecked) -> mReverseVolumePage.setEnabled(isChecked)); - mReverseVolumePage.setEnabled(mVolumePage.isChecked()); - mReverseVolumePage.setChecked(Settings.getReverseVolumePage()); - mReadingFullscreen.setChecked(Settings.getReadingFullscreen()); - mCustomScreenLightness.setChecked(Settings.getCustomScreenLightness()); - mScreenLightness.setProgress(Settings.getScreenLightness()); - mScreenLightness.setEnabled(Settings.getCustomScreenLightness()); - - mCustomScreenLightness.setOnCheckedChangeListener((buttonView, isChecked) -> mScreenLightness.setEnabled(isChecked)); - } - - public View getView() { - return mView; - } - - @Override - public void onClick(DialogInterface dialog, int which) { - if (mGalleryView == null) { - return; - } - - int screenRotation = mScreenRotation.getSelectedItemPosition(); - int layoutMode = GalleryView.sanitizeLayoutMode(mReadingDirection.getSelectedItemPosition()); - int scaleMode = GalleryView.sanitizeScaleMode(mScaleMode.getSelectedItemPosition()); - int startPosition = GalleryView.sanitizeStartPosition(mStartPosition.getSelectedItemPosition()); - int readTheme = mReadTheme.getSelectedItemPosition(); - boolean keepScreenOn = mKeepScreenOn.isChecked(); - boolean showClock = mShowClock.isChecked(); - boolean showProgress = mShowProgress.isChecked(); - boolean showBattery = mShowBattery.isChecked(); - boolean showPageInterval = mShowPageInterval.isChecked(); - boolean volumePage = mVolumePage.isChecked(); - boolean reverseVolumePage = mReverseVolumePage.isChecked(); - boolean readingFullscreen = mReadingFullscreen.isChecked(); - boolean customScreenLightness = mCustomScreenLightness.isChecked(); - int screenLightness = mScreenLightness.getProgress(); - - boolean oldReadingFullscreen = Settings.getReadingFullscreen(); - int oldReadTheme = Settings.getReadTheme(); - - Settings.putScreenRotation(screenRotation); - Settings.putReadingDirection(layoutMode); - Settings.putPageScaling(scaleMode); - Settings.putStartPosition(startPosition); - Settings.putReadTheme(readTheme); - Settings.putKeepScreenOn(keepScreenOn); - Settings.putShowClock(showClock); - Settings.putShowProgress(showProgress); - Settings.putShowBattery(showBattery); - Settings.putShowPageInterval(showPageInterval); - Settings.putVolumePage(volumePage); - Settings.putReverseVolumePage(reverseVolumePage); - Settings.putReadingFullscreen(readingFullscreen); - Settings.putCustomScreenLightness(customScreenLightness); - Settings.putScreenLightness(screenLightness); - - int orientation; - switch (screenRotation) { - default: - case 0: - orientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; - break; - case 1: - orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT; - break; - case 2: - orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; - break; - case 3: - orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR; - break; - } - setRequestedOrientation(orientation); - mGalleryView.setLayoutMode(layoutMode); - mGalleryView.setScaleMode(scaleMode); - mGalleryView.setStartPosition(startPosition); - if (keepScreenOn) { - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } else { - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - if (mClock != null) { - mClock.setVisibility(showClock ? View.VISIBLE : View.GONE); - } - if (mProgress != null) { - mProgress.setVisibility(showProgress ? View.VISIBLE : View.GONE); - } - if (mBattery != null) { - mBattery.setVisibility(showBattery ? View.VISIBLE : View.GONE); - } - mGalleryView.setPagerInterval(showPageInterval ? getResources().getDimensionPixelOffset(R.dimen.gallery_pager_interval) : 0); - mGalleryView.setScrollInterval(showPageInterval ? getResources().getDimensionPixelOffset(R.dimen.gallery_scroll_interval) : 0); - setScreenLightness(customScreenLightness, screenLightness); - - // Update slider - mLayoutMode = layoutMode; - updateSlider(); - - if (oldReadingFullscreen != readingFullscreen || oldReadTheme != readTheme) { - recreate(); - } - } - } - - private class NotifyTask implements Runnable { - - public static final int KEY_LAYOUT_MODE = 0; - public static final int KEY_SIZE = 1; - public static final int KEY_CURRENT_INDEX = 2; - public static final int KEY_TAP_SLIDER_AREA = 3; - public static final int KEY_TAP_MENU_AREA = 4; - public static final int KEY_TAP_ERROR_TEXT = 5; - public static final int KEY_LONG_PRESS_PAGE = 6; - - private int mKey; - private int mValue; - - public void setData(int key, int value) { - mKey = key; - mValue = value; - } - - private void onTapMenuArea() { - BaseDialogBuilder builder = new BaseDialogBuilder(GalleryActivity.this); - GalleryMenuHelper helper = new GalleryMenuHelper(builder.getContext()); - builder.setTitle(R.string.gallery_menu_title) - .setView(helper.getView()) - .setPositiveButton(android.R.string.ok, helper).show(); - } - - private void onTapSliderArea() { - if (mSeekBarPanel == null || mSize <= 0 || mCurrentIndex < 0) { - return; - } - - SimpleHandler.getInstance().removeCallbacks(mHideSliderRunnable); - - if (mSeekBarPanel.getVisibility() == View.VISIBLE) { - hideSlider(mSeekBarPanel); - } else { - showSlider(mSeekBarPanel); - SimpleHandler.getInstance().postDelayed(mHideSliderRunnable, HIDE_SLIDER_DELAY); - } - } - - private void onTapErrorText(int index) { - if (mGalleryProvider != null) { - mGalleryProvider.forceRequest(index); - } - } - - private void onLongPressPage(final int index) { - showPageDialog(index); - } - - @Override - public void run() { - switch (mKey) { - case KEY_LAYOUT_MODE: - GalleryActivity.this.mLayoutMode = mValue; - updateSlider(); - break; - case KEY_SIZE: - GalleryActivity.this.mSize = mValue; - updateSlider(); - updateProgress(); - break; - case KEY_CURRENT_INDEX: - GalleryActivity.this.mCurrentIndex = mValue; - updateSlider(); - updateProgress(); - break; - case KEY_TAP_MENU_AREA: - onTapMenuArea(); - break; - case KEY_TAP_SLIDER_AREA: - onTapSliderArea(); - break; - case KEY_TAP_ERROR_TEXT: - onTapErrorText(mValue); - break; - case KEY_LONG_PRESS_PAGE: - onLongPressPage(mValue); - break; - } - mNotifyTaskPool.push(this); - } - } - - private class GalleryAdapter extends SimpleAdapter { - - public GalleryAdapter(@NonNull GLRootView glRootView, @NonNull GalleryProvider provider) { - super(glRootView, provider); - } - - @Override - public void onDataChanged() { - super.onDataChanged(); - - if (mGalleryProvider != null) { - int size = mGalleryProvider.size(); - NotifyTask task = mNotifyTaskPool.pop(); - if (task == null) { - task = new NotifyTask(); - } - task.setData(NotifyTask.KEY_SIZE, size); - SimpleHandler.getInstance().post(task); - } - } - } - - - - -} diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/DownloadsScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/DownloadsScene.java index 90d65d616..09a5dc090 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/DownloadsScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/DownloadsScene.java @@ -70,7 +70,7 @@ import com.hippo.ehviewer.dao.DownloadLabel; import com.hippo.ehviewer.download.DownloadManager; import com.hippo.ehviewer.download.DownloadService; import com.hippo.ehviewer.spider.SpiderDen; -import com.hippo.ehviewer.ui.GalleryActivity; +import eu.kanade.tachiyomi.ui.reader.ReaderActivity; import com.hippo.ehviewer.widget.SimpleRatingView; import com.hippo.scene.Announcer; import com.hippo.unifile.UniFile; @@ -596,9 +596,9 @@ public class DownloadsScene extends ToolbarScene return false; } - Intent intent = new Intent(activity, GalleryActivity.class); - intent.setAction(GalleryActivity.ACTION_EH); - intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, list.get(position)); + Intent intent = new Intent(activity, ReaderActivity.class); + intent.setAction(ReaderActivity.ACTION_EH); + intent.putExtra(ReaderActivity.KEY_GALLERY_INFO, list.get(position)); startActivity(intent); return true; } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryDetailScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryDetailScene.java index a988335dc..d963908c9 100755 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryDetailScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryDetailScene.java @@ -89,11 +89,11 @@ import com.hippo.ehviewer.client.parser.RateGalleryParser; import com.hippo.ehviewer.client.parser.VoteTagParser; import com.hippo.ehviewer.dao.DownloadInfo; import com.hippo.ehviewer.dao.Filter; -import com.hippo.ehviewer.gallery.EhGalleryProvider; -import com.hippo.ehviewer.gallery.GalleryProvider2; +import com.hippo.ehviewer.gallery.EhPageLoader; +import com.hippo.ehviewer.gallery.PageLoader2; import com.hippo.ehviewer.spider.SpiderDen; import com.hippo.ehviewer.ui.CommonOperations; -import com.hippo.ehviewer.ui.GalleryActivity; +import eu.kanade.tachiyomi.ui.reader.ReaderActivity; import com.hippo.ehviewer.ui.GalleryInfoBottomSheet; import com.hippo.ehviewer.ui.MainActivity; import com.hippo.ehviewer.ui.annotation.WholeLifeCircle; @@ -452,7 +452,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O super.onResume(); if (mRead != null) { try { - GalleryProvider2 galleryProvider = new EhGalleryProvider(requireContext(), mGalleryInfo); + PageLoader2 galleryProvider = new EhPageLoader(requireContext(), mGalleryInfo); galleryProvider.start(); int startPage = galleryProvider.getStartPage(); if (startPage != 0) { @@ -866,10 +866,10 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O if (mPage != 0) { Snackbar.make(requireActivity().findViewById(R.id.snackbar), getString(R.string.read_from, mPage), Snackbar.LENGTH_LONG) .setAction(R.string.read, v -> { - Intent intent = new Intent(requireContext(), GalleryActivity.class); - intent.setAction(GalleryActivity.ACTION_EH); - intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, mGalleryDetail); - intent.putExtra(GalleryActivity.KEY_PAGE, mPage); + Intent intent = new Intent(requireContext(), ReaderActivity.class); + intent.setAction(ReaderActivity.ACTION_EH); + intent.putExtra(ReaderActivity.KEY_GALLERY_INFO, mGalleryDetail); + intent.putExtra(ReaderActivity.KEY_PAGE, mPage); startActivity(intent); }) .show(); @@ -1164,9 +1164,9 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O galleryInfo = mGalleryDetail; } if (galleryInfo != null) { - Intent intent = new Intent(activity, GalleryActivity.class); - intent.setAction(GalleryActivity.ACTION_EH); - intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, galleryInfo); + Intent intent = new Intent(activity, ReaderActivity.class); + intent.setAction(ReaderActivity.ACTION_EH); + intent.putExtra(ReaderActivity.KEY_GALLERY_INFO, galleryInfo); startActivity(intent); } } else if (mNewerVersion == v) { @@ -1298,10 +1298,10 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O o = v.getTag(R.id.index); if (null != galleryInfo && o instanceof Integer) { int index = (Integer) o; - Intent intent = new Intent(context, GalleryActivity.class); - intent.setAction(GalleryActivity.ACTION_EH); - intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, galleryInfo); - intent.putExtra(GalleryActivity.KEY_PAGE, index); + Intent intent = new Intent(context, ReaderActivity.class); + intent.setAction(ReaderActivity.ACTION_EH); + intent.putExtra(ReaderActivity.KEY_GALLERY_INFO, galleryInfo); + intent.putExtra(ReaderActivity.KEY_PAGE, index); startActivity(intent); } } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.java index b23ea59ce..151aefe31 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.java @@ -87,7 +87,7 @@ import com.hippo.ehviewer.dao.DownloadLabel; import com.hippo.ehviewer.dao.QuickSearch; import com.hippo.ehviewer.download.DownloadManager; import com.hippo.ehviewer.ui.CommonOperations; -import com.hippo.ehviewer.ui.GalleryActivity; +import eu.kanade.tachiyomi.ui.reader.ReaderActivity; import com.hippo.ehviewer.ui.MainActivity; import com.hippo.ehviewer.ui.dialog.SelectItemWithIconAdapter; import com.hippo.ehviewer.widget.GalleryInfoContentHelper; @@ -977,9 +977,9 @@ public final class GalleryListScene extends SearchBarScene .setAdapter(new SelectItemWithIconAdapter(context, items, icons), (dialog, which) -> { switch (which) { case 0: // Read - Intent intent = new Intent(activity, GalleryActivity.class); - intent.setAction(GalleryActivity.ACTION_EH); - intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, gi); + Intent intent = new Intent(activity, ReaderActivity.class); + intent.setAction(ReaderActivity.ACTION_EH); + intent.putExtra(ReaderActivity.KEY_GALLERY_INFO, gi); startActivity(intent); break; case 1: // Download diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryPreviewsScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryPreviewsScene.java index 44e2caa5b..1db3f3bf7 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryPreviewsScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryPreviewsScene.java @@ -49,7 +49,7 @@ import com.hippo.ehviewer.client.data.GalleryInfo; import com.hippo.ehviewer.client.data.GalleryPreview; import com.hippo.ehviewer.client.data.PreviewSet; import com.hippo.ehviewer.client.exception.EhException; -import com.hippo.ehviewer.ui.GalleryActivity; +import eu.kanade.tachiyomi.ui.reader.ReaderActivity; import com.hippo.ehviewer.ui.MainActivity; import com.hippo.scene.SceneFragment; import com.hippo.widget.ContentLayout; @@ -227,10 +227,10 @@ public class GalleryPreviewsScene extends ToolbarScene { if (null != context && null != mHelper && null != mGalleryInfo) { GalleryPreview p = mHelper.getDataAtEx(position); if (p != null) { - Intent intent = new Intent(context, GalleryActivity.class); - intent.setAction(GalleryActivity.ACTION_EH); - intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, mGalleryInfo); - intent.putExtra(GalleryActivity.KEY_PAGE, p.getPosition()); + Intent intent = new Intent(context, ReaderActivity.class); + intent.setAction(ReaderActivity.ACTION_EH); + intent.putExtra(ReaderActivity.KEY_GALLERY_INFO, mGalleryInfo); + intent.putExtra(ReaderActivity.KEY_PAGE, p.getPosition()); startActivity(intent); } } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/HistoryScene.kt b/app/src/main/java/com/hippo/ehviewer/ui/scene/HistoryScene.kt index 745c385ff..b64903a6d 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/HistoryScene.kt +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/HistoryScene.kt @@ -45,7 +45,7 @@ import com.hippo.ehviewer.dao.DownloadInfo import com.hippo.ehviewer.dao.HistoryInfo import com.hippo.ehviewer.download.DownloadManager.DownloadInfoListener import com.hippo.ehviewer.ui.CommonOperations -import com.hippo.ehviewer.ui.GalleryActivity +import eu.kanade.tachiyomi.ui.reader.ReaderActivity import com.hippo.ehviewer.ui.dialog.SelectItemWithIconAdapter import com.hippo.ehviewer.widget.SimpleRatingView import com.hippo.scene.Announcer @@ -267,9 +267,9 @@ class HistoryScene : ToolbarScene() { ) { _: DialogInterface?, which: Int -> when (which) { 0 -> { - val intent = Intent(activity, GalleryActivity::class.java) - intent.action = GalleryActivity.ACTION_EH - intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, gi) + val intent = Intent(activity, ReaderActivity::class.java) + intent.action = ReaderActivity.ACTION_EH + intent.putExtra(ReaderActivity.KEY_GALLERY_INFO, gi) startActivity(intent) } diff --git a/app/src/main/java/com/hippo/ehviewer/widget/GalleryGuideView.java b/app/src/main/java/com/hippo/ehviewer/widget/GalleryGuideView.java deleted file mode 100644 index 189a051f4..000000000 --- a/app/src/main/java/com/hippo/ehviewer/widget/GalleryGuideView.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.ehviewer.widget; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import com.hippo.ehviewer.R; -import com.hippo.ehviewer.Settings; -import com.hippo.yorozuya.ViewUtils; - -public class GalleryGuideView extends ViewGroup implements View.OnClickListener { - - private final float[] mPoints = new float[3 * 4]; - private int mBgColor; - private Paint mPaint; - private int mStep; - - private TextView mLeftText; - private TextView mRightText; - private TextView mMenuText; - private TextView mProgressText; - private TextView mLongClickText; - - public GalleryGuideView(Context context) { - super(context); - init(context); - } - - public GalleryGuideView(Context context, AttributeSet attrs) { - super(context, attrs); - init(context); - } - - public GalleryGuideView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(context); - } - - private void init(Context context) { - mBgColor = Color.GRAY; - mPaint = new Paint(); - mPaint.setColor(Color.WHITE); - mPaint.setStyle(Paint.Style.STROKE); - mPaint.setStrokeWidth(context.getResources().getDimension(R.dimen.gallery_guide_divider_width)); - setOnClickListener(this); - setWillNotDraw(false); - bind(); - } - - private void bind() { - // Clear up - removeAllViews(); - mLeftText = null; - mRightText = null; - mMenuText = null; - mProgressText = null; - mLongClickText = null; - - switch (mStep) { - case 0: - bind1(); - break; - default: - case 1: - bind2(); - break; - } - } - - private void bind1() { - LayoutInflater inflater = LayoutInflater.from(getContext()); - inflater.inflate(R.layout.widget_gallery_guide_1, this); - mLeftText = (TextView) getChildAt(0); - mRightText = (TextView) getChildAt(1); - mMenuText = (TextView) getChildAt(2); - mProgressText = (TextView) getChildAt(3); - } - - private void bind2() { - LayoutInflater inflater = LayoutInflater.from(getContext()); - inflater.inflate(R.layout.widget_gallery_guide_2, this); - mLongClickText = (TextView) getChildAt(0); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int widthSize = MeasureSpec.getSize(widthMeasureSpec); - int widthMode = MeasureSpec.getMode(widthMeasureSpec); - int heightSize = MeasureSpec.getSize(heightMeasureSpec); - int heightMode = MeasureSpec.getMode(heightMeasureSpec); - if (MeasureSpec.EXACTLY != widthMode || MeasureSpec.EXACTLY != heightMode) { - throw new IllegalStateException(); - } - - switch (mStep) { - case 0: - mLeftText.measure(MeasureSpec.makeMeasureSpec(widthSize / 3, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY)); - mRightText.measure(MeasureSpec.makeMeasureSpec(widthSize / 3, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY)); - mMenuText.measure(MeasureSpec.makeMeasureSpec(widthSize / 3, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(heightSize / 2, MeasureSpec.EXACTLY)); - mProgressText.measure(MeasureSpec.makeMeasureSpec(widthSize / 3, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(heightSize / 2, MeasureSpec.EXACTLY)); - break; - default: - case 1: - mLongClickText.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY)); - break; - } - - setMeasuredDimension(widthSize, heightSize); - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - int width = r - l; - int height = b - t; - - switch (mStep) { - case 0: - mLeftText.layout(0, 0, width / 3, height); - mRightText.layout(width * 2 / 3, 0, width, height); - mMenuText.layout(width / 3, 0, width * 2 / 3, height / 2); - mProgressText.layout(width / 3, height / 2, width * 2 / 3, height); - break; - default: - case 1: - mLongClickText.layout(0, 0, width, height); - break; - } - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - super.onSizeChanged(w, h, oldw, oldh); - - if (0 == mStep) { - mPoints[0] = w / 3; - mPoints[1] = 0; - mPoints[2] = w / 3; - mPoints[3] = h; - - mPoints[4] = w * 2 / 3; - mPoints[5] = 0; - mPoints[6] = w * 2 / 3; - mPoints[7] = h; - - mPoints[8] = w / 3; - mPoints[9] = h / 2; - mPoints[10] = w * 2 / 3; - mPoints[11] = h / 2; - } - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - canvas.drawColor(mBgColor); - if (0 == mStep) { - canvas.drawLines(mPoints, mPaint); - } - } - - @Override - public void onClick(View v) { - switch (mStep) { - case 0: - mStep++; - bind(); - break; - default: - case 1: - Settings.putGuideGallery(false); - ViewUtils.removeFromParent(this); - break; - } - } -} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/GalleryHeader.java b/app/src/main/java/com/hippo/ehviewer/widget/GalleryHeader.java deleted file mode 100644 index bf73be3fa..000000000 --- a/app/src/main/java/com/hippo/ehviewer/widget/GalleryHeader.java +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright 2019 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.ehviewer.widget; - -import android.content.Context; -import android.graphics.Rect; -import android.os.Build; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; -import androidx.core.view.DisplayCutoutCompat; - -import com.hippo.ehviewer.R; -import com.hippo.yorozuya.ObjectUtils; - -public class GalleryHeader extends ViewGroup { - - private final Rect batteryRect = new Rect(); - private final Rect progressRect = new Rect(); - private final Rect clockRect = new Rect(); - private final int[] location = new int[2]; - private DisplayCutoutCompat displayCutout; - private int topInsets = 0; - private View battery; - private View progress; - private View clock; - private int lastX = 0; - private int lastY = 0; - - public GalleryHeader(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public void setDisplayCutout(@Nullable DisplayCutoutCompat displayCutout) { - if (!ObjectUtils.equal(this.displayCutout, displayCutout)) { - this.displayCutout = displayCutout; - requestLayout(); - } - } - - public void setTopInsets(int topInsets) { - if (this.topInsets != topInsets) { - this.topInsets = topInsets; - requestLayout(); - } - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - - battery = findViewById(R.id.battery); - progress = findViewById(R.id.progress); - clock = findViewById(R.id.clock); - } - - private void measureChild(Rect rect, View view, int width, int paddingLeft, int paddingRight) { - int left; - MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams(); - if (view == battery) { - left = paddingLeft + lp.leftMargin; - } else if (view == progress) { - left = paddingLeft + (width - paddingLeft - paddingRight) / 2 - view.getMeasuredWidth() / 2; - } else { - left = width - paddingRight - lp.rightMargin - view.getMeasuredWidth(); - } - rect.set(left, lp.topMargin + topInsets, left + view.getMeasuredWidth(), lp.topMargin + topInsets + view.getMeasuredHeight()); - } - - @RequiresApi(api = Build.VERSION_CODES.P) - private boolean offsetVertically(Rect rect, View view, int width) { - int offset = 0; - - measureChild(rect, view, width, 0, 0); - rect.offset(lastX, lastY); - - for (Rect notch : displayCutout.getBoundingRects()) { - if (Rect.intersects(notch, rect)) { - offset = Math.max(offset, notch.bottom - lastY); - } - } - - if (offset != 0) { - rect.offset(-lastX, -lastY); - rect.offset(0, offset); - return true; - } else { - return false; - } - } - - @RequiresApi(api = Build.VERSION_CODES.P) - private int getOffsetLeft(Rect rect, View view, int width) { - int offset = 0; - - measureChild(rect, view, width, 0, 0); - rect.offset(lastX, lastY); - - for (Rect notch : displayCutout.getBoundingRects()) { - if (Rect.intersects(notch, rect)) { - offset = Math.max(offset, notch.right - lastX); - } - } - - return offset; - } - - @RequiresApi(api = Build.VERSION_CODES.P) - private int getOffsetRight(Rect rect, View view, int width) { - int offset = 0; - - measureChild(rect, view, width, 0, 0); - rect.offset(lastX, lastY); - - for (Rect notch : displayCutout.getBoundingRects()) { - if (Rect.intersects(notch, rect)) { - offset = Math.max(offset, lastX + width - notch.left); - } - } - - return offset; - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) { - throw new IllegalStateException(); - } - int width = MeasureSpec.getSize(widthMeasureSpec); - - int height = 0; - for (int i = 0; i < getChildCount(); i++) { - View child = getChildAt(i); - measureChild(child, widthMeasureSpec, heightMeasureSpec); - MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); - height = Math.max(height, child.getMeasuredHeight() + lp.topMargin + topInsets); - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && displayCutout != null) { - // Check progress covered - if (offsetVertically(progressRect, progress, width)) { - offsetVertically(batteryRect, battery, width); - offsetVertically(clockRect, clock, width); - height = Math.max(progressRect.bottom, Math.max(batteryRect.bottom, clockRect.bottom)); - } else { - // Clamp left and right - int paddingLeft = getOffsetLeft(batteryRect, battery, width); - int paddingRight = getOffsetRight(clockRect, clock, width); - measureChild(batteryRect, battery, width, paddingLeft, paddingRight); - measureChild(progressRect, progress, width, paddingLeft, paddingRight); - measureChild(clockRect, clock, width, paddingLeft, paddingRight); - } - } else { - measureChild(batteryRect, battery, width, 0, 0); - measureChild(progressRect, progress, width, 0, 0); - measureChild(clockRect, clock, width, 0, 0); - } - - setMeasuredDimension(width, height); - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - battery.layout(batteryRect.left, batteryRect.top, batteryRect.right, batteryRect.bottom); - progress.layout(progressRect.left, progressRect.top, progressRect.right, progressRect.bottom); - clock.layout(clockRect.left, clockRect.top, clockRect.right, clockRect.bottom); - - getLocationOnScreen(location); - if (lastX != location[0] || lastY != location[1]) { - lastX = location[0]; - lastY = location[1]; - requestLayout(); - } - } - - @Override - public MarginLayoutParams generateLayoutParams(AttributeSet attrs) { - return new MarginLayoutParams(getContext(), attrs); - } - - @Override - protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { - return p instanceof MarginLayoutParams; - } - - @Override - protected MarginLayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { - if (lp instanceof MarginLayoutParams) { - return new MarginLayoutParams((MarginLayoutParams) lp); - } - return new MarginLayoutParams(lp); - } -} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/ReversibleSeekBar.java b/app/src/main/java/com/hippo/ehviewer/widget/ReversibleSeekBar.java deleted file mode 100644 index c6daa7ecf..000000000 --- a/app/src/main/java/com/hippo/ehviewer/widget/ReversibleSeekBar.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.ehviewer.widget; - -import android.content.Context; -import android.graphics.Canvas; -import android.util.AttributeSet; -import android.view.MotionEvent; - -import androidx.appcompat.widget.AppCompatSeekBar; - -public class ReversibleSeekBar extends AppCompatSeekBar { - - private boolean mReverse; - - public ReversibleSeekBar(Context context) { - super(context); - } - - public ReversibleSeekBar(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public ReversibleSeekBar(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - public void setReverse(boolean reverse) { - mReverse = reverse; - invalidate(); - } - - @Override - public void draw(Canvas canvas) { - boolean reverse = mReverse; - int saveCount = 0; - if (reverse) { - saveCount = canvas.save(); - float px = this.getWidth() / 2.0f; - float py = this.getHeight() / 2.0f; - canvas.scale(-1, 1, px, py); - } - super.draw(canvas); - if (reverse) { - canvas.restoreToCount(saveCount); - } - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - boolean reverse = mReverse; - float x = 0.0f, y = 0.0f; - if (reverse) { - x = event.getX(); - y = event.getY(); - event.setLocation(getWidth() - x, y); - } - boolean result = super.onTouchEvent(event); - if (reverse) { - event.setLocation(x, y); - } - return result; - } -} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/SeekBarPanel.java b/app/src/main/java/com/hippo/ehviewer/widget/SeekBarPanel.java deleted file mode 100644 index 7fe6ce04f..000000000 --- a/app/src/main/java/com/hippo/ehviewer/widget/SeekBarPanel.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.ehviewer.widget; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.widget.LinearLayout; -import android.widget.SeekBar; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.hippo.ehviewer.R; -import com.hippo.yorozuya.ViewUtils; - -public class SeekBarPanel extends LinearLayout { - - private final int[] mLocation = new int[2]; - private SeekBar mSeekBar; - - public SeekBarPanel(Context context) { - super(context); - init(); - } - - public SeekBarPanel(Context context, @Nullable @org.jetbrains.annotations.Nullable AttributeSet attrs) { - super(context, attrs); - init(); - } - - public SeekBarPanel(Context context, @Nullable @org.jetbrains.annotations.Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(); - } - - public SeekBarPanel(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - init(); - } - - private void init() { - post(() -> { - var rootWindowInsets = getRootWindowInsets(); - if (rootWindowInsets != null) { - setPadding(0, 0, 0, rootWindowInsets.getSystemWindowInsetBottom()); - } - }); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mSeekBar = (SeekBar) ViewUtils.$$(this, R.id.seek_bar); - } - - @SuppressLint("ClickableViewAccessibility") - @Override - public boolean onTouchEvent(@NonNull MotionEvent event) { - if (mSeekBar == null) { - return super.onTouchEvent(event); - } else { - ViewUtils.getLocationInAncestor(mSeekBar, mLocation, this); - final float offsetX = -mLocation[0]; - final float offsetY = -mLocation[1]; - event.offsetLocation(offsetX, offsetY); - mSeekBar.onTouchEvent(event); - event.offsetLocation(-offsetX, -offsetY); - return true; - } - } -} diff --git a/app/src/main/java/com/hippo/glgallery/DownUpDetector.java b/app/src/main/java/com/hippo/glgallery/DownUpDetector.java deleted file mode 100644 index e8139f6f1..000000000 --- a/app/src/main/java/com/hippo/glgallery/DownUpDetector.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glgallery; - -import android.view.MotionEvent; - -class DownUpDetector { - private final DownUpListener mListener; - private boolean mStillDown; - - public DownUpDetector(DownUpListener listener) { - mListener = listener; - } - - private void setState(boolean down, MotionEvent e) { - if (down == mStillDown) return; - mStillDown = down; - if (down) { - mListener.onDown(e); - } else { - mListener.onUp(e); - } - } - - public void onTouchEvent(MotionEvent ev) { - switch (ev.getActionMasked()) { - case MotionEvent.ACTION_DOWN: - setState(true, ev); - break; - case MotionEvent.ACTION_POINTER_DOWN: - mListener.onPointerDown(ev); - break; - case MotionEvent.ACTION_UP: - mListener.onPointerUp(ev); - setState(false, ev); - break; - case MotionEvent.ACTION_CANCEL: - setState(false, ev); - break; - } - } - - public boolean isDown() { - return mStillDown; - } - - public interface DownUpListener { - void onDown(MotionEvent e); - - void onUp(MotionEvent e); - - void onPointerDown(MotionEvent e); - - void onPointerUp(MotionEvent e); - } -} diff --git a/app/src/main/java/com/hippo/glgallery/Fling.java b/app/src/main/java/com/hippo/glgallery/Fling.java deleted file mode 100644 index d6d57a65f..000000000 --- a/app/src/main/java/com/hippo/glgallery/Fling.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glgallery; - -import android.content.Context; -import android.hardware.SensorManager; -import android.view.ViewConfiguration; -import android.view.animation.Interpolator; - -import com.hippo.glview.anim.Animation; - -abstract class Fling extends Animation { - - private static final float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9)); - private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1) - private static final float START_TENSION = 0.5f; - private static final float END_TENSION = 1.0f; - private static final float P1 = START_TENSION * INFLEXION; - private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION); - private static final float FLING_FRICTION = ViewConfiguration.getScrollFriction(); - - private static final int NB_SAMPLES = 100; - private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1]; - private static final Interpolator FLING_INTERPOLATOR = new Interpolator() { - @Override - public float getInterpolation(float input) { - final int index = (int) (NB_SAMPLES * input); - float distanceCoef = 1.f; - float velocityCoef; - if (index < NB_SAMPLES) { - final float t_inf = (float) index / NB_SAMPLES; - final float t_sup = (float) (index + 1) / NB_SAMPLES; - final float d_inf = SPLINE_POSITION[index]; - final float d_sup = SPLINE_POSITION[index + 1]; - velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); - distanceCoef = d_inf + (input - t_inf) * velocityCoef; - } - return distanceCoef; - } - }; - private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1]; - - static { - float x_min = 0.0f; - float y_min = 0.0f; - for (int i = 0; i < NB_SAMPLES; i++) { - final float alpha = (float) i / NB_SAMPLES; - - float x_max = 1.0f; - float x, tx, coef; - while (true) { - x = x_min + (x_max - x_min) / 2.0f; - coef = 3.0f * x * (1.0f - x); - tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x; - if (Math.abs(tx - alpha) < 1E-5) break; - if (tx > alpha) x_max = x; - else x_min = x; - } - SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x; - - float y_max = 1.0f; - float y, dy; - while (true) { - y = y_min + (y_max - y_min) / 2.0f; - coef = 3.0f * y * (1.0f - y); - dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y; - if (Math.abs(dy - alpha) < 1E-5) break; - if (dy > alpha) y_max = y; - else y_min = y; - } - SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y; - } - SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f; - } - - private final float mPhysicalCoeff; - - public Fling(Context context) { - final float ppi = context.getResources().getDisplayMetrics().density * 160.0f; - mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2) - * 39.37f // inch/meter - * ppi - * 0.84f; // look and feel tuning - setInterpolator(FLING_INTERPOLATOR); - } - - private double getSplineDeceleration(int velocity) { - return Math.log(INFLEXION * Math.abs(velocity) / (FLING_FRICTION * mPhysicalCoeff)); - } - - /* Returns the duration, expressed in milliseconds */ - protected int getSplineFlingDuration(int velocity) { - final double l = getSplineDeceleration(velocity); - final double decelMinusOne = DECELERATION_RATE - 1.0; - return (int) (1000.0 * Math.exp(l / decelMinusOne)); - } - - protected double getSplineFlingDistance(int velocity) { - final double l = getSplineDeceleration(velocity); - final double decelMinusOne = DECELERATION_RATE - 1.0; - return FLING_FRICTION * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l); - } - - /** - * Modifies mDuration to the duration it takes to get from start to newFinal using the - * spline interpolation. The previous duration was needed to get to oldFinal. - **/ - protected int adjustDuration(int start, int oldFinal, int newFinal, int duration) { - final int oldDistance = oldFinal - start; - final int newDistance = newFinal - start; - final float x = Math.abs((float) newDistance / oldDistance); - final int index = (int) (NB_SAMPLES * x); - if (index < NB_SAMPLES) { - final float x_inf = (float) index / NB_SAMPLES; - final float x_sup = (float) (index + 1) / NB_SAMPLES; - final float t_inf = SPLINE_TIME[index]; - final float t_sup = SPLINE_TIME[index + 1]; - final float timeCoef = t_inf + (x - x_inf) / (x_sup - x_inf) * (t_sup - t_inf); - duration *= timeCoef; - } - return duration; - } -} diff --git a/app/src/main/java/com/hippo/glgallery/GalleryPageView.java b/app/src/main/java/com/hippo/glgallery/GalleryPageView.java deleted file mode 100644 index 6170195df..000000000 --- a/app/src/main/java/com/hippo/glgallery/GalleryPageView.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glgallery; - -import com.hippo.glview.glrenderer.BasicTexture; -import com.hippo.glview.glrenderer.Texture; -import com.hippo.glview.image.GLImageMovableTextView; -import com.hippo.glview.image.ImageMovableTextTexture; -import com.hippo.glview.image.ImageTexture; -import com.hippo.glview.view.Gravity; -import com.hippo.glview.widget.GLFrameLayout; -import com.hippo.glview.widget.GLLinearLayout; -import com.hippo.glview.widget.GLProgressView; -import com.hippo.glview.widget.GLTextureView; - -public class GalleryPageView extends GLFrameLayout { - - public static final int INVALID_INDEX = -1; - - public static final float PROGRESS_GONE = -1.0f; - public static final float PROGRESS_INDETERMINATE = -2.0f; - - private final ImageView mImage; - private final GLLinearLayout mInfo; - private final GLImageMovableTextView mPage; - private final GLTextureView mError; - private final GLProgressView mProgress; - - private final int mMinHeight; - - private int mIndex = INVALID_INDEX; - - public GalleryPageView(ImageMovableTextTexture pageTextTexture, - int progressColor, int progressBgColor, int progressSize, - int minHeight, int infoInterval) { - // Add image - mImage = new ImageView(); - GravityLayoutParams glp = new GravityLayoutParams(LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT); - addComponent(mImage, glp); - - // Add other panel - mInfo = new GLLinearLayout(); - mInfo.setOrientation(GLLinearLayout.VERTICAL); - mInfo.setInterval(infoInterval); - glp = new GravityLayoutParams(LayoutParams.WRAP_CONTENT, - LayoutParams.WRAP_CONTENT); - glp.gravity = Gravity.CENTER; - addComponent(mInfo, glp); - - // Add page - mPage = new GLImageMovableTextView(); - mPage.setTextTexture(pageTextTexture); - GLLinearLayout.LayoutParams lp = new GLLinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, - LayoutParams.WRAP_CONTENT); - lp.gravity = Gravity.CENTER_HORIZONTAL; - mInfo.addComponent(mPage, lp); - - // Add error - mError = new GLTextureView(); - lp = new GLLinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, - LayoutParams.WRAP_CONTENT); - lp.gravity = Gravity.CENTER_HORIZONTAL; - mInfo.addComponent(mError, lp); - - // Add progress - mProgress = new GLProgressView(); - mProgress.setBgColor(progressBgColor); - mProgress.setColor(progressColor); - mProgress.setMinimumWidth(progressSize); - mProgress.setMinimumHeight(progressSize); - lp = new GLLinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, - LayoutParams.WRAP_CONTENT); - lp.gravity = Gravity.CENTER_HORIZONTAL; - mInfo.addComponent(mProgress, lp); - - mMinHeight = minHeight; - } - - @Override - protected int getSuggestedMinimumHeight() { - // The height of the actual image may be smaller than mPageMinHeight. - // Set min height as 0 when the image is visible. - // For PageLayoutManager, min height is useless. - if (mImage.getVisibility() == VISIBLE) { - return 0; - } else { - return mMinHeight; - } - } - - int getIndex() { - return mIndex; - } - - void setIndex(int index) { - mIndex = index; - } - - public void showImage() { - mImage.setVisibility(VISIBLE); - mInfo.setVisibility(GONE); - } - - public void showInfo() { - // For image valid rect - mImage.setVisibility(INVISIBLE); - mInfo.setVisibility(VISIBLE); - } - - private void unbindImage() { - ImageTexture texture = mImage.getImageTexture(); - if (texture != null) { - mImage.setImageTexture(null); - texture.recycle(); - } - } - - public void setImage(ImageTexture imageTexture) { - unbindImage(); - if (imageTexture != null) { - mImage.setImageTexture(imageTexture); - } - } - - public void setPage(int page) { - mPage.setVisibility(VISIBLE); - mPage.setText(Integer.toString(page)); - } - - public void hidePage() { - mPage.setVisibility(GONE); - } - - public void setProgress(float progress) { - if (progress == PROGRESS_GONE) { - mProgress.setVisibility(GONE); - } else if (progress == PROGRESS_INDETERMINATE) { - mProgress.setVisibility(VISIBLE); - mProgress.setIndeterminate(true); - } else { - mProgress.setVisibility(VISIBLE); - mProgress.setIndeterminate(false); - mProgress.setProgress(progress); - } - } - - private void unbindError() { - Texture texture = mError.getTexture(); - if (texture != null) { - mError.setTexture(null); - if (texture instanceof BasicTexture) { - ((BasicTexture) texture).recycle(); - } - } - } - - public void setError(String error, GalleryView galleryView) { - unbindError(); - if (error == null) { - mError.setVisibility(GONE); - } else { - mError.setVisibility(VISIBLE); - galleryView.bindErrorView(mError, error); - } - } - - ImageView getImageView() { - return mImage; - } - - boolean isLoaded() { - return mImage.getVisibility() == VISIBLE; - } - - boolean isError() { - return mError.getVisibility() == VISIBLE; - } - - boolean isUnderInfo(float x, float y) { - return mInfo.bounds().contains((int) x, (int) y); - } -} diff --git a/app/src/main/java/com/hippo/glgallery/GalleryProvider.java b/app/src/main/java/com/hippo/glgallery/GalleryProvider.java deleted file mode 100644 index 4d27c1b61..000000000 --- a/app/src/main/java/com/hippo/glgallery/GalleryProvider.java +++ /dev/null @@ -1,263 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glgallery; - -import android.util.LruCache; - -import androidx.annotation.IntDef; -import androidx.annotation.UiThread; - -import com.hippo.glview.glrenderer.GLCanvas; -import com.hippo.glview.image.ImageWrapper; -import com.hippo.glview.view.GLRoot; -import com.hippo.image.Image; -import com.hippo.yorozuya.ConcurrentPool; -import com.hippo.yorozuya.OSUtils; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -public abstract class GalleryProvider { - - public static final int STATE_WAIT = -1; - public static final int STATE_ERROR = -2; - - private final ConcurrentPool mNotifyTaskPool = new ConcurrentPool<>(5); - private final ImageCache mImageCache = new ImageCache(); - private volatile Listener mListener; - private volatile GLRoot mGLRoot; - private boolean mStarted = false; - - @UiThread - public void start() { - OSUtils.checkMainLoop(); - - if (mStarted) { - throw new IllegalStateException("Can't start it twice"); - } - mStarted = true; - } - - @UiThread - public void stop() { - OSUtils.checkMainLoop(); - mImageCache.evictAll(); - } - - public void setGLRoot(GLRoot glRoot) { - mGLRoot = glRoot; - } - - /** - * @return {@link #STATE_WAIT} for wait, - * {@link #STATE_ERROR} for error, {@link #getError()} to get error message, - * 0 for empty - */ - public abstract int size(); - - public final void request(int index) { - ImageWrapper imageWrapper = mImageCache.get(index); - if (imageWrapper != null) { - notifyPageSucceed(index, imageWrapper); - } else { - onRequest(index); - } - } - - public final void forceRequest(int index) { - onForceRequest(index); - } - - public void removeCache(int index) { - mImageCache.remove(index); - } - - protected abstract void onRequest(int index); - - protected abstract void onForceRequest(int index); - - public final void cancelRequest(int index) { - onCancelRequest(index); - } - - protected abstract void onCancelRequest(int index); - - public abstract String getError(); - - public void setListener(Listener listener) { - mListener = listener; - } - - public void notifyDataChanged() { - notify(NotifyTask.TYPE_DATA_CHANGED, -1, 0.0f, null, null); - } - - public void notifyDataChanged(int index) { - notify(NotifyTask.TYPE_DATA_CHANGED, index, 0.0f, null, null); - } - - public void notifyPageWait(int index) { - notify(NotifyTask.TYPE_WAIT, index, 0.0f, null, null); - } - - public void notifyPagePercent(int index, float percent) { - notify(NotifyTask.TYPE_PERCENT, index, percent, null, null); - } - - public void notifyPageSucceed(int index, Image image) { - ImageWrapper imageWrapper = new ImageWrapper(image); - mImageCache.add(index, imageWrapper); - notifyPageSucceed(index, imageWrapper); - } - - public void notifyPageSucceed(int index, ImageWrapper image) { - notify(NotifyTask.TYPE_SUCCEED, index, 0.0f, image, null); - } - - public void notifyPageFailed(int index, String error) { - notify(NotifyTask.TYPE_FAILED, index, 0.0f, null, error); - } - - private void notify(@NotifyTask.Type int type, int index, float percent, ImageWrapper image, String error) { - Listener listener = mListener; - if (listener == null) { - return; - } - - GLRoot glRoot = mGLRoot; - if (glRoot == null) { - return; - } - - NotifyTask task = mNotifyTaskPool.pop(); - if (task == null) { - task = new NotifyTask(listener, mNotifyTaskPool); - } - task.setData(type, index, percent, image, error); - glRoot.addOnGLIdleListener(task); - } - - public interface Listener { - - void onDataChanged(); - - void onPageWait(int index); - - void onPagePercent(int index, float percent); - - void onPageSucceed(int index, ImageWrapper image); - - void onPageFailed(int index, String error); - - void onDataChanged(int index); - } - - private static class NotifyTask implements GLRoot.OnGLIdleListener { - - public static final int TYPE_DATA_CHANGED = 0; - public static final int TYPE_WAIT = 1; - public static final int TYPE_PERCENT = 2; - public static final int TYPE_SUCCEED = 3; - public static final int TYPE_FAILED = 4; - private final Listener mListener; - private final ConcurrentPool mPool; - @Type - private int mType; - private int mIndex; - private float mPercent; - private ImageWrapper mImage; - private String mError; - - public NotifyTask(Listener listener, ConcurrentPool pool) { - mListener = listener; - mPool = pool; - } - - public void setData(@Type int type, int index, float percent, ImageWrapper image, String error) { - mType = type; - mIndex = index; - mPercent = percent; - mImage = image; - mError = error; - } - - @Override - public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) { - switch (mType) { - case TYPE_DATA_CHANGED: - if (mIndex < 0) { - mListener.onDataChanged(); - } else { - mListener.onDataChanged(mIndex); - } - break; - case TYPE_WAIT: - mListener.onPageWait(mIndex); - break; - case TYPE_PERCENT: - mListener.onPagePercent(mIndex, mPercent); - break; - case TYPE_SUCCEED: - mListener.onPageSucceed(mIndex, mImage); - break; - case TYPE_FAILED: - mListener.onPageFailed(mIndex, mError); - break; - } - - // Clean data - mImage = null; - mError = null; - // Push back - mPool.push(this); - - return false; - } - - @IntDef({TYPE_DATA_CHANGED, NotifyTask.TYPE_WAIT, TYPE_PERCENT, TYPE_SUCCEED, TYPE_FAILED}) - @Retention(RetentionPolicy.SOURCE) - public @interface Type { - } - } - - private static class ImageCache extends LruCache { - - private static final int CACHE_SIZE = 512 * 1024 * 1024; - - public ImageCache() { - super(CACHE_SIZE); - } - - public void add(Integer key, ImageWrapper value) { - if (!value.getAnimated()) - if (value.obtain()) - put(key, value); - } - - @Override - protected int sizeOf(Integer key, ImageWrapper value) { - return value.getWidth() * value.getHeight() * 4; - } - - @Override - protected void entryRemoved(boolean evicted, Integer key, ImageWrapper oldValue, ImageWrapper newValue) { - if (oldValue != null) { - oldValue.release(); - } - } - } -} diff --git a/app/src/main/java/com/hippo/glgallery/GalleryView.java b/app/src/main/java/com/hippo/glgallery/GalleryView.java deleted file mode 100644 index ebb533a61..000000000 --- a/app/src/main/java/com/hippo/glgallery/GalleryView.java +++ /dev/null @@ -1,1206 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glgallery; - -import android.content.Context; -import android.graphics.Color; -import android.graphics.Rect; -import android.graphics.Typeface; -import android.view.MotionEvent; - -import androidx.annotation.IntDef; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.hippo.glview.annotation.RenderThread; -import com.hippo.glview.glrenderer.BasicTexture; -import com.hippo.glview.glrenderer.GLCanvas; -import com.hippo.glview.glrenderer.StringTexture; -import com.hippo.glview.glrenderer.Texture; -import com.hippo.glview.image.ImageMovableTextTexture; -import com.hippo.glview.util.GalleryUtils; -import com.hippo.glview.view.AnimationTime; -import com.hippo.glview.view.GLRoot; -import com.hippo.glview.view.GLView; -import com.hippo.glview.widget.GLProgressView; -import com.hippo.glview.widget.GLTextureView; -import com.hippo.yorozuya.MathUtils; -import com.hippo.yorozuya.Pool; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; - -public final class GalleryView extends GLView implements GestureRecognizer.Listener { - - public static final int LAYOUT_LEFT_TO_RIGHT = 0; - public static final int LAYOUT_RIGHT_TO_LEFT = 1; - public static final int LAYOUT_TOP_TO_BOTTOM = 2; - public static final int SCALE_ORIGIN = ImageView.SCALE_ORIGIN; - public static final int SCALE_FIT_WIDTH = ImageView.SCALE_FIT_WIDTH; - public static final int SCALE_FIT_HEIGHT = ImageView.SCALE_FIT_HEIGHT; - public static final int SCALE_FIT = ImageView.SCALE_FIT; - public static final int SCALE_FIXED = ImageView.SCALE_FIXED; - public static final int START_POSITION_TOP_LEFT = ImageView.START_POSITION_TOP_LEFT; - public static final int START_POSITION_TOP_RIGHT = ImageView.START_POSITION_TOP_RIGHT; - public static final int START_POSITION_BOTTOM_LEFT = ImageView.START_POSITION_BOTTOM_LEFT; - public static final int START_POSITION_BOTTOM_RIGHT = ImageView.START_POSITION_BOTTOM_RIGHT; - public static final int START_POSITION_CENTER = ImageView.START_POSITION_CENTER; - private static final float[] LEFT_AREA = {0.0f, 0.0f, 1.0f / 3.0f, 1f}; - private static final float[] RIGHT_AREA = {2.0f / 3.0f, 0.0f, 1.0f, 1f}; - private static final float[] MENU_AREA = {1.0f / 3.0f, 0.0f, 2.0f / 3.0f, 1.0f / 2.0f}; - private static final float[] SLIDER_AREA = {1.0f / 3.0f, 1.0f / 2.0f, 2.0f / 3.0f, 1.0f}; - private static final int METHOD_ON_SINGLE_TAP_UP = 0; - private static final int METHOD_ON_SINGLE_TAP_CONFIRMED = 1; - private static final int METHOD_ON_DOUBLE_TAP = 2; - private static final int METHOD_ON_DOUBLE_TAP_CONFIRMED = 3; - private static final int METHOD_ON_LONG_PRESS = 4; - private static final int METHOD_ON_SCROLL = 5; - private static final int METHOD_ON_FLING = 6; - private static final int METHOD_ON_SCALE_BEGIN = 7; - private static final int METHOD_ON_SCALE = 8; - private static final int METHOD_ON_SCALE_END = 9; - private static final int METHOD_ON_DOWN = 10; - private static final int METHOD_ON_UP = 11; - private static final int METHOD_ON_POINTER_DOWN = 12; - private static final int METHOD_ON_POINTER_UP = 13; - private static final int METHOD_SET_LAYOUT_MODE = 14; - private static final int METHOD_SET_CURRENT_PAGE = 15; - private static final int METHOD_PAGE_LEFT = 16; - private static final int METHOD_PAGE_RIGHT = 17; - private static final int METHOD_SET_SCALE_MODE = 18; - private static final int METHOD_SET_START_POSITION = 19; - private static final int METHOD_ON_ATTACH_TO_ROOT = 20; - private static final int METHOD_SET_PAGER_INTERVAL = 21; - private static final int METHOD_SET_SCROLL_INTERVAL = 22; - private final Context mContext; - private final GestureRecognizer mGestureRecognizer; - @Nullable - private final Listener mListener; - private final Pool mGalleryPageViewPool = new Pool<>(5); - private final int mBackgroundColor; - private final int mPageMinHeight; - private final int mPageInfoInterval; - private final int mProgressColor; - private final int mProgressSize; - private final int mPageTextColor; - private final int mPageTextSize; - private final Typeface mPageTextTypeface; - private final int mErrorTextSize; - private final int mErrorTextColor; - private final String mDefaultErrorString; - private final String mEmptyString; - private final Rect mLeftArea = new Rect(); - private final Rect mRightArea = new Rect(); - private final Rect mMenuArea = new Rect(); - private final Rect mSliderArea = new Rect(); - private final List mMethodList = new ArrayList<>(5); - private final List mArgsList = new ArrayList<>(5); - private final List mMethodListTemp = new ArrayList<>(5); - private final List mArgsListTemp = new ArrayList<>(5); - private final AtomicInteger mCurrentIndex = new AtomicInteger(GalleryPageView.INVALID_INDEX); - private Adapter mAdapter; - private ImageMovableTextTexture mPageTextTexture; - private PagerLayoutManager mPagerLayoutManager; - private ScrollLayoutManager mScrollLayoutManager; - @Nullable - private LayoutManager mLayoutManager; - private GLProgressView mProgressCache; - private GLTextureView mErrorViewCache; - private int mPagerInterval; - private int mScrollInterval; - private boolean mEnableRequestFill = true; - private boolean mRequestFill = false; - private boolean mWillFill = false; - private boolean mScale = false; - private boolean mScroll = false; - private boolean mFirstScroll = false; - private int mLayoutMode = LAYOUT_RIGHT_TO_LEFT; - private int mScaleMode = ImageView.SCALE_FIT; - private int mStartPosition = ImageView.START_POSITION_TOP_LEFT; - private int mIndex; - - private GalleryView(Builder build) { - mContext = build.mContext; - mAdapter = build.mAdapter; - mAdapter.setGalleryView(this); - mListener = build.mListener; - mGestureRecognizer = new GestureRecognizer(mContext, this); - - mLayoutMode = build.mLayoutMode; - mScaleMode = build.mScaleMode; - mStartPosition = build.mStartPosition; - mIndex = MathUtils.clamp(build.mStartPage, 0, Integer.MAX_VALUE); - - mBackgroundColor = build.mBackgroundColor; - mPageMinHeight = build.mPageMinHeight; - mPagerInterval = build.mPagerInterval; - mScrollInterval = build.mScrollInterval; - mPageInfoInterval = build.mPageInfoInterval; - mProgressColor = build.mProgressColor; - mProgressSize = build.mProgressSize; - mPageTextColor = build.mPageTextColor; - mPageTextSize = build.mPageTextSize; - mPageTextTypeface = build.mPageTextTypeface; - mErrorTextColor = build.mErrorTextColor; - mErrorTextSize = build.mErrorTextSize; - - mDefaultErrorString = build.mDefaultErrorString; - mEmptyString = build.mEmptyString; - - setBackgroundColor(mBackgroundColor); - } - - @LayoutMode - public static int sanitizeLayoutMode(int layoutMode) { - if (layoutMode != GalleryView.LAYOUT_LEFT_TO_RIGHT && - layoutMode != GalleryView.LAYOUT_RIGHT_TO_LEFT && - layoutMode != GalleryView.LAYOUT_TOP_TO_BOTTOM) { - return GalleryView.LAYOUT_LEFT_TO_RIGHT; - } else { - return layoutMode; - } - } - - @ScaleMode - public static int sanitizeScaleMode(int scaleMode) { - if (scaleMode != GalleryView.SCALE_ORIGIN && - scaleMode != GalleryView.SCALE_FIT_WIDTH && - scaleMode != GalleryView.SCALE_FIT_HEIGHT && - scaleMode != GalleryView.SCALE_FIT && - scaleMode != GalleryView.SCALE_FIXED) { - return GalleryView.SCALE_FIT; - } else { - return scaleMode; - } - } - - @StartPosition - public static int sanitizeStartPosition(int startPosition) { - if (startPosition != GalleryView.START_POSITION_TOP_LEFT && - startPosition != GalleryView.START_POSITION_TOP_RIGHT && - startPosition != GalleryView.START_POSITION_BOTTOM_LEFT && - startPosition != GalleryView.START_POSITION_BOTTOM_RIGHT && - startPosition != GalleryView.START_POSITION_CENTER) { - return GalleryView.START_POSITION_TOP_LEFT; - } else { - return startPosition; - } - } - - private void ensurePagerLayoutManager() { - if (mPagerLayoutManager == null) { - mPagerLayoutManager = new PagerLayoutManager(mContext, this, - mScaleMode, mStartPosition, 1.0f, mPagerInterval); - } - } - - private void ensureScrollLayoutManager() { - if (mScrollLayoutManager == null) { - mScrollLayoutManager = new ScrollLayoutManager(mContext, this, mScrollInterval); - } - } - - private void attachLayoutManager() { - if (null != mLayoutManager) { - return; - } - - switch (mLayoutMode) { - case LAYOUT_LEFT_TO_RIGHT: - ensurePagerLayoutManager(); - mPagerLayoutManager.setMode(PagerLayoutManager.MODE_LEFT_TO_RIGHT); - mPagerLayoutManager.onAttach(mAdapter); - mPagerLayoutManager.setCurrentIndex(mIndex); - mAdapter = null; - mLayoutManager = mPagerLayoutManager; - break; - case LAYOUT_RIGHT_TO_LEFT: - ensurePagerLayoutManager(); - mPagerLayoutManager.setMode(PagerLayoutManager.MODE_RIGHT_TO_LEFT); - mPagerLayoutManager.onAttach(mAdapter); - mPagerLayoutManager.setCurrentIndex(mIndex); - mAdapter = null; - mLayoutManager = mPagerLayoutManager; - break; - case LAYOUT_TOP_TO_BOTTOM: - ensureScrollLayoutManager(); - mScrollLayoutManager.onAttach(mAdapter); - mScrollLayoutManager.setCurrentIndex(mIndex); - mAdapter = null; - mLayoutManager = mScrollLayoutManager; - break; - } - - requestFill(); - } - - private void detachLayoutManager() { - if (null == mLayoutManager) { - return; - } - - mIndex = mLayoutManager.getInternalCurrentIndex(); - mAdapter = mLayoutManager.onDetach(); - mLayoutManager = null; - } - - @SuppressWarnings("deprecation") - private void onAttachToRootInternal() { - if (null == mPageTextTexture) { - mPageTextTexture = ImageMovableTextTexture.create(mPageTextTypeface, - mPageTextSize, mPageTextColor, - new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}); - } - attachLayoutManager(); - } - - private void setPagerIntervalInternal(int interval) { - mPagerInterval = interval; - if (mPagerLayoutManager != null) { - mPagerLayoutManager.setInterval(interval); - } - } - - private void setScrollIntervalInternal(int interval) { - mScrollInterval = interval; - if (mScrollLayoutManager != null) { - mScrollLayoutManager.setInterval(interval); - } - } - - @Override - public void onAttachToRoot(GLRoot root) { - super.onAttachToRoot(root); - postMethod(METHOD_ON_ATTACH_TO_ROOT); - } - - @Override - public void onDetachFromRoot() { - // When detached, render() will not be called. So do it here - detachLayoutManager(); - if (null != mPageTextTexture) { - mPageTextTexture.recycle(); - mPageTextTexture = null; - } - - super.onDetachFromRoot(); - } - - public int getLayoutMode() { - return mLayoutMode; - } - - public void setLayoutMode(@LayoutMode int layoutMode) { - postMethod(METHOD_SET_LAYOUT_MODE, layoutMode); - } - - public int getCurrentIndex() { - return mCurrentIndex.get(); - } - - @Override - public void requestLayout() { - // Do not need requestLayout, because the size will not change - requestFill(); - } - - public void requestFill() { - if (mEnableRequestFill) { - mRequestFill = true; - if (!mWillFill) { - invalidate(); - } - } - } - - @Override - protected boolean dispatchTouchEvent(MotionEvent event) { - // Do not pass event to component, so handle event here - mGestureRecognizer.onTouchEvent(event); - return true; - } - - String getDefaultErrorStr() { - return mDefaultErrorString; - } - - String getEmptyStr() { - return mEmptyString; - } - - boolean isFirstScroll() { - boolean firstScroll = mFirstScroll; - mFirstScroll = false; - return firstScroll; - } - - // Make sure method run in render thread to ensure thread safe - private void postMethod(int method, Object... args) { - synchronized (this) { - mMethodList.add(method); - mArgsList.add(args); - } - - invalidate(); - } - - public void setCurrentPage(int page) { - postMethod(METHOD_SET_CURRENT_PAGE, page); - } - - public void pageLeft() { - postMethod(METHOD_PAGE_LEFT); - } - - public void pageRight() { - postMethod(METHOD_PAGE_RIGHT); - } - - public void setScaleMode(int scaleMode) { - postMethod(METHOD_SET_SCALE_MODE, scaleMode); - } - - public void setStartPosition(int startPosition) { - postMethod(METHOD_SET_START_POSITION, startPosition); - } - - public void setPagerInterval(int interval) { - postMethod(METHOD_SET_PAGER_INTERVAL, interval); - } - - public void setScrollInterval(int interval) { - postMethod(METHOD_SET_SCROLL_INTERVAL, interval); - } - - @Override - public boolean onSingleTapUp(float x, float y) { - postMethod(METHOD_ON_SINGLE_TAP_UP, x, y); - return true; - } - - @Override - public boolean onSingleTapConfirmed(float x, float y) { - postMethod(METHOD_ON_SINGLE_TAP_CONFIRMED, x, y); - return true; - } - - @Override - public boolean onDoubleTap(float x, float y) { - postMethod(METHOD_ON_DOUBLE_TAP, x, y); - return true; - } - - @Override - public boolean onDoubleTapConfirmed(float x, float y) { - postMethod(METHOD_ON_DOUBLE_TAP_CONFIRMED, x, y); - return true; - } - - @Override - public void onLongPress(float x, float y) { - if (mLayoutManager != null && !mLayoutManager.isTapOrPressEnable()) { - return; - } - - postMethod(METHOD_ON_LONG_PRESS, x, y); - } - - @Override - public boolean onScroll(float dx, float dy, float totalX, float totalY, float x, float y) { - postMethod(METHOD_ON_SCROLL, dx, dy, totalX, totalY, x, y); - return true; - } - - @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { - postMethod(METHOD_ON_FLING, velocityX, velocityY); - return true; - } - - @Override - public boolean onScaleBegin(float focusX, float focusY) { - postMethod(METHOD_ON_SCALE_BEGIN, focusX, focusY); - return true; - } - - @Override - public boolean onScale(float focusX, float focusY, float scale) { - postMethod(METHOD_ON_SCALE, focusX, focusY, scale); - return true; - } - - @Override - public void onScaleEnd() { - postMethod(METHOD_ON_SCALE_END); - } - - @Override - public void onDown(float x, float y) { - postMethod(METHOD_ON_DOWN, x, y); - } - - @Override - public void onUp() { - postMethod(METHOD_ON_UP); - } - - @Override - public void onPointerDown(float x, float y) { - postMethod(METHOD_ON_POINTER_DOWN, x, y); - } - - @Override - public void onPointerUp() { - postMethod(METHOD_ON_POINTER_UP); - } - - @Override - protected void onLayout(boolean changeSize, int left, int top, int right, int bottom) { - fill(); - if (changeSize) { - int width = right - left; - int height = bottom - top; - mLeftArea.set((int) (LEFT_AREA[0] * width), (int) (LEFT_AREA[1] * height), - (int) (LEFT_AREA[2] * width), (int) (LEFT_AREA[3] * height)); - mRightArea.set((int) (RIGHT_AREA[0] * width), (int) (RIGHT_AREA[1] * height), - (int) (RIGHT_AREA[2] * width), (int) (RIGHT_AREA[3] * height)); - mMenuArea.set((int) (MENU_AREA[0] * width), (int) (MENU_AREA[1] * height), - (int) (MENU_AREA[2] * width), (int) (MENU_AREA[3] * height)); - mSliderArea.set((int) (SLIDER_AREA[0] * width), (int) (SLIDER_AREA[1] * height), - (int) (SLIDER_AREA[2] * width), (int) (SLIDER_AREA[3] * height)); - } - } - - @RenderThread - public void onDataChanged() { - GalleryUtils.assertInRenderThread(); - - if (mLayoutManager != null) { - mLayoutManager.onDataChanged(); - } - } - - private void onSingleTapUpInternal(float x, float y) { - } - - private GalleryPageView findPageUnder(float x, float y) { - for (int i = 0, n = getComponentCount(); i < n; i++) { - GLView view = getComponent(i); - if (view instanceof GalleryPageView && view.bounds().contains((int) x, (int) y)) { - return (GalleryPageView) view; - } - } - return null; - } - - private void onSingleTapConfirmedInternal(float x, float y) { - if (mLayoutManager == null || !mLayoutManager.isTapOrPressEnable()) { - return; - } - - GalleryPageView page = findPageUnder(x, y); - if (page != null && - page.getIndex() != GalleryPageView.INVALID_INDEX && - page.isError() && - page.isUnderInfo(x - page.bounds().left, y - page.bounds().top)) { - if (mListener != null) { - mListener.onTapErrorText(page.getIndex()); - } - } else if (mSliderArea.contains((int) x, (int) y)) { - if (mListener != null) { - mListener.onTapSliderArea(); - } - } else if (mMenuArea.contains((int) x, (int) y)) { - if (mListener != null) { - mListener.onTapMenuArea(); - } - } else if (mLeftArea.contains((int) x, (int) y)) { - mLayoutManager.onPageLeft(); - } else if (mRightArea.contains((int) x, (int) y)) { - mLayoutManager.onPageRight(); - } - } - - private void onDoubleTapInternal(float x, float y) { - } - - private void onDoubleTapConfirmedInternal(float x, float y) { - if (mScale) { - return; - } - - if (mLayoutManager != null) { - mLayoutManager.onDoubleTapConfirmed(x, y); - } - } - - private void onLongPressInternal(float x, float y) { - if (mScale) { - return; - } - - if (mLayoutManager == null) { - return; - } - - int index = mLayoutManager.getIndexUnder(x, y); - if (index == GalleryPageView.INVALID_INDEX) { - return; - } - - if (mListener != null) { - mListener.onLongPressPage(index); - } - } - - private void onScrollInternal(float dx, float dy, float totalX, float totalY, float x, float y) { - if (mScale) { - return; - } - mScroll = true; - - if (mLayoutManager != null) { - mLayoutManager.onScroll(dx, dy, totalX, totalY, x, y); - } - } - - private void onFlingInternal(float velocityX, float velocityY) { - if (mLayoutManager != null) { - mLayoutManager.onFling(velocityX, velocityY); - } - } - - private void onScaleBeginInternal(float focusX, float focusY) { - onScaleInternal(focusX, focusY, 1.0f); - } - - private void onScaleInternal(float focusX, float focusY, float scale) { - if (mScroll || (mLayoutManager != null && !mLayoutManager.canScale())) { - return; - } - mScale = true; - - if (mLayoutManager != null) { - mLayoutManager.onScale(focusX, focusY, scale); - } - } - - private void onScaleEndInternal() { - } - - private void onDownInternal(float x, float y) { - mScale = false; - mScroll = false; - mFirstScroll = true; - if (mLayoutManager != null) { - mLayoutManager.onDown(); - } - } - - private void onUpInternal() { - if (mLayoutManager != null) { - mLayoutManager.onUp(); - } - } - - private void onPointerDownInternal(float x, float y) { - if (!mScroll && (mLayoutManager != null && mLayoutManager.canScale())) { - mScale = true; - } - } - - private void onPointerUpInternal() { - } - - private void setLayoutModeInternal(int layoutMode) { - if (mLayoutMode == layoutMode) { - return; - } - mLayoutMode = layoutMode; - - if (mLayoutManager == null) { - return; - } - - switch (mLayoutMode) { - case LAYOUT_LEFT_TO_RIGHT: - if (mLayoutManager == mPagerLayoutManager) { - // mPagerLayoutManager already attached, just change mode - mPagerLayoutManager.setMode(PagerLayoutManager.MODE_LEFT_TO_RIGHT); - } else { - ensurePagerLayoutManager(); - mPagerLayoutManager.setMode(PagerLayoutManager.MODE_LEFT_TO_RIGHT); - int index = mLayoutManager.getInternalCurrentIndex(); - mPagerLayoutManager.onAttach(mLayoutManager.onDetach()); - mPagerLayoutManager.setCurrentIndex(index); - mLayoutManager = mPagerLayoutManager; - } - break; - case LAYOUT_RIGHT_TO_LEFT: - if (mLayoutManager == mPagerLayoutManager) { - // mPagerLayoutManager already attached, just change mode - mPagerLayoutManager.setMode(PagerLayoutManager.MODE_RIGHT_TO_LEFT); - } else { - ensurePagerLayoutManager(); - mPagerLayoutManager.setMode(PagerLayoutManager.MODE_RIGHT_TO_LEFT); - int index = mLayoutManager.getInternalCurrentIndex(); - mPagerLayoutManager.onAttach(mLayoutManager.onDetach()); - mPagerLayoutManager.setCurrentIndex(index); - mLayoutManager = mPagerLayoutManager; - } - break; - case LAYOUT_TOP_TO_BOTTOM: - ensureScrollLayoutManager(); - int index = mLayoutManager.getInternalCurrentIndex(); - mScrollLayoutManager.onAttach(mLayoutManager.onDetach()); - mScrollLayoutManager.setCurrentIndex(index); - mLayoutManager = mScrollLayoutManager; - break; - } - - requestFill(); - } - - private void setCurrentPageInternal(int page) { - if (mLayoutManager != null) { - mLayoutManager.setCurrentIndex(page); - } else { - mIndex = page; - } - } - - private void pageLeftInternal() { - if (mLayoutManager != null) { - mLayoutManager.onPageLeft(); - } - } - - private void pageRightInternal() { - if (mLayoutManager != null) { - mLayoutManager.onPageRight(); - } - } - - private void setScaleModeInternal(int scaleMode) { - mScaleMode = scaleMode; - if (mPagerLayoutManager != null) { - mPagerLayoutManager.setScaleMode(scaleMode); - } - } - - private void setStartPositionInternal(int startPosition) { - mStartPosition = startPosition; - if (mPagerLayoutManager != null) { - mPagerLayoutManager.setStartPosition(startPosition); - } - } - - @RenderThread - void forceFill() { - mRequestFill = true; - fill(); - } - - @RenderThread - private void fill() { - GalleryUtils.assertInRenderThread(); - - if (!mRequestFill) { - return; - } - - // Disable request layout - mEnableRequestFill = false; - if (mLayoutManager != null) { - mLayoutManager.onFill(); - } - mEnableRequestFill = true; - mRequestFill = false; - } - - private void dispatchMethod() { - List methodListTemp = mMethodListTemp; - List argsListTemp = mArgsListTemp; - - synchronized (this) { - if (mMethodList.isEmpty()) { - return; - } - - methodListTemp.addAll(mMethodList); - argsListTemp.addAll(mArgsList); - mMethodList.clear(); - mArgsList.clear(); - } - - for (int i = 0, n = methodListTemp.size(); i < n; i++) { - int method = methodListTemp.get(i); - Object[] args = argsListTemp.get(i); - - switch (method) { - case METHOD_ON_SINGLE_TAP_UP: - onSingleTapUpInternal((Float) args[0], (Float) args[1]); - break; - case METHOD_ON_SINGLE_TAP_CONFIRMED: - onSingleTapConfirmedInternal((Float) args[0], (Float) args[1]); - break; - case METHOD_ON_DOUBLE_TAP: - onDoubleTapInternal((Float) args[0], (Float) args[1]); - break; - case METHOD_ON_DOUBLE_TAP_CONFIRMED: - onDoubleTapConfirmedInternal((Float) args[0], (Float) args[1]); - break; - case METHOD_ON_LONG_PRESS: - onLongPressInternal((Float) args[0], (Float) args[1]); - break; - case METHOD_ON_SCROLL: - onScrollInternal((Float) args[0], (Float) args[1], (Float) args[2], - (Float) args[3], (Float) args[4], (Float) args[5]); - break; - case METHOD_ON_FLING: - onFlingInternal((Float) args[0], (Float) args[1]); - break; - case METHOD_ON_SCALE_BEGIN: - onScaleBeginInternal((Float) args[0], (Float) args[1]); - break; - case METHOD_ON_SCALE: - onScaleInternal((Float) args[0], (Float) args[1], (Float) args[2]); - break; - case METHOD_ON_SCALE_END: - onScaleEndInternal(); - break; - case METHOD_ON_DOWN: - onDownInternal((Float) args[0], (Float) args[1]); - break; - case METHOD_ON_UP: - onUpInternal(); - break; - case METHOD_ON_POINTER_DOWN: - onPointerDownInternal((Float) args[0], (Float) args[1]); - break; - case METHOD_ON_POINTER_UP: - onPointerUpInternal(); - break; - case METHOD_SET_LAYOUT_MODE: - setLayoutModeInternal((Integer) args[0]); - break; - case METHOD_SET_CURRENT_PAGE: - setCurrentPageInternal((Integer) args[0]); - break; - case METHOD_PAGE_LEFT: - pageLeftInternal(); - break; - case METHOD_PAGE_RIGHT: - pageRightInternal(); - break; - case METHOD_SET_SCALE_MODE: - setScaleModeInternal((Integer) args[0]); - break; - case METHOD_SET_START_POSITION: - setStartPositionInternal((Integer) args[0]); - break; - case METHOD_ON_ATTACH_TO_ROOT: - onAttachToRootInternal(); - break; - case METHOD_SET_PAGER_INTERVAL: - setPagerIntervalInternal((Integer) args[0]); - break; - case METHOD_SET_SCROLL_INTERVAL: - setScrollIntervalInternal((Integer) args[0]); - break; - } - } - - methodListTemp.clear(); - argsListTemp.clear(); - } - - @Override - public void render(GLCanvas canvas) { - mWillFill = true; - int oldCurrentIndex = mCurrentIndex.get(); - - // Dispatch method - dispatchMethod(); - - long time = AnimationTime.get(); - if (mLayoutManager != null && mLayoutManager.onUpdateAnimation(time)) { - invalidate(); - } - - fill(); - mWillFill = false; - - super.render(canvas); - - int newCurrentIndex; - if (mLayoutManager != null) { - newCurrentIndex = mLayoutManager.getCurrentIndex(); - } else { - newCurrentIndex = GalleryPageView.INVALID_INDEX; - } - mCurrentIndex.lazySet(newCurrentIndex); - - if (oldCurrentIndex != newCurrentIndex && mListener != null) { - mListener.onUpdateCurrentIndex(newCurrentIndex); - } - } - - @RenderThread - public GalleryPageView findPageByIndex(int id) { - if (mLayoutManager != null) { - return mLayoutManager.findPageByIndex(id); - } else { - return null; - } - } - - GalleryPageView obtainPage() { - GalleryPageView page = mGalleryPageViewPool.pop(); - if (page == null) { - page = new GalleryPageView(mPageTextTexture, - mProgressColor, mBackgroundColor, mProgressSize, - mPageMinHeight, mPageInfoInterval); - } - return page; - } - - void releasePage(GalleryPageView page) { - mGalleryPageViewPool.push(page); - } - - /** - * Indeterminate GLProgressView - */ - GLProgressView obtainProgress() { - GLProgressView progress; - if (mProgressCache != null) { - progress = mProgressCache; - mProgressCache = null; - } else { - progress = new GLProgressView(); - progress.setColor(mProgressColor); - progress.setBgColor(mBackgroundColor); - progress.setIndeterminate(true); - progress.setMinimumWidth(mProgressSize); - progress.setMinimumHeight(mProgressSize); - } - return progress; - } - - /** - * @param progress Indeterminate GLProgressView - */ - void releaseProgress(GLProgressView progress) { - mProgressCache = progress; - } - - GLTextureView obtainErrorView() { - GLTextureView errorView; - if (mErrorViewCache != null) { - errorView = mErrorViewCache; - mErrorViewCache = null; - } else { - errorView = new GLTextureView(); - } - return errorView; - } - - void unbindErrorView(GLTextureView errorView) { - Texture texture = errorView.getTexture(); - if (texture != null) { - errorView.setTexture(null); - if (texture instanceof BasicTexture) { - ((BasicTexture) texture).recycle(); - } - } - } - - void bindErrorView(GLTextureView errorView, String error) { - unbindErrorView(errorView); - - Texture texture = StringTexture.newInstance(error, mErrorTextSize, mErrorTextColor); - errorView.setTexture(texture); - } - - void releaseErrorView(GLTextureView errorView) { - unbindErrorView(errorView); - mErrorViewCache = errorView; - } - - @IntDef({LAYOUT_LEFT_TO_RIGHT, LAYOUT_RIGHT_TO_LEFT, LAYOUT_TOP_TO_BOTTOM}) - @Retention(RetentionPolicy.SOURCE) - public @interface LayoutMode { - } - - @IntDef({SCALE_ORIGIN, SCALE_FIT_WIDTH, SCALE_FIT_HEIGHT, SCALE_FIT, SCALE_FIXED}) - @Retention(RetentionPolicy.SOURCE) - public @interface ScaleMode { - } - - @IntDef({START_POSITION_TOP_LEFT, START_POSITION_TOP_RIGHT, START_POSITION_BOTTOM_LEFT, - START_POSITION_BOTTOM_RIGHT, START_POSITION_CENTER}) - @Retention(RetentionPolicy.SOURCE) - public @interface StartPosition { - } - - public interface Listener { - - @RenderThread - void onUpdateCurrentIndex(int index); - - @RenderThread - void onTapSliderArea(); - - @RenderThread - void onTapMenuArea(); - - @RenderThread - void onTapErrorText(int index); - - @RenderThread - void onLongPressPage(int index); - } - - public static class Builder { - - private final Context mContext; - private final Adapter mAdapter; - private final int mEdgeColor = Color.WHITE; - private Listener mListener; - private int mLayoutMode = LAYOUT_LEFT_TO_RIGHT; - private int mScaleMode = SCALE_FIT; - private int mStartPosition = START_POSITION_TOP_LEFT; - private int mStartPage = 0; - private int mBackgroundColor = Color.BLACK; - private int mPagerInterval = 48; - private int mScrollInterval = 24; - private int mPageMinHeight = 256; - private int mPageInfoInterval = 24; - private int mProgressColor = Color.WHITE; - private int mProgressSize = 56; - private int mPageTextColor = Color.WHITE; - private int mPageTextSize = 56; - private Typeface mPageTextTypeface = Typeface.DEFAULT; - private int mErrorTextColor = Color.RED; - private int mErrorTextSize = 24; - private String mDefaultErrorString = "Error"; - private String mEmptyString = "Empty"; - - public Builder(@NonNull Context context, @NonNull Adapter adapter) { - mContext = context; - mAdapter = adapter; - } - - public Builder setListener(Listener listener) { - mListener = listener; - return this; - } - - public Builder setLayoutMode(@LayoutMode int layoutMode) { - mLayoutMode = layoutMode; - return this; - } - - public Builder setScaleMode(@ScaleMode int scaleMode) { - mScaleMode = scaleMode; - return this; - } - - public Builder setStartPosition(@StartPosition int startPosition) { - mStartPosition = startPosition; - return this; - } - - public Builder setStartPage(int startPage) { - mStartPage = startPage; - return this; - } - - public Builder setBackgroundColor(int backgroundColor) { - mBackgroundColor = backgroundColor; - return this; - } - - public Builder setPagerInterval(int pagerInterval) { - mPagerInterval = pagerInterval; - return this; - } - - public Builder setScrollInterval(int scrollInterval) { - mScrollInterval = scrollInterval; - return this; - } - - public Builder setPageMinHeight(int pageMinHeight) { - mPageMinHeight = pageMinHeight; - return this; - } - - public Builder setPageInfoInterval(int pageInfoInterval) { - mPageInfoInterval = pageInfoInterval; - return this; - } - - public Builder setProgressColor(int progressColor) { - mProgressColor = progressColor; - return this; - } - - public Builder setProgressSize(int progressSize) { - mProgressSize = progressSize; - return this; - } - - public Builder setPageTextColor(int pageTextColor) { - mPageTextColor = pageTextColor; - return this; - } - - public Builder setPageTextSize(int pageTextSize) { - mPageTextSize = pageTextSize; - return this; - } - - public Builder setPageTextTypeface(Typeface pageTextTypeface) { - mPageTextTypeface = pageTextTypeface; - return this; - } - - public Builder setErrorTextColor(int errorTextColor) { - mErrorTextColor = errorTextColor; - return this; - } - - public Builder setErrorTextSize(int errorTextSize) { - mErrorTextSize = errorTextSize; - return this; - } - - public Builder setDefaultErrorString(String defaultErrorString) { - mDefaultErrorString = defaultErrorString; - return this; - } - - public Builder setEmptyString(String emptyString) { - mEmptyString = emptyString; - return this; - } - - public GalleryView build() { - return new GalleryView(this); - } - } - - public static abstract class Adapter { - - protected GalleryView mGalleryView; - - private void setGalleryView(@NonNull GalleryView galleryView) { - mGalleryView = galleryView; - } - - public void bind(GalleryPageView view, int index) { - onBind(view, index); - view.setIndex(index); - } - - public void unbind(GalleryPageView view) { - onUnbind(view, view.getIndex()); - view.setIndex(GalleryPageView.INVALID_INDEX); - } - - public abstract void onBind(GalleryPageView view, int index); - - public abstract void onUnbind(GalleryPageView view, int index); - - /** - * @return Null for no error - */ - public abstract String getError(); - - public abstract int size(); - } - - public static abstract class LayoutManager { - - protected GalleryView mGalleryView; - - public LayoutManager(@NonNull GalleryView galleryView) { - mGalleryView = galleryView; - } - - public abstract void onAttach(Adapter iterator); - - public abstract Adapter onDetach(); - - public abstract void onFill(); - - public abstract void onDown(); - - public abstract void onUp(); - - public abstract void onDoubleTapConfirmed(float x, float y); - - public abstract void onLongPress(float x, float y); - - public abstract void onScroll(float dx, float dy, float totalX, float totalY, float x, float y); - - public abstract void onFling(float velocityX, float velocityY); - - public abstract boolean canScale(); - - public abstract void onScale(float focusX, float focusY, float scale); - - public abstract boolean onUpdateAnimation(long time); - - public abstract void onDataChanged(); - - public abstract void onPageLeft(); - - public abstract void onPageRight(); - - public abstract boolean isTapOrPressEnable(); - - public abstract GalleryPageView findPageByIndex(int index); - - /** - * @return {@link GalleryPageView#INVALID_INDEX} for error - */ - public abstract int getCurrentIndex(); - - public abstract void setCurrentIndex(int index); - - public abstract int getIndexUnder(float x, float y); - - abstract int getInternalCurrentIndex(); - - protected void placeCenter(GLView view) { - int spec = GLView.MeasureSpec.makeMeasureSpec(GLView.LayoutParams.WRAP_CONTENT, - GLView.LayoutParams.WRAP_CONTENT); - view.measure(spec, spec); - int viewWidth = view.getMeasuredWidth(); - int viewHeight = view.getMeasuredHeight(); - int viewLeft = mGalleryView.getWidth() / 2 - viewWidth / 2; - int viewTop = mGalleryView.getHeight() / 2 - viewHeight / 2; - view.layout(viewLeft, viewTop, viewLeft + viewWidth, viewTop + viewHeight); - } - } -} diff --git a/app/src/main/java/com/hippo/glgallery/GestureRecognizer.java b/app/src/main/java/com/hippo/glgallery/GestureRecognizer.java deleted file mode 100644 index 00d74a2cb..000000000 --- a/app/src/main/java/com/hippo/glgallery/GestureRecognizer.java +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glgallery; - -import android.content.Context; -import android.os.SystemClock; -import android.view.GestureDetector; -import android.view.MotionEvent; -import android.view.ScaleGestureDetector; - -import androidx.core.view.GestureDetectorCompat; - -// This class aggregates three gesture detectors: GestureDetector, -// ScaleGestureDetector, and DownUpDetector. -class GestureRecognizer { - @SuppressWarnings("unused") - private static final String TAG = "GestureRecognizer"; - private final GestureDetectorCompat mGestureDetector; - private final ScaleGestureDetector mScaleDetector; - private final DownUpDetector mDownUpDetector; - private final Listener mListener; - - public GestureRecognizer(Context context, Listener listener) { - mListener = listener; - MyGestureListener gestureListener = new MyGestureListener(); - mGestureDetector = new GestureDetectorCompat(context, gestureListener, - null /* ignoreMultitouch */); - mGestureDetector.setOnDoubleTapListener(gestureListener); - mScaleDetector = new ScaleGestureDetector( - context, new MyScaleListener()); - mDownUpDetector = new DownUpDetector(new MyDownUpListener()); - } - - public void onTouchEvent(MotionEvent event) { - mScaleDetector.onTouchEvent(event); - mGestureDetector.onTouchEvent(event); - mDownUpDetector.onTouchEvent(event); - } - - public boolean isDown() { - return mDownUpDetector.isDown(); - } - - public void cancelScale() { - long now = SystemClock.uptimeMillis(); - MotionEvent cancelEvent = MotionEvent.obtain( - now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0); - mScaleDetector.onTouchEvent(cancelEvent); - cancelEvent.recycle(); - } - - public interface Listener { - boolean onSingleTapUp(float x, float y); - - boolean onSingleTapConfirmed(float x, float y); - - boolean onDoubleTap(float x, float y); - - boolean onDoubleTapConfirmed(float x, float y); - - void onLongPress(float x, float y); - - boolean onScroll(float dx, float dy, float totalX, float totalY, float x, float y); - - /** - * @param velocityX Finger from top to bottom is positive - * @param velocityY Finger from left to right is positive - */ - boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY); - - boolean onScaleBegin(float focusX, float focusY); - - boolean onScale(float focusX, float focusY, float scale); - - void onScaleEnd(); - - void onDown(float x, float y); - - void onUp(); - - void onPointerDown(float x, float y); - - void onPointerUp(); - } - - private class MyGestureListener - extends GestureDetector.SimpleOnGestureListener { - @Override - public boolean onSingleTapUp(MotionEvent e) { - return mListener.onSingleTapUp(e.getX(), e.getY()); - } - - @Override - public boolean onSingleTapConfirmed(MotionEvent e) { - return mListener.onSingleTapConfirmed(e.getX(), e.getY()); - } - - @Override - public boolean onDoubleTapEvent(MotionEvent e) { - if (e.getAction() == MotionEvent.ACTION_UP) { - return mListener.onDoubleTapConfirmed(e.getX(), e.getY()); - } else { - return true; - } - } - - @Override - public boolean onDoubleTap(MotionEvent e) { - return mListener.onDoubleTap(e.getX(), e.getY()); - } - - @Override - public void onLongPress(MotionEvent e) { - mListener.onLongPress(e.getX(), e.getY()); - } - - @Override - public boolean onScroll( - MotionEvent e1, MotionEvent e2, float dx, float dy) { - return mListener.onScroll( - dx, dy, e2.getX() - e1.getX(), e2.getY() - e1.getY(), e2.getX(), e2.getY()); - } - - @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, - float velocityY) { - return mListener.onFling(e1, e2, velocityX, velocityY); - } - } - - private class MyScaleListener - extends ScaleGestureDetector.SimpleOnScaleGestureListener { - @Override - public boolean onScaleBegin(ScaleGestureDetector detector) { - return mListener.onScaleBegin( - detector.getFocusX(), detector.getFocusY()); - } - - @Override - public boolean onScale(ScaleGestureDetector detector) { - return mListener.onScale(detector.getFocusX(), - detector.getFocusY(), detector.getScaleFactor()); - } - - @Override - public void onScaleEnd(ScaleGestureDetector detector) { - mListener.onScaleEnd(); - } - } - - private class MyDownUpListener implements DownUpDetector.DownUpListener { - @Override - public void onDown(MotionEvent e) { - mListener.onDown(e.getX(), e.getY()); - } - - @Override - public void onUp(MotionEvent e) { - mListener.onUp(); - } - - @Override - public void onPointerDown(MotionEvent e) { - mListener.onPointerDown(e.getX(), e.getY()); - } - - @Override - public void onPointerUp(MotionEvent e) { - mListener.onPointerUp(); - } - } -} diff --git a/app/src/main/java/com/hippo/glgallery/ImageView.java b/app/src/main/java/com/hippo/glgallery/ImageView.java deleted file mode 100644 index 91a1863cc..000000000 --- a/app/src/main/java/com/hippo/glgallery/ImageView.java +++ /dev/null @@ -1,597 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glgallery; - -import android.graphics.Rect; -import android.graphics.RectF; - -import com.hippo.glview.anim.AlphaAnimation; -import com.hippo.glview.glrenderer.GLCanvas; -import com.hippo.glview.glrenderer.Texture; -import com.hippo.glview.image.ImageTexture; -import com.hippo.glview.view.GLView; -import com.hippo.yorozuya.AnimationUtils; -import com.hippo.yorozuya.MathUtils; - -import java.util.Arrays; - -class ImageView extends GLView implements ImageTexture.Callback { - - public static final int SCALE_ORIGIN = 0; - public static final int SCALE_FIT_WIDTH = 1; - public static final int SCALE_FIT_HEIGHT = 2; - public static final int SCALE_FIT = 3; - public static final int SCALE_FIXED = 4; - public static final int START_POSITION_TOP_LEFT = 0; - public static final int START_POSITION_TOP_RIGHT = 1; - public static final int START_POSITION_BOTTOM_LEFT = 2; - public static final int START_POSITION_BOTTOM_RIGHT = 3; - public static final int START_POSITION_CENTER = 4; - // TODO adjust scale max and min according to image size and screen size - private static final float SCALE_MIN = 1 / 10.0f; - private static final float SCALE_MAX = 10.0f; - private static final long ALPHA_ANIMATION_DURING = 300L; - private final RectF mDst = new RectF(); - private final RectF mSrcActual = new RectF(); - private final RectF mDstActual = new RectF(); - private final Rect mValidRect = new Rect(); - private final AlphaAnimation mAlphaAnimation; - private ImageTexture mImageTexture; - private int mTextureWidth; - private int mTextureHeight; - private int mScaleMode = SCALE_FIT; - private int mStartPosition = START_POSITION_TOP_RIGHT; - private float mScaleValue = 1.0f; - private float mScale = 1.0f; - private boolean mScaleOffsetDirty = true; - private boolean mPositionInRootDirty = true; - - public ImageView() { - mAlphaAnimation = new AlphaAnimation(0.0f, 1.0f); - mAlphaAnimation.setDuration(ALPHA_ANIMATION_DURING); - mAlphaAnimation.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR); - } - - public static int sanitizeScaleMode(int scaleMode) { - if (scaleMode != SCALE_ORIGIN && - scaleMode != SCALE_FIT_WIDTH && - scaleMode != SCALE_FIT_HEIGHT && - scaleMode != SCALE_FIT && - scaleMode != SCALE_FIXED) { - return SCALE_FIT; - } else { - return scaleMode; - } - } - - public static int sanitizeStartPosition(int startPosition) { - if (startPosition != START_POSITION_TOP_LEFT && - startPosition != START_POSITION_TOP_RIGHT && - startPosition != START_POSITION_BOTTOM_LEFT && - startPosition != START_POSITION_BOTTOM_RIGHT && - startPosition != START_POSITION_CENTER) { - return START_POSITION_TOP_RIGHT; - } else { - return startPosition; - } - } - - @Override - protected int getSuggestedMinimumWidth() { - return Math.max(super.getSuggestedMinimumWidth(), - mImageTexture == null ? 0 : mTextureWidth); - } - - @Override - protected int getSuggestedMinimumHeight() { - return Math.max(super.getSuggestedMinimumHeight(), - mImageTexture == null ? 0 : mTextureHeight); - } - - @Override - protected void onMeasure(int widthSpec, int heightSpec) { - if (mImageTexture == null) { - super.onMeasure(widthSpec, heightSpec); - } else { - float ratio = (float) mTextureWidth / mTextureHeight; - int widthSize = MeasureSpec.getSize(widthSpec); - int heightSize = MeasureSpec.getSize(heightSpec); - int widthMode = MeasureSpec.getMode(widthSpec); - int heightMode = MeasureSpec.getMode(heightSpec); - int measureWidth = -1; - int measureHeight = -1; - - if (widthMode == MeasureSpec.EXACTLY) { - measureWidth = widthSize; - if (heightMode == MeasureSpec.EXACTLY) { - measureHeight = heightSize; - } else { - measureHeight = (int) (widthSize / ratio); - if (heightMode == MeasureSpec.AT_MOST) { - measureHeight = Math.min(measureHeight, heightSize); - } - } - } else if (heightMode == MeasureSpec.EXACTLY) { - measureHeight = heightSize; - measureWidth = (int) (heightSize * ratio); - if (widthMode == MeasureSpec.AT_MOST) { - measureWidth = Math.min(measureWidth, widthSize); - } - } - - if (measureWidth == -1 || measureHeight == -1) { - super.onMeasure(widthSpec, heightSpec); - } else { - setMeasuredSize(measureWidth, measureHeight); - } - } - } - - @Override - protected void onSizeChanged(int newW, int newH, int oldW, int oldH) { - mScaleOffsetDirty = true; - mPositionInRootDirty = true; - } - - @Override - protected void onPositionInRootChanged(int x, int y, int oldX, int oldY) { - mPositionInRootDirty = true; - - if (mImageTexture != null) { - getValidRect(mValidRect); - if (!mValidRect.isEmpty()) { - mImageTexture.start(); - } else { - mImageTexture.stop(); - } - } - } - - public void getScaleDefault(float[] scaleDefault) { - if (mImageTexture == null) { - return; - } - - scaleDefault[0] = 1.0f; - scaleDefault[1] = (float) getWidth() / mTextureWidth; - scaleDefault[2] = (float) getHeight() / mTextureHeight; - scaleDefault[3] = Math.max(scaleDefault[1], scaleDefault[2]) * 2; - - scaleDefault[0] = MathUtils.clamp(scaleDefault[0], SCALE_MIN, SCALE_MAX); - scaleDefault[1] = MathUtils.clamp(scaleDefault[1], SCALE_MIN, SCALE_MAX); - scaleDefault[2] = MathUtils.clamp(scaleDefault[2], SCALE_MIN, SCALE_MAX); - scaleDefault[3] = MathUtils.clamp(scaleDefault[3], SCALE_MIN, SCALE_MAX); - - Arrays.sort(scaleDefault); - } - - public ImageTexture getImageTexture() { - return mImageTexture; - } - - public void setImageTexture(ImageTexture imageTexture) { - // Remove callback - if (mImageTexture != null) { - mImageTexture.setCallback(null); - mImageTexture.stop(); - } - - int oldTextureWidth = mTextureWidth; - int oldTextureHeight = mTextureHeight; - - mImageTexture = imageTexture; - - if (imageTexture != null) { - imageTexture.setCallback(this); - mTextureWidth = imageTexture.getWidth(); - mTextureHeight = imageTexture.getHeight(); - // Avoid zero and negative - if (mTextureWidth <= 0) { - mTextureWidth = 1; - } - if (mTextureHeight <= 0) { - mTextureHeight = 1; - } - - // Start alpha animation, do not show animation for image has no valid rect - getValidRect(mValidRect); - if (!mValidRect.isEmpty()) { - startAnimation(mAlphaAnimation, true); - mImageTexture.start(); - } - } else { - mTextureWidth = 1; - mTextureHeight = 1; - } - - mScaleOffsetDirty = true; - mPositionInRootDirty = true; - - if (oldTextureWidth != mTextureWidth || oldTextureHeight != mTextureHeight) { - requestLayout(); - } - } - - public boolean isLoaded() { - return mImageTexture != null; - } - - public boolean canFlingVertically() { - if (mScaleOffsetDirty) { - setScaleOffset(mScaleMode, mStartPosition, mScaleValue); - if (mScaleOffsetDirty) { - return false; - } - } - - return mDst.top < 0.0f || mDst.bottom > getHeight(); - } - - public boolean canFlingHorizontally() { - if (mScaleOffsetDirty) { - setScaleOffset(mScaleMode, mStartPosition, mScaleValue); - if (mScaleOffsetDirty) { - return false; - } - } - - return mDst.left < 0.0f || mDst.right > getWidth(); - } - - public boolean canFling() { - if (mScaleOffsetDirty) { - setScaleOffset(mScaleMode, mStartPosition, mScaleValue); - if (mScaleOffsetDirty) { - return false; - } - } - - return mDst.left < 0.0f || mDst.top < 0.0f || mDst.right > getWidth() || mDst.bottom > getHeight(); - } - - public int getMaxDx() { - if (mScaleOffsetDirty) { - setScaleOffset(mScaleMode, mStartPosition, mScaleValue); - if (mScaleOffsetDirty) { - return 0; - } - } - return Math.max(0, -(int) mDst.left); - } - - public int getMinDx() { - if (mScaleOffsetDirty) { - setScaleOffset(mScaleMode, mStartPosition, mScaleValue); - if (mScaleOffsetDirty) { - return 0; - } - } - return Math.min(0, getWidth() - (int) mDst.right); - } - - public int getMaxDy() { - if (mScaleOffsetDirty) { - setScaleOffset(mScaleMode, mStartPosition, mScaleValue); - if (mScaleOffsetDirty) { - return 0; - } - } - return Math.max(0, -(int) mDst.top); - } - - public int getMinDy() { - if (mScaleOffsetDirty) { - setScaleOffset(mScaleMode, mStartPosition, mScaleValue); - if (mScaleOffsetDirty) { - return 0; - } - } - return Math.min(0, getHeight() - (int) mDst.bottom); - } - - public float getScale() { - return mScale; - } - - /** - * If target is shorter then screen, make it in screen center. If target is - * longer then parent, make sure target fill parent over - */ - private void adjustPosition() { - RectF dst = mDst; - int screenWidth = getWidth(); - int screenHeight = getHeight(); - float targetWidth = dst.width(); - float targetHeight = dst.height(); - - if (targetWidth > screenWidth) { - float fixXOffset = dst.left; - if (fixXOffset > 0) { - dst.left -= fixXOffset; - dst.right -= fixXOffset; - } else if ((fixXOffset = screenWidth - dst.right) > 0) { - dst.left += fixXOffset; - dst.right += fixXOffset; - } - } else { - float left = (screenWidth - targetWidth) / 2; - dst.offsetTo(left, dst.top); - } - if (targetHeight > screenHeight) { - float fixYOffset = dst.top; - if (fixYOffset > 0) { - dst.top -= fixYOffset; - dst.bottom -= fixYOffset; - } else if ((fixYOffset = screenHeight - dst.bottom) > 0) { - dst.top += fixYOffset; - dst.bottom += fixYOffset; - } - } else { - float top = (screenHeight - targetHeight) / 2; - dst.offsetTo(dst.left, top); - } - } - - public void setScaleOffset(int scaleMode, int startPosition, float scaleValue) { - mScaleMode = scaleMode; - mStartPosition = startPosition; - mScaleValue = scaleValue; - - int screenWidth = getWidth(); - int screenHeight = getHeight(); - - if (mImageTexture == null || screenWidth == 0 || screenHeight == 0) { - mScaleOffsetDirty = true; - return; - } - - int textureWidth = mTextureWidth; - int textureHeight = mTextureHeight; - - // Set scale - float targetWidth; - float targetHeight; - switch (scaleMode) { - case SCALE_ORIGIN: - mScale = 1.0f; - targetWidth = textureWidth; - targetHeight = textureHeight; - break; - case SCALE_FIT_WIDTH: - mScale = (float) screenWidth / textureWidth; - targetWidth = screenWidth; - targetHeight = textureHeight * mScale; - break; - case SCALE_FIT_HEIGHT: - mScale = (float) screenHeight / textureHeight; - targetWidth = textureWidth * mScale; - targetHeight = screenHeight; - break; - case SCALE_FIT: - float scaleX = (float) screenWidth / textureWidth; - float scaleY = (float) screenHeight / textureHeight; - if (scaleX < scaleY) { - mScale = scaleX; - targetWidth = screenWidth; - targetHeight = textureHeight * scaleX; - } else { - mScale = scaleY; - targetWidth = textureWidth * scaleY; - targetHeight = screenHeight; - break; - } - break; - case SCALE_FIXED: - default: - mScale = scaleValue; - targetWidth = textureWidth * scaleValue; - targetHeight = textureHeight * scaleValue; - break; - } - - // adjust scale, not too big, not too small - if (mScale < SCALE_MIN) { - mScale = SCALE_MIN; - targetWidth = textureWidth * SCALE_MIN; - targetHeight = textureHeight * SCALE_MIN; - } else if (mScale > SCALE_MAX) { - mScale = SCALE_MAX; - targetWidth = textureWidth * SCALE_MAX; - targetHeight = textureHeight * SCALE_MAX; - } - - // Set mDst.left and mDst.right - RectF dst = mDst; - switch (startPosition) { - case START_POSITION_TOP_LEFT: - dst.left = 0; - dst.top = 0; - break; - case START_POSITION_TOP_RIGHT: - dst.left = screenWidth - targetWidth; - dst.top = 0; - break; - case START_POSITION_BOTTOM_LEFT: - dst.left = 0; - dst.top = screenHeight - targetHeight; - break; - case START_POSITION_BOTTOM_RIGHT: - dst.left = screenWidth - targetWidth; - dst.top = screenHeight - targetHeight; - break; - case START_POSITION_CENTER: - default: - dst.left = (screenWidth - targetWidth) / 2; - dst.top = (screenHeight - targetHeight) / 2; - break; - } - - // Set mDst.right and mDst.bottom - dst.right = dst.left + targetWidth; - dst.bottom = dst.top + targetHeight; - - // adjust position - adjustPosition(); - - mScaleOffsetDirty = false; - mPositionInRootDirty = true; - } - - public void scroll(int dx, int dy, int[] remain) { - // Only work after layout - if (mScaleOffsetDirty) { - setScaleOffset(mScaleMode, mStartPosition, mScaleValue); - } - if (mScaleOffsetDirty) { - remain[0] = dx; - remain[1] = dy; - return; - } - - RectF dst = mDst; - int screenWidth = getWidth(); - int screenHeight = getHeight(); - float targetWidth = dst.width(); - float targetHeight = dst.height(); - - if (targetWidth > screenWidth) { - dst.left -= dx; - dst.right -= dx; - - float fixXOffset = dst.left; - if (fixXOffset > 0) { - dst.left -= fixXOffset; - dst.right -= fixXOffset; - remain[0] = -(int) fixXOffset; - } else if ((fixXOffset = screenWidth - dst.right) > 0) { - dst.left += fixXOffset; - dst.right += fixXOffset; - remain[0] = (int) fixXOffset; - } else { - remain[0] = 0; - } - } else { - remain[0] = dx; - } - if (targetHeight > screenHeight) { - dst.top -= dy; - dst.bottom -= dy; - - float fixYOffset = dst.top; - if (fixYOffset > 0) { - dst.top -= fixYOffset; - dst.bottom -= fixYOffset; - remain[1] = -(int) fixYOffset; - } else if ((fixYOffset = screenHeight - dst.bottom) > 0) { - dst.top += fixYOffset; - dst.bottom += fixYOffset; - remain[1] = (int) fixYOffset; - } else { - remain[1] = 0; - } - } else { - remain[1] = dy; - } - - if (dx != remain[0] || dy != remain[1]) { - mPositionInRootDirty = true; - invalidate(); - } - } - - public void scale(float focusX, float focusY, float scale) { - // Only work after layout - if (mScaleOffsetDirty) { - setScaleOffset(mScaleMode, mStartPosition, mScaleValue); - } - if (mScaleOffsetDirty) { - return; - } - - if ((mScale == SCALE_MAX && scale >= 1.0f) || (mScale == SCALE_MIN && scale < 1.0f)) { - return; - } - - float newScale = mScale * scale; - newScale = MathUtils.clamp(newScale, SCALE_MIN, SCALE_MAX); - mScale = newScale; - RectF dst = mDst; - float left = (focusX - ((focusX - dst.left) * scale)); - float top = (focusY - ((focusY - dst.top) * scale)); - dst.set(left, top, - (left + (mImageTexture.getWidth() * newScale)), - (top + (mImageTexture.getHeight() * newScale))); - - // adjust position - adjustPosition(); - - mPositionInRootDirty = true; - invalidate(); - } - - private void applyPositionInRoot() { - int width = mImageTexture.getWidth(); - int height = mImageTexture.getHeight(); - RectF dst = mDst; - RectF dstActual = mDstActual; - RectF srcActual = mSrcActual; - - dstActual.set(dst); - getValidRect(mValidRect); - if (dstActual.intersect(mValidRect.left, mValidRect.top, mValidRect.right, mValidRect.bottom)) { - srcActual.left = MathUtils.lerp(0, width, - MathUtils.delerp(dst.left, dst.right, dstActual.left)); - srcActual.right = MathUtils.lerp(0, width, - MathUtils.delerp(dst.left, dst.right, dstActual.right)); - srcActual.top = MathUtils.lerp(0, height, - MathUtils.delerp(dst.top, dst.bottom, dstActual.top)); - srcActual.bottom = MathUtils.lerp(0, height, - MathUtils.delerp(dst.top, dst.bottom, dstActual.bottom)); - } else { - // Can't be seen, set src and dst empty - srcActual.setEmpty(); - dstActual.setEmpty(); - } - - mPositionInRootDirty = false; - } - - @Override - public void onRender(GLCanvas canvas) { - Texture texture = mImageTexture; - if (texture == null) { - return; - } - - if (mScaleOffsetDirty) { - setScaleOffset(mScaleMode, mStartPosition, mScaleValue); - } - - if (mPositionInRootDirty) { - applyPositionInRoot(); - } - - if (!mSrcActual.isEmpty()) { - texture.draw(canvas, mSrcActual, mDstActual); - } - } - - @Override - public void invalidateImageTexture(ImageTexture who) { - invalidate(); - } -} diff --git a/app/src/main/java/com/hippo/glgallery/PagerLayoutManager.java b/app/src/main/java/com/hippo/glgallery/PagerLayoutManager.java deleted file mode 100644 index fb7313ca0..000000000 --- a/app/src/main/java/com/hippo/glgallery/PagerLayoutManager.java +++ /dev/null @@ -1,963 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glgallery; - -import android.content.Context; -import android.graphics.Rect; -import android.util.Log; -import android.view.animation.Interpolator; - -import androidx.annotation.IntDef; -import androidx.annotation.NonNull; - -import com.hippo.glview.anim.Animation; -import com.hippo.glview.view.GLView; -import com.hippo.glview.widget.GLProgressView; -import com.hippo.glview.widget.GLTextureView; -import com.hippo.yorozuya.AnimationUtils; -import com.hippo.yorozuya.AssertUtils; -import com.hippo.yorozuya.MathUtils; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -class PagerLayoutManager extends GalleryView.LayoutManager { - - public static final int MODE_LEFT_TO_RIGHT = 0; - public static final int MODE_RIGHT_TO_LEFT = 1; - private static final String TAG = PagerLayoutManager.class.getSimpleName(); - private static final Interpolator SMOOTH_SCROLLER_INTERPOLATOR = new Interpolator() { - @Override - public float getInterpolation(float t) { - t -= 1.0f; - return t * t * t * t * t + 1.0f; - } - }; - private final int[] mScrollRemain = new int[2]; - private final float[] mScaleDefault = new float[4]; - private final SmoothScroller mSmoothScroller; - private final PageFling mPageFling; - private final SmoothScaler mSmoothScaler; - private final Rect mTempRect = new Rect(); - private GalleryView.Adapter mAdapter; - private GLProgressView mProgress; - private String mErrorStr; - private GLTextureView mErrorView; - private GalleryPageView mPrevious; - private GalleryPageView mCurrent; - private GalleryPageView mNext; - @Mode - private int mMode = MODE_RIGHT_TO_LEFT; - private int mScaleMode; - private int mStartPosition; - private float mScaleValue; - private int mOffset; - private int mDeltaX; - private int mDeltaY; - private boolean mCanScrollBetweenPages = false; - private boolean mStopAnimationFinger; - private int mInterval; - // Current index - private int mIndex; - - public PagerLayoutManager(Context context, @NonNull GalleryView galleryView, - int scaleMode, int startPoint, float scaleValue, int interval) { - super(galleryView); - - mScaleMode = scaleMode; - mStartPosition = startPoint; - mScaleValue = scaleValue; - - mInterval = interval; - mSmoothScroller = new SmoothScroller(); - mPageFling = new PageFling(context); - mSmoothScaler = new SmoothScaler(); - } - - public void setInterval(int interval) { - if (mInterval == interval) { - return; - } - - if (mAdapter != null) { - int index = getInternalCurrentIndex(); - GalleryView.Adapter adapter = onDetach(); - mInterval = interval; - onAttach(adapter); - setCurrentIndex(index); - mGalleryView.requestFill(); - } else { - mInterval = interval; - } - } - - private void resetParameters() { - mOffset = 0; - mDeltaX = 0; - mDeltaY = 0; - mCanScrollBetweenPages = false; - mStopAnimationFinger = false; - } - - private boolean cancelAllAnimations() { - boolean running = mSmoothScroller.isRunning() || - mPageFling.isRunning() || - mSmoothScaler.isRunning(); - mSmoothScroller.cancel(); - mPageFling.cancel(); - mSmoothScaler.cancel(); - return running; - } - - public void setMode(@Mode int mode) { - if (mMode == mode) { - return; - } - - mMode = mode; - if (mAdapter != null) { - // It is attached, refill - // Cancel all animations - cancelAllAnimations(); - // Remove all view - removeProgress(); - removeErrorView(); - removeAllPages(); - // Reset parameters - resetParameters(); - // Request fill - mGalleryView.requestFill(); - } - } - - public void setScaleMode(int scaleMode) { - if (mScaleMode == scaleMode) { - return; - } - mScaleMode = scaleMode; - - if (mCurrent != null) { - mCurrent.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue); - } - if (mPrevious != null) { - mPrevious.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue); - } - if (mNext != null) { - mNext.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue); - } - } - - public void setStartPosition(int startPosition) { - if (mStartPosition == startPosition) { - return; - } - mStartPosition = startPosition; - - if (mCurrent != null) { - mCurrent.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue); - } - if (mPrevious != null) { - mPrevious.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue); - } - if (mNext != null) { - mNext.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue); - } - } - - @Override - public void onAttach(GalleryView.Adapter adapter) { - AssertUtils.assertNull("The PagerLayoutManager is attached", mAdapter); - AssertUtils.assertNotNull("The adapter is null", adapter); - mAdapter = adapter; - // Reset parameters - resetParameters(); - } - - private void removeProgress() { - if (mProgress != null) { - mGalleryView.removeComponent(mProgress); - mGalleryView.releaseProgress(mProgress); - mProgress = null; - } - } - - private void removeErrorView() { - if (mErrorView != null) { - mGalleryView.removeComponent(mErrorView); - mGalleryView.releaseErrorView(mErrorView); - mErrorView = null; - mErrorStr = null; - } - } - - private void removePage(@NonNull GalleryPageView page) { - mGalleryView.removeComponent(page); - mAdapter.unbind(page); - mGalleryView.releasePage(page); - } - - private void removeAllPages() { - // Remove gallery view - if (mPrevious != null) { - removePage(mPrevious); - mPrevious = null; - } - if (mCurrent != null) { - removePage(mCurrent); - mCurrent = null; - } - if (mNext != null) { - removePage(mNext); - mNext = null; - } - } - - @Override - public GalleryView.Adapter onDetach() { - AssertUtils.assertNotNull("The PagerLayoutManager is not attached", mAdapter); - - // Cancel all animations - cancelAllAnimations(); - - // Remove all view - removeProgress(); - removeErrorView(); - removeAllPages(); - - // Clear iterator - GalleryView.Adapter adapter = mAdapter; - mAdapter = null; - - return adapter; - } - - private GalleryPageView getLeftPage() { - switch (mMode) { - case MODE_LEFT_TO_RIGHT: - return mPrevious; - default: - case MODE_RIGHT_TO_LEFT: - return mNext; - } - } - - private GalleryPageView getRightPage() { - switch (mMode) { - case MODE_LEFT_TO_RIGHT: - return mNext; - default: - case MODE_RIGHT_TO_LEFT: - return mPrevious; - } - } - - private GalleryPageView obtainPage() { - GalleryPageView page = mGalleryView.obtainPage(); - page.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue); - return page; - } - - private void layoutPage(GalleryPageView page, int widthSpec, int heightSpec, - int left, int top, int right, int bottom) { - Rect rect = mTempRect; - page.getValidRect(rect); - boolean oldValid = !rect.isEmpty(); - page.measure(widthSpec, heightSpec); - page.layout(left, top, right, bottom); - page.getValidRect(rect); - boolean newValid = !rect.isEmpty(); - if (!oldValid && newValid) { - page.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue); - } - } - - @Override - public void onFill() { - GalleryView.Adapter adapter = mAdapter; - GalleryView galleryView = mGalleryView; - AssertUtils.assertNotNull("The PagerLayoutManager is not attached", adapter); - - int width = galleryView.getWidth(); - int height = galleryView.getHeight(); - int size = adapter.size(); - String errorStr = adapter.getError(); - - if (size == GalleryProvider.STATE_WAIT) { // Wait here, show progress bar - // Remove error view and all pages - removeErrorView(); - removeAllPages(); - - // Ensure progress - if (mProgress == null) { - mProgress = galleryView.obtainProgress(); - galleryView.addComponent(mProgress); - } - - // Place progress center - placeCenter(mProgress); - } else if (size <= GalleryProvider.STATE_ERROR || size == 0) { // Get error or empty, show error text - // Ensure error is not null - if (0 == size) { - errorStr = galleryView.getEmptyStr(); - } else if (null == errorStr) { - errorStr = galleryView.getDefaultErrorStr(); - } - - // Remove progress and all pages - removeProgress(); - removeAllPages(); - - // Ensure error view - if (mErrorView == null) { - mErrorView = galleryView.obtainErrorView(); - galleryView.addComponent(mErrorView); - } - - // Update error string - if (!errorStr.equals(mErrorStr)) { - mErrorStr = errorStr; - galleryView.bindErrorView(mErrorView, errorStr); - } - - // Place error view center - placeCenter(mErrorView); - } else { - // Remove progress and error view - removeProgress(); - removeErrorView(); - - // Ensure index in range - int index = mIndex; - if (index < 0) { - index = 0; - mIndex = index; - removeAllPages(); - Log.e(TAG, "index < 0, index = " + index); - } else if (index >= size) { - index = size - 1; - mIndex = index; - removeAllPages(); - Log.e(TAG, "index >= size, index = " + index + ", size = " + size); - } - - // Ensure pages - if (mCurrent == null) { - mCurrent = obtainPage(); - galleryView.addComponent(mCurrent); - adapter.bind(mCurrent, index); - } - if (mPrevious == null && index > 0) { - mPrevious = obtainPage(); - galleryView.addComponent(mPrevious); - adapter.bind(mPrevious, index - 1); - } else if (mPrevious != null && index == 0) { - removePage(mPrevious); - } - if (mNext == null && index < size - 1) { - mNext = obtainPage(); - galleryView.addComponent(mNext); - adapter.bind(mNext, index + 1); - } else if (mNext != null && index == size - 1) { - removePage(mNext); - } - - GalleryPageView leftPage = getLeftPage(); - GalleryPageView rightPage = getRightPage(); - - // Fix offset - final int min = rightPage == null ? 0 : -width - mInterval + 1; - final int max = leftPage == null ? 0 : width + mInterval - 1; - mOffset = MathUtils.clamp(mOffset, min, max); - - // Measure and layout pages - final int offset = mOffset; - final int widthSpec = GLView.MeasureSpec.makeMeasureSpec(width, GLView.MeasureSpec.EXACTLY); - final int heightSpec = GLView.MeasureSpec.makeMeasureSpec(height, GLView.MeasureSpec.EXACTLY); - if (mCurrent != null) { - layoutPage(mCurrent, widthSpec, heightSpec, - offset, 0, width + offset, height); - } - if (leftPage != null) { - layoutPage(leftPage, widthSpec, heightSpec, - -mInterval - width + offset, 0, -mInterval + offset, height); - } - if (rightPage != null) { - layoutPage(rightPage, widthSpec, heightSpec, - width + mInterval + offset, 0, width + mInterval + width + offset, height); - } - } - } - - @Override - public void onDown() { - mDeltaX = 0; - mDeltaY = 0; - mStopAnimationFinger = cancelAllAnimations(); - } - - @Override - public void onUp() { - if (mCurrent == null) { - return; - } - - // Scroll - if (mOffset != 0) { - int width = mGalleryView.getWidth(); - int dx; - if (mOffset >= mInterval && getLeftPage() != null) { - dx = mOffset - width - mInterval; - } else if (mOffset <= -mInterval && getRightPage() != null) { - dx = mOffset + width + mInterval; - } else { - dx = mOffset; - } - final float pageDelta = 7 * (float) Math.abs(mOffset) / (width + mInterval); - int duration = (int) ((pageDelta + 1) * 100); - mSmoothScroller.startSmoothScroll(dx, 0, duration); - } - } - - @Override - public void onDoubleTapConfirmed(float x, float y) { - if (mCurrent == null || !mCurrent.getImageView().isLoaded()) { - return; - } - - float[] scales = mScaleDefault; - ImageView image = mCurrent.getImageView(); - image.getScaleDefault(scales); - float scale = image.getScale(); - float endScale = scales[0]; - for (float value : scales) { - if (scale < value - 0.01f) { - endScale = value; - break; - } - } - - mSmoothScaler.startSmoothScaler(x, y, scale, endScale, 300); - } - - @Override - public void onLongPress(float x, float y) { - } - - private void pagePrevious() { - if (mIndex <= 0) { - return; - } - mIndex--; - - if (mNext != null) { - removePage(mNext); - } - mNext = mCurrent; - mCurrent = mPrevious; - mPrevious = null; - - if (mIndex > 0) { - mPrevious = obtainPage(); - mGalleryView.addComponent(mPrevious); - mAdapter.bind(mPrevious, mIndex - 1); - } - } - - private void pageNext() { - GalleryView.Adapter adapter = mAdapter; - int size = adapter.size(); - if (mIndex >= size - 1) { - return; - } - mIndex++; - - if (mPrevious != null) { - removePage(mPrevious); - } - mPrevious = mCurrent; - mCurrent = mNext; - mNext = null; - - if (mIndex < size - 1) { - mNext = obtainPage(); - mGalleryView.addComponent(mNext); - adapter.bind(mNext, mIndex + 1); - } - } - - private void pageLeft() { - switch (mMode) { - case MODE_LEFT_TO_RIGHT: - pagePrevious(); - break; - default: - case MODE_RIGHT_TO_LEFT: - pageNext(); - break; - } - } - - private void pageRight() { - switch (mMode) { - case MODE_LEFT_TO_RIGHT: - pageNext(); - break; - default: - case MODE_RIGHT_TO_LEFT: - pagePrevious(); - break; - } - } - - private int scrollBetweenPages(int dx) { - GalleryPageView leftPage = getLeftPage(); - GalleryPageView rightPage = getRightPage(); - int width = mGalleryView.getWidth(); - - int remain; - if (dx < 0) { // Try to show left - int limit; - if (leftPage == null) { - limit = 0; - } else { - limit = width + mInterval; - } - - if (dx > mOffset - limit) { - remain = 0; - mOffset -= dx; - } else { - // Go to left page if left page not null - if (leftPage != null) { - pageLeft(); - } - remain = dx + limit - mOffset; - mOffset = 0; - } - } else { // Try to show right - int limit; - if (rightPage == null) { - limit = 0; - } else { - limit = -width - mInterval; - } - - if (dx < mOffset - limit) { - remain = 0; - mOffset -= dx; - } else { - // Go to right page if right page not null - if (rightPage != null) { - pageRight(); - } - remain = dx + limit - mOffset; - mOffset = 0; - } - } - - return remain; - } - - public void scrollInternal(float dx, float dy, float x, float y) { - if (mCurrent == null) { - return; - } - - boolean needFill = false; - boolean canImageScroll = true; - int remainX = (int) dx; - int remainY = (int) dy; - - if (mGalleryView.isFirstScroll()) { - mCanScrollBetweenPages = Math.abs(dx) > Math.abs(dy) * 1.5; - } - - while (remainX != 0 || remainY != 0) { - if (mOffset == 0 && canImageScroll) { - ImageView image = mCurrent.getImageView(); - image.scroll(remainX, remainY, mScrollRemain); - remainX = mScrollRemain[0]; - remainY = mScrollRemain[1]; - canImageScroll = false; - mDeltaX = 0; - mDeltaY = 0; - } else if (remainX == 0 || - (getLeftPage() == null && mOffset == 0 && remainX < 0) || - (getRightPage() == null && mOffset == 0 && remainX > 0)) { - // On edge - remainX = 0; - remainY = 0; - } else if (mCanScrollBetweenPages) { - remainX = scrollBetweenPages(remainX); - canImageScroll = true; - needFill = true; - mDeltaX = 0; - mDeltaY = 0; - } else { - remainX = 0; - remainY = 0; - mDeltaX = 0; - mDeltaY = 0; - } - } - - if (needFill) { - mGalleryView.requestFill(); - } - } - - @Override - public void onScroll(float dx, float dy, float totalX, float totalY, float x, float y) { - scrollInternal(dx, dy, x, y); - } - - @Override - public void onFling(float velocityX, float velocityY) { - if (mCurrent == null || mOffset != 0 || !mCurrent.getImageView().isLoaded() || - !mCurrent.getImageView().canFling()) { - return; - } - - ImageView image = mCurrent.getImageView(); - mPageFling.startFling((int) velocityX, image.getMinDx(), image.getMaxDx(), - (int) velocityY, image.getMinDy(), image.getMaxDy()); - } - - @Override - public boolean canScale() { - return mCurrent != null && mOffset == 0 && mCurrent.getImageView().isLoaded(); - } - - @Override - public void onScale(float focusX, float focusY, float scale) { - if (mCurrent == null || !mCurrent.getImageView().isLoaded()) { - return; - } - - mCurrent.getImageView().scale(focusX, focusY, scale); - mScaleValue = mCurrent.getImageView().getScale(); - } - - @Override - public boolean onUpdateAnimation(long time) { - boolean invalidate = mSmoothScroller.calculate(time); - invalidate |= mPageFling.calculate(time); - invalidate |= mSmoothScaler.calculate(time); - return invalidate; - } - - @Override - public void onDataChanged() { - AssertUtils.assertNotNull("The PagerLayoutManager is not attached", mAdapter); - - // Cancel all animations - cancelAllAnimations(); - // Remove all views - removeProgress(); - removeErrorView(); - removeAllPages(); - // Reset parameters - resetParameters(); - mGalleryView.requestFill(); - } - - @Override - public void onPageLeft() { - int size = mAdapter.size(); - if (size <= 0 || mCurrent == null) { - return; - } - - if (mMode == MODE_LEFT_TO_RIGHT) { - if (mIndex != 0) { - setCurrentIndex(mIndex - 1); - } - } else { - if (mIndex < size - 1) { - setCurrentIndex(mIndex + 1); - } - } - } - - @Override - public void onPageRight() { - int size = mAdapter.size(); - if (size <= 0 || mCurrent == null) { - return; - } - - if (mMode == MODE_LEFT_TO_RIGHT) { - if (mIndex < size - 1) { - setCurrentIndex(mIndex + 1); - } - } else { - if (mIndex != 0) { - setCurrentIndex(mIndex - 1); - } - } - } - - @Override - public boolean isTapOrPressEnable() { - return !mStopAnimationFinger; - } - - @Override - public GalleryPageView findPageByIndex(int index) { - if (mCurrent != null && mCurrent.getIndex() == index) { - return mCurrent; - } - if (mPrevious != null && mPrevious.getIndex() == index) { - return mPrevious; - } - if (mNext != null && mNext.getIndex() == index) { - return mNext; - } - return null; - } - - @Override - public int getCurrentIndex() { - if (mCurrent != null) { - return mCurrent.getIndex(); - } else { - return GalleryPageView.INVALID_INDEX; - } - } - - @Override - public void setCurrentIndex(int index) { - int size = mAdapter.size(); - if (size <= 0) { - // Can't get size now, assume size is MAX - size = Integer.MAX_VALUE; - } - if (index == mIndex || index < 0 || index >= size) { - return; - } - if (mCurrent == null) { - mIndex = index; - } else if (index == mIndex - 1) { - // Cancel all animations - cancelAllAnimations(); - // Reset parameters - resetParameters(); - // Go to previous - pagePrevious(); - // Request fill - mGalleryView.requestFill(); - } else if (index == mIndex + 1) { - // Cancel all animations - cancelAllAnimations(); - // Reset parameters - resetParameters(); - // Go to next - pageNext(); - // Request fill - mGalleryView.requestFill(); - } else { - mIndex = index; - // It is attached, refill - // Cancel all animations - cancelAllAnimations(); - // Remove all view - removeProgress(); - removeErrorView(); - removeAllPages(); - // Reset parameters - resetParameters(); - // Request fill - mGalleryView.requestFill(); - } - } - - @Override - public int getIndexUnder(float x, float y) { - if (mCurrent == null) { - return GalleryPageView.INVALID_INDEX; - } else { - int intX = (int) x; - int intY = (int) y; - if (mCurrent.bounds().contains(intX, intY)) { - return mCurrent.getIndex(); - } else if (mPrevious != null && mPrevious.bounds().contains(intX, intY)) { - return mPrevious.getIndex(); - } else if (mNext != null && mNext.bounds().contains(intX, intY)) { - return mNext.getIndex(); - } else { - return GalleryPageView.INVALID_INDEX; - } - } - } - - @Override - int getInternalCurrentIndex() { - int currentIndex = getCurrentIndex(); - if (currentIndex == GalleryPageView.INVALID_INDEX) { - currentIndex = mIndex; - } - return currentIndex; - } - - @IntDef({MODE_LEFT_TO_RIGHT, MODE_RIGHT_TO_LEFT}) - @Retention(RetentionPolicy.SOURCE) - private @interface Mode { - } - - private class SmoothScroller extends Animation { - - private int mDx; - private int mDy; - private int mLastX; - private int mLastY; - - public SmoothScroller() { - setInterpolator(SMOOTH_SCROLLER_INTERPOLATOR); - } - - public void startSmoothScroll(int dx, int dy, int duration) { - mDx = dx; - mDy = dy; - mLastX = 0; - mLastY = 0; - setDuration(duration); - start(); - mGalleryView.invalidate(); - } - - @Override - protected void onCalculate(float progress) { - int x = (int) (mDx * progress); - int y = (int) (mDy * progress); - int offsetX = x - mLastX; - while (offsetX != 0) { - int oldOffsetX = offsetX; - offsetX = scrollBetweenPages(offsetX); - // Avoid loop infinitely - if (offsetX == oldOffsetX) { - break; - } else { - mGalleryView.requestFill(); - } - } - mLastX = x; - mLastY = y; - } - } - - private class PageFling extends Fling { - - private final int[] mTemp = new int[2]; - private int mVelocityX; - private int mVelocityY; - private int mDx; - private int mDy; - private int mLastX; - private int mLastY; - - public PageFling(Context context) { - super(context); - } - - public void startFling(int velocityX, int minX, int maxX, - int velocityY, int minY, int maxY) { - mVelocityX = velocityX; - mVelocityY = velocityY; - mDx = (int) (getSplineFlingDistance(velocityX) * Math.signum(velocityX)); - mDy = (int) (getSplineFlingDistance(velocityY) * Math.signum(velocityY)); - mLastX = 0; - mLastY = 0; - int durationX = getSplineFlingDuration(velocityX); - int durationY = getSplineFlingDuration(velocityY); - - if (mDx < minX) { - durationX = adjustDuration(0, mDx, minX, durationX); - mDx = minX; - } - if (mDx > maxX) { - durationX = adjustDuration(0, mDx, maxX, durationX); - mDx = maxX; - } - if (mDy < minY) { - durationY = adjustDuration(0, mDy, minY, durationY); - mDy = minY; - } - if (mDy > maxY) { - durationY = adjustDuration(0, mDy, maxY, durationY); - mDy = maxY; - } - - if (mDx == 0 && mDy == 0) { - return; - } - - setDuration(Math.max(durationX, durationY)); - start(); - mGalleryView.invalidate(); - } - - @Override - protected void onCalculate(float progress) { - int x = (int) (mDx * progress); - int y = (int) (mDy * progress); - int offsetX = x - mLastX; - int offsetY = y - mLastY; - if (mCurrent != null && (offsetX != 0 || offsetY != 0)) { - mCurrent.getImageView().scroll(-offsetX, -offsetY, mTemp); - } - mLastX = x; - mLastY = y; - } - } - - private class SmoothScaler extends Animation { - - private float mFocusX; - private float mFocusY; - private float mStartScale; - private float mEndScale; - private float mLastScale; - - public SmoothScaler() { - setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR); - } - - public void startSmoothScaler(float focusX, float focusY, - float startScale, float endScale, int duration) { - mFocusX = focusX; - mFocusY = focusY; - mStartScale = startScale; - mEndScale = endScale; - mLastScale = startScale; - setDuration(duration); - start(); - mGalleryView.invalidate(); - } - - @Override - protected void onCalculate(float progress) { - if (mCurrent == null) { - return; - } - - float scale = MathUtils.lerp(mStartScale, mEndScale, progress); - mCurrent.getImageView().scale(mFocusX, mFocusY, scale / mLastScale); - mLastScale = scale; - mScaleValue = scale; - } - } -} diff --git a/app/src/main/java/com/hippo/glgallery/ScrollLayoutManager.java b/app/src/main/java/com/hippo/glgallery/ScrollLayoutManager.java deleted file mode 100644 index 7dfc4ff57..000000000 --- a/app/src/main/java/com/hippo/glgallery/ScrollLayoutManager.java +++ /dev/null @@ -1,1134 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glgallery; - -import android.content.Context; -import android.graphics.Rect; -import android.util.Log; - -import androidx.annotation.NonNull; - -import com.hippo.glview.anim.Animation; -import com.hippo.glview.view.GLView; -import com.hippo.glview.widget.GLProgressView; -import com.hippo.glview.widget.GLTextureView; -import com.hippo.yorozuya.AnimationUtils; -import com.hippo.yorozuya.AssertUtils; -import com.hippo.yorozuya.MathUtils; - -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; - -class ScrollLayoutManager extends GalleryView.LayoutManager { - - private static final String TAG = ScrollLayoutManager.class.getSimpleName(); - - private static final float RESERVATION = 1f; - - private static final float MAX_SCALE = 2.0f; - private static final float MIN_SCALE = 1.0f; - private static final float SCALE_ERROR = 0.01f; - - private static final int INVALID_TOP = Integer.MAX_VALUE; - private final LinkedList mPages = new LinkedList<>(); - private final LinkedList mTempPages = new LinkedList<>(); - private final PageFling mPageFling; - private final SmoothScaler mSmoothScaler; - private GalleryView.Adapter mAdapter; - private GLProgressView mProgress; - private String mErrorStr; - private GLTextureView mErrorView; - private float mScale = 0.5f; - private int mOffsetX; - private int mOffsetY; - private int mDeltaX; - private int mDeltaY; - private int mKeepTopPageIndex = GalleryPageView.INVALID_INDEX; - private int mKeepTop = INVALID_TOP; - private int mFirstShownPageIndex = GalleryPageView.INVALID_INDEX; - private boolean mScrollUp; - private boolean mFlingUp; - private boolean mStopAnimationFinger; - private int mInterval; - // Current index - private int mIndex; - - private int mBottomStateBottom; - private boolean mBottomStateHasNext; - - public ScrollLayoutManager(Context context, @NonNull GalleryView galleryView, int interval) { - super(galleryView); - - mInterval = interval; - mPageFling = new PageFling(context); - mSmoothScaler = new SmoothScaler(); - } - - public void setInterval(int interval) { - if (mInterval == interval) { - return; - } - - if (mAdapter != null) { - int index = getInternalCurrentIndex(); - GalleryView.Adapter adapter = onDetach(); - mInterval = interval; - onAttach(adapter); - setCurrentIndex(index); - mGalleryView.requestFill(); - } else { - mInterval = interval; - } - } - - private void resetParameters() { - mScale = 1.0f; - mOffsetX = 0; - mOffsetY = 0; - mDeltaX = 0; - mDeltaY = 0; - mKeepTopPageIndex = GalleryPageView.INVALID_INDEX; - mKeepTop = INVALID_TOP; - mFirstShownPageIndex = GalleryPageView.INVALID_INDEX; - mScrollUp = false; - mFlingUp = false; - mStopAnimationFinger = false; - } - - // Return true for animations are running - private boolean cancelAllAnimations() { - boolean running = mPageFling.isRunning() || - mSmoothScaler.isRunning(); - mPageFling.cancel(); - mSmoothScaler.cancel(); - return running; - } - - @Override - public void onAttach(GalleryView.Adapter adapter) { - AssertUtils.assertNull("The ScrollLayoutManager is attached", mAdapter); - AssertUtils.assertNotNull("The iterator is null", adapter); - mAdapter = adapter; - // Reset parameters - resetParameters(); - } - - private void removeProgress() { - if (mProgress != null) { - mGalleryView.removeComponent(mProgress); - mGalleryView.releaseProgress(mProgress); - mProgress = null; - } - } - - private void removeErrorView() { - if (mErrorView != null) { - mGalleryView.removeComponent(mErrorView); - mGalleryView.releaseErrorView(mErrorView); - mErrorView = null; - mErrorStr = null; - } - } - - private void removePage(@NonNull GalleryPageView page) { - mGalleryView.removeComponent(page); - mAdapter.unbind(page); - mGalleryView.releasePage(page); - } - - private void removeAllPages() { - for (GalleryPageView page : mPages) { - removePage(page); - } - mPages.clear(); - } - - @Override - public GalleryView.Adapter onDetach() { - AssertUtils.assertNotNull("The PagerLayoutManager is not attached", mAdapter); - - // Cancel all animations - cancelAllAnimations(); - - // Remove all view - removeProgress(); - removeErrorView(); - removeAllPages(); - - // Clear iterator - GalleryView.Adapter iterator = mAdapter; - mAdapter = null; - - return iterator; - } - - private GalleryPageView obtainPage() { - GalleryPageView page = mGalleryView.obtainPage(); - page.getImageView().setScaleOffset(ImageView.SCALE_FIT, ImageView.START_POSITION_TOP_RIGHT, 1.0f); - return page; - } - - private GalleryPageView getPageForIndex(List pages, int index, boolean remove) { - for (Iterator iterator = pages.iterator(); iterator.hasNext(); ) { - GalleryPageView page = iterator.next(); - if (index == page.getIndex()) { - if (remove) { - iterator.remove(); - } - return page; - } - } - return null; - } - - private boolean isInScreen(GalleryPageView page) { - int height = mGalleryView.getHeight(); - Rect bound = page.bounds(); - int pageTop = bound.top; - int pageBottom = bound.bottom; - return (pageTop >= 0 && pageTop < height) || (pageBottom > 0 && pageBottom <= height) || - (pageTop < 0 && pageBottom > height); - } - - private float getReservation() { - return Math.max(RESERVATION, (((1 + 2 * RESERVATION) * mScale) - 1) / 2); - } - - private void fillPages(int startIndex, int startOffset) { - final GalleryView.Adapter adapter = mAdapter; - final GalleryView galleryView = mGalleryView; - final LinkedList pages = mPages; - final LinkedList tempPages = mTempPages; - final int width = galleryView.getWidth(); - final int height = galleryView.getHeight(); - final int pageWidth = (int) (width * mScale); - final int size = adapter.size(); - final int interval = mInterval; - final float reservation = getReservation(); - final int minY = (int) (-height * reservation); - final int maxY = (int) (height * (1 + reservation)); - final int widthSpec = GLView.MeasureSpec.makeMeasureSpec(pageWidth, GLView.MeasureSpec.EXACTLY); - final int heightSpec = GLView.MeasureSpec.makeMeasureSpec(height, GLView.MeasureSpec.UNSPECIFIED); - - // Fix start index and start offset - if (startIndex < 0) { - startIndex = 0; - startOffset = 0; - } else if (startIndex >= size) { - startIndex = size - 1; - startOffset = 0; - } else if (startOffset < minY) { - while (true) { - GalleryPageView page = getPageForIndex(pages, startIndex, false); - if (null == page) { - startOffset = minY; - break; - } else { - page.measure(widthSpec, heightSpec); - if (startOffset + page.getHeight() > minY) { - break; - } else if (size - 1 == startIndex) { - startOffset = 0; - break; - } else { - ++startIndex; - startOffset += page.getHeight() + interval; - if (startOffset >= minY) { - break; - } - } - } - } - } else if (startOffset >= maxY) { - if (0 == startIndex) { - startOffset = 0; - } else { - --startIndex; - int startBottomOffset = startOffset - interval; - while (true) { - GalleryPageView page = getPageForIndex(pages, startIndex, false); - if (null == page) { - startOffset = maxY - 1; - break; - } else { - page.measure(widthSpec, heightSpec); - startOffset = startBottomOffset - page.getHeight(); - if (startOffset < maxY) { - break; - } else if (0 == startIndex) { - startOffset = 0; - break; - } else { - --startIndex; - startBottomOffset = startOffset - interval; - } - } - } - } - } - - // Put page to temp list - tempPages.addAll(pages); - pages.clear(); - - // Sanitize offsetX - int margin = pageWidth - width; - if (margin >= 0) { - mOffsetX = MathUtils.clamp(mOffsetX, -margin, 0); - } else { - mOffsetX = -margin / 2; - } - - // Layout start page - GalleryPageView page = getPageForIndex(tempPages, startIndex, true); - if (null == page) { - page = obtainPage(); - galleryView.addComponent(page); - adapter.bind(page, startIndex); - } - pages.add(page); - page.measure(widthSpec, heightSpec); - page.layout(mOffsetX, startOffset, mOffsetX + pageWidth, startOffset + page.getMeasuredHeight()); - - // Prepare for check up and down - int bottomOffset = startOffset - interval; - int topOffset = startOffset + page.getMeasuredHeight() + interval; - - // Check up - int index = startIndex - 1; - while (bottomOffset > minY && index >= 0) { - page = getPageForIndex(tempPages, index, true); - if (null == page) { - page = obtainPage(); - galleryView.addComponent(page); - adapter.bind(page, index); - } - pages.addFirst(page); - page.measure(widthSpec, heightSpec); - page.layout(mOffsetX, bottomOffset - page.getMeasuredHeight(), mOffsetX + pageWidth, bottomOffset); - // Update - bottomOffset -= page.getMeasuredHeight() + interval; - index--; - } - - // Avoid space in top - page = pages.getFirst(); - if (0 == page.getIndex() && page.bounds().top > 0) { - int offset = -page.bounds().top; - for (GalleryPageView p : pages) { - p.offsetTopAndBottom(offset); - } - topOffset += offset; - } - - // Check down - index = startIndex + 1; - while (topOffset < maxY && index < size) { - page = getPageForIndex(tempPages, index, true); - if (null == page) { - page = obtainPage(); - galleryView.addComponent(page); - adapter.bind(page, index); - } - pages.addLast(page); - page.measure(widthSpec, heightSpec); - page.layout(mOffsetX, topOffset, mOffsetX + pageWidth, topOffset + page.getMeasuredHeight()); - // Update - topOffset += page.getMeasuredHeight() + interval; - index++; - } - - // Avoid space in bottom - if (size - 1 == pages.getLast().getIndex()) { - while (true) { - page = pages.getLast(); - int pagesBottom = page.bounds().bottom; - if (pagesBottom >= height) { - break; - } - page = pages.getFirst(); - index = page.getIndex(); - if (0 == index) { - break; - } - --index; - int pagesTop = page.bounds().top; - - page = getPageForIndex(tempPages, index, true); - if (null == page) { - page = obtainPage(); - galleryView.addComponent(page); - adapter.bind(page, index); - } - pages.addFirst(page); - page.measure(widthSpec, heightSpec); - - int offset = Math.min(height - pagesBottom, page.getMeasuredHeight()); - for (GalleryPageView p : pages) { - p.offsetTopAndBottom(offset); - } - int bottom = pagesTop - interval + offset; - page.layout(mOffsetX, bottom - page.getMeasuredHeight(), mOffsetX + pageWidth, bottom); - } - } - - // Remove remain page - for (GalleryPageView p : tempPages) { - removePage(p); - } - tempPages.clear(); - - // Update state - if (!pages.isEmpty()) { - page = pages.getFirst(); - mIndex = page.getIndex(); - mOffsetY = page.bounds().top; - } - } - - @Override - public void onFill() { - GalleryView.Adapter adapter = mAdapter; - GalleryView galleryView = mGalleryView; - AssertUtils.assertNotNull("The PagerLayoutManager is not attached", adapter); - - int size = adapter.size(); - String errorStr = adapter.getError(); - - if (size == GalleryProvider.STATE_WAIT) { // Wait here, show progress bar - // Remove error view and all pages - removeErrorView(); - removeAllPages(); - - // Ensure progress - if (mProgress == null) { - mProgress = galleryView.obtainProgress(); - galleryView.addComponent(mProgress); - } - - // Place progress center - placeCenter(mProgress); - } else if (size <= GalleryProvider.STATE_ERROR || size == 0) { // Get error or empty, show error text - // Ensure error is not null - if (0 == size) { - errorStr = galleryView.getEmptyStr(); - } else if (null == errorStr) { - errorStr = galleryView.getDefaultErrorStr(); - } - - // Remove progress and all pages - removeProgress(); - removeAllPages(); - - // Ensure error view - if (mErrorView == null) { - mErrorView = galleryView.obtainErrorView(); - galleryView.addComponent(mErrorView); - } - - // Update error string - if (!errorStr.equals(mErrorStr)) { - mErrorStr = errorStr; - galleryView.bindErrorView(mErrorView, errorStr); - } - - // Place error view center - placeCenter(mErrorView); - } else { - LinkedList pages = mPages; - - // Remove progress and error view - removeProgress(); - removeErrorView(); - - // Ensure index in range - int index = mIndex; - if (index < 0) { - Log.e(TAG, "index < 0, index = " + index); - index = 0; - mIndex = index; - removeAllPages(); - } else if (index >= size) { - Log.e(TAG, "index >= size, index = " + index + ", size = " + size); - index = size - 1; - mIndex = index; - removeAllPages(); - } - - // Find keep index and keep top - int keepTop = INVALID_TOP; - int keepTopIndex; - if (GalleryPageView.INVALID_INDEX != mKeepTopPageIndex) { - keepTopIndex = mKeepTopPageIndex; - keepTop = mKeepTop; - } else if (GalleryPageView.INVALID_INDEX != mFirstShownPageIndex) { - keepTopIndex = mFirstShownPageIndex; - } else { - keepTopIndex = GalleryPageView.INVALID_INDEX; - } - if (GalleryPageView.INVALID_INDEX != keepTopIndex && INVALID_TOP == keepTop) { - keepTop = mOffsetY; - for (GalleryPageView page : pages) { - // Check keep page - if (keepTopIndex == page.getIndex()) { - break; - } - keepTop += page.getHeight() + mInterval; - } - } - - int startIndex; - int startOffset; - if (GalleryPageView.INVALID_INDEX != keepTopIndex) { - startIndex = keepTopIndex; - startOffset = keepTop; - } else { - startIndex = mIndex; - startOffset = mOffsetY; - } - fillPages(startIndex, startOffset); - - // Get first shown image - mFirstShownPageIndex = GalleryPageView.INVALID_INDEX; - for (GalleryPageView page : mPages) { - // Check first shown loaded page - if ((mScrollUp || mFlingUp) && !page.isLoaded()) { - continue; - } - - if (isInScreen(page)) { - mFirstShownPageIndex = page.getIndex(); - break; - } - } - } - } - - @Override - public void onDown() { - mDeltaX = 0; - mDeltaY = 0; - mScrollUp = false; - mStopAnimationFinger = cancelAllAnimations(); - } - - @Override - public void onUp() { - mScrollUp = false; - } - - @Override - public void onDoubleTapConfirmed(float x, float y) { - if (mPages.size() <= 0) { - return; - } - - float startScale = mScale; - float endScale; - if (startScale < MAX_SCALE - SCALE_ERROR) { - endScale = MAX_SCALE; - } else { - endScale = MIN_SCALE; - } - - mSmoothScaler.startSmoothScaler(x, y, startScale, endScale, 300); - } - - @Override - public void onLongPress(float x, float y) { - - } - - private void getBottomState() { - List pages = mPages; - int bottom = mOffsetY; - int i = 0; - for (GalleryPageView page : pages) { - if (i != 0) { - bottom += mInterval; - } - bottom += page.getHeight(); - i++; - } - boolean hasNext = mIndex + pages.size() < mAdapter.size(); - - mBottomStateBottom = bottom; - mBottomStateHasNext = hasNext; - } - - // True for get top or bottom - private boolean scrollInternal(float dx, float dy, boolean fling, float x, float y) { - if (mPages.size() <= 0) { - return false; - } - - GalleryView galleryView = mGalleryView; - int width = galleryView.getWidth(); - int height = galleryView.getHeight(); - int pageWidth = (int) (width * mScale); - final float reservation = getReservation(); - boolean requestFill = false; - boolean result = false; - - int margin = pageWidth - width; - int dxInt = (int) dx; - if (margin > 0 && 0 != dxInt) { - int oldOffsetX = mOffsetX; - int exceptOffsetX = oldOffsetX - dxInt; - mOffsetX = MathUtils.clamp(exceptOffsetX, -margin, 0); - if (mOffsetX != oldOffsetX) { - requestFill = true; - } - // Do not show over scroll effect for left and right - /* - int extraOffsetX = mOffsetX - exceptOffsetX; - if (0 != extraOffsetX) { - overScrollEdge(extraOffsetX, 0, x, y); - } - */ - } - - int remainY = (int) dy; - while (remainY != 0) { - if (remainY < 0) { // Try to show top - int limit; - if (mIndex > 0) { - limit = (int) (-height * reservation) + mInterval; - } else { - limit = 0; - } - - if (mOffsetY - remainY <= limit) { - mOffsetY -= remainY; - remainY = 0; - requestFill = true; - mDeltaX = 0; - mDeltaY = 0; - } else { - if (mIndex > 0) { - mOffsetY = limit; - remainY = remainY + limit - mOffsetY; - // Offset one pixel to avoid infinite loop - ++mOffsetY; - ++remainY; - galleryView.forceFill(); - requestFill = false; - mDeltaX = 0; - mDeltaY = 0; - } else { - if (mOffsetY != limit) { - mOffsetY = limit; - requestFill = true; - } - remainY = 0; - result = true; - } - } - } else { // Try to show bottom - getBottomState(); - int bottom = mBottomStateBottom; - boolean hasNext = mBottomStateHasNext; - - int limit; - if (hasNext) { - limit = (int) (height * (1 + reservation)) - mInterval; - } else { - limit = height; - } - // Fix limit for page not fill screen - limit = Math.min(bottom, limit); - - if (bottom - remainY >= limit) { - mOffsetY -= remainY; - remainY = 0; - requestFill = true; - mDeltaX = 0; - mDeltaY = 0; - } else { - if (hasNext) { - mOffsetY -= bottom - limit; - remainY = remainY + limit - bottom; - // Offset one pixel to avoid infinite loop - --mOffsetY; - --remainY; - galleryView.forceFill(); - requestFill = false; - mDeltaX = 0; - mDeltaY = 0; - } else { - if (mOffsetY != limit) { - mOffsetY -= bottom - limit; - requestFill = true; - } - remainY = 0; - result = true; - } - } - } - } - - if (requestFill) { - mGalleryView.requestFill(); - } - - return result; - } - - @Override - public void onScroll(float dx, float dy, float totalX, float totalY, float x, float y) { - mKeepTopPageIndex = GalleryPageView.INVALID_INDEX; - mKeepTop = INVALID_TOP; - mScrollUp = dy < 0; - scrollInternal(dx, dy, false, x, y); - } - - @Override - public void onFling(float velocityX, float velocityY) { - if (mPages.size() <= 0) { - return; - } - - mKeepTopPageIndex = GalleryPageView.INVALID_INDEX; - mKeepTop = INVALID_TOP; - mFlingUp = velocityY > 0; - - int maxX; - int minX; - int width = mGalleryView.getWidth(); - int pageWidth = (int) (width * mScale); - int margin = pageWidth - width; - if (margin > 0) { - maxX = -mOffsetX; - minX = -margin + mOffsetX; - } else { - maxX = 0; - minX = 0; - } - - int maxY; - if (mIndex > 0) { - maxY = Integer.MAX_VALUE; - } else { - maxY = -mOffsetY; - } - - getBottomState(); - int bottom = mBottomStateBottom; - boolean hasNext = mBottomStateHasNext; - int minY; - if (hasNext) { - minY = Integer.MIN_VALUE; - } else { - minY = mGalleryView.getHeight() - bottom; - } - - mPageFling.startFling((int) velocityX, minX, maxX, - (int) velocityY, minY, maxY); - } - - @Override - public boolean canScale() { - return mPages.size() > 0; - } - - @Override - public void onScale(float focusX, float focusY, float scale) { - if (mPages.isEmpty()) { - return; - } - - float oldScale = mScale; - mScale = MathUtils.clamp(oldScale * scale, MIN_SCALE, MAX_SCALE); - scale = mScale / oldScale; - - if (oldScale != mScale) { - GalleryPageView page = null; - // Keep scale page origin position - for (GalleryPageView p : mPages) { - if (p.bounds().top < focusY) { - page = p; - } else { - break; - } - } - - if (null != page) { - mKeepTopPageIndex = page.getIndex(); - mKeepTop = page.bounds().top; - - mGalleryView.forceFill(); - int oldKeepTop = mKeepTop; - mKeepTop = INVALID_TOP; - - // Apply scroll - int newOffsetX = (int) (focusX - ((focusX - mOffsetX) * scale)); - int newKeepTop; - if (page.isLoaded()) { - newKeepTop = (int) (focusY - ((focusY - oldKeepTop) * scale)); - } else { - newKeepTop = oldKeepTop; - } - scrollInternal(mOffsetX - newOffsetX, oldKeepTop - newKeepTop, false, focusX, focusY); - } else { - Log.e(TAG, "Can't find target page"); - mKeepTopPageIndex = GalleryPageView.INVALID_INDEX; - mKeepTop = INVALID_TOP; - mGalleryView.forceFill(); - } - } - } - - @Override - public boolean onUpdateAnimation(long time) { - boolean invalidate = mPageFling.calculate(time); - invalidate |= mSmoothScaler.calculate(time); - return invalidate; - } - - @Override - public void onDataChanged() { - AssertUtils.assertNotNull("The PagerLayoutManager is not attached", mAdapter); - - // Cancel all animations - cancelAllAnimations(); - // Remove all views - removeProgress(); - removeErrorView(); - removeAllPages(); - // Reset parameters - resetParameters(); - mGalleryView.requestFill(); - } - - @Override - public void onPageLeft() { - int size = mAdapter.size(); - if (size <= 0 || mPages.isEmpty()) { - return; - } - - /////// - // UP - /////// - GalleryView galleryView = mGalleryView; - if (mIndex == 0 && mOffsetY >= 0) { - } else { - // Cancel all animations - cancelAllAnimations(); - - // Get first shown page - GalleryPageView previousPage = null; - GalleryPageView firstShownPage = null; - for (GalleryPageView p : mPages) { - if (isInScreen(p)) { - firstShownPage = p; - break; - } - previousPage = p; - } - - int height = galleryView.getHeight(); - int maxOffset = height - mInterval; - if (null == firstShownPage) { - Log.e(TAG, "Can't find first shown page when paging left"); - mOffsetY += height / 2; - } else { - int firstShownTop = firstShownPage.bounds().top; - if (firstShownTop >= 0) { - if (null == previousPage) { - Log.e(TAG, "Can't find previous page when paging left and offsetY == 0"); - mOffsetY += height / 2; - } else { - mOffsetY += Math.min(maxOffset, -previousPage.bounds().top); - } - } else { - mOffsetY += Math.min(maxOffset, -firstShownTop); - } - } - - // Request fill - mGalleryView.requestFill(); - } - } - - @Override - public void onPageRight() { - int size = mAdapter.size(); - if (size <= 0 || mPages.isEmpty()) { - return; - } - - ///////// - // DOWN - ///////// - GalleryView galleryView = mGalleryView; - getBottomState(); - int bottom = mBottomStateBottom; - boolean hasNext = mBottomStateHasNext; - if (hasNext || bottom > galleryView.getHeight()) { - // Cancel all animations - cancelAllAnimations(); - - // Get first shown page - GalleryPageView lastShownPage = null; - GalleryPageView nextPage = null; - for (GalleryPageView p : mPages) { - if (isInScreen(p)) { - lastShownPage = p; - } else if (null != lastShownPage) { - nextPage = p; - break; - } - } - - int height = galleryView.getHeight(); - int maxOffset = height - mInterval; - if (null == lastShownPage) { - Log.e(TAG, "Can't find last shown page when paging left"); - mOffsetY -= height / 2; - } else { - int lastShownBottom = lastShownPage.bounds().bottom; - if (lastShownBottom <= height) { - if (null == nextPage) { - Log.e(TAG, "Can't find previous page when paging left and offsetY == 0"); - mOffsetY -= height / 2; - } else { - mOffsetY -= Math.min(maxOffset, nextPage.bounds().bottom - height); - } - } else { - mOffsetY -= Math.min(maxOffset, lastShownBottom - height); - } - } - - // Request fill - mGalleryView.requestFill(); - } - } - - @Override - public boolean isTapOrPressEnable() { - return !mStopAnimationFinger; - } - - @Override - public GalleryPageView findPageByIndex(int index) { - for (GalleryPageView page : mPages) { - if (page.getIndex() == index) { - return page; - } - } - return null; - } - - @Override - public int getCurrentIndex() { - for (GalleryPageView page : mPages) { - if (isInScreen(page)) { - return page.getIndex(); - } - } - return GalleryPageView.INVALID_INDEX; - } - - @Override - public void setCurrentIndex(int index) { - int size = mAdapter.size(); - if (size <= 0) { - // Can't get size now, assume size is MAX - size = Integer.MAX_VALUE; - } - if (index < 0 || index >= size) { - return; - } - - mKeepTopPageIndex = index; - mKeepTop = INVALID_TOP; - - if (mPages.isEmpty()) { - mIndex = index; - } else { - // Fix the index page - GalleryPageView targetPage = null; - for (GalleryPageView page : mPages) { - if (page.getIndex() == index) { - targetPage = page; - break; - } - } - - if (targetPage != null) { - // Cancel all animations - cancelAllAnimations(); - mOffsetY -= targetPage.bounds().top; - // Request fill - mGalleryView.requestFill(); - } else { - mIndex = index; - mOffsetY = 0; - // Cancel all animations - cancelAllAnimations(); - // Remove all view - removeProgress(); - removeErrorView(); - removeAllPages(); - // Request fill - mGalleryView.requestFill(); - } - } - } - - @Override - public int getIndexUnder(float x, float y) { - if (mPages.isEmpty()) { - return GalleryPageView.INVALID_INDEX; - } else { - int intX = (int) x; - int intY = (int) y; - for (GalleryPageView page : mPages) { - if (page.bounds().contains(intX, intY)) { - return page.getIndex(); - } - } - return GalleryPageView.INVALID_INDEX; - } - } - - @Override - int getInternalCurrentIndex() { - int currentIndex = getCurrentIndex(); - if (currentIndex == GalleryPageView.INVALID_INDEX) { - currentIndex = mIndex; - } - return currentIndex; - } - - private class PageFling extends Fling { - - private int mVelocityX; - private int mVelocityY; - private int mDx; - private int mDy; - private int mLastX; - private int mLastY; - - public PageFling(Context context) { - super(context); - } - - public void startFling(int velocityX, int minX, int maxX, - int velocityY, int minY, int maxY) { - mVelocityX = velocityX; - mVelocityY = velocityY; - mDx = (int) (getSplineFlingDistance(velocityX) * Math.signum(velocityX)); - mDy = (int) (getSplineFlingDistance(velocityY) * Math.signum(velocityY)); - mLastX = 0; - mLastY = 0; - int durationX = getSplineFlingDuration(velocityX); - int durationY = getSplineFlingDuration(velocityY); - - if (mDx < minX) { - durationX = adjustDuration(0, mDx, minX, durationX); - mDx = minX; - } - if (mDx > maxX) { - durationX = adjustDuration(0, mDx, maxX, durationX); - mDx = maxX; - } - if (mDy < minY) { - durationY = adjustDuration(0, mDy, minY, durationY); - mDy = minY; - } - if (mDy > maxY) { - durationY = adjustDuration(0, mDy, maxY, durationY); - mDy = maxY; - } - - if (mDx == 0 && mDy == 0) { - return; - } - - setDuration(Math.max(durationX, durationY)); - start(); - mGalleryView.invalidate(); - } - - @Override - protected void onCalculate(float progress) { - int x = (int) (mDx * progress); - int y = (int) (mDy * progress); - int offsetX = x - mLastX; - int offsetY = y - mLastY; - if (scrollInternal(-offsetX, -offsetY, true, 0, 0)) { - cancel(); - onFinish(); - } - mLastX = x; - mLastY = y; - } - - @Override - protected void onFinish() { - mFlingUp = false; - - int index = mIndex; - - boolean topEdge = index <= 0 && mOffsetY >= 0; - - getBottomState(); - int bottom = mBottomStateBottom; - boolean hasNext = mBottomStateHasNext; - boolean bottomEdge = !hasNext && bottom <= mGalleryView.getHeight(); - } - } - - private class SmoothScaler extends Animation { - - private float mFocusX; - private float mFocusY; - private float mStartScale; - private float mEndScale; - private float mLastScale; - - public SmoothScaler() { - setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR); - } - - public void startSmoothScaler(float focusX, float focusY, - float startScale, float endScale, int duration) { - mFocusX = focusX; - mFocusY = focusY; - mStartScale = startScale; - mEndScale = endScale; - mLastScale = startScale; - setDuration(duration); - start(); - mGalleryView.invalidate(); - } - - @Override - protected void onCalculate(float progress) { - if (mPages.size() <= 0) { - return; - } - - float scale = MathUtils.lerp(mStartScale, mEndScale, progress); - onScale(mFocusX, mFocusY, scale / mLastScale); - mLastScale = scale; - } - } -} diff --git a/app/src/main/java/com/hippo/glgallery/SimpleAdapter.java b/app/src/main/java/com/hippo/glgallery/SimpleAdapter.java deleted file mode 100644 index a4712302d..000000000 --- a/app/src/main/java/com/hippo/glgallery/SimpleAdapter.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glgallery; - -import androidx.annotation.NonNull; - -import com.hippo.glview.image.ImageTexture; -import com.hippo.glview.image.ImageWrapper; -import com.hippo.glview.view.GLRootView; - -public class SimpleAdapter extends GalleryView.Adapter implements GalleryProvider.Listener { - - private final GalleryProvider mProvider; - private final ImageTexture.Uploader mUploader; - private boolean mShowIndex = true; - - public SimpleAdapter(@NonNull GLRootView glRootView, @NonNull GalleryProvider provider) { - mProvider = provider; - mUploader = new ImageTexture.Uploader(glRootView); - } - - public void clearUploader() { - mUploader.clear(); - } - - public void setShowIndex(boolean show) { - mShowIndex = show; - } - - @Override - public void onBind(GalleryPageView view, int index) { - mProvider.request(index); - view.showInfo(); - view.setImage(null); - if (mShowIndex) { - view.setPage(index + 1); - } else { - view.hidePage(); - } - view.setProgress(GalleryPageView.PROGRESS_INDETERMINATE); - view.setError(null, null); - } - - @Override - public void onUnbind(GalleryPageView view, int index) { - mProvider.cancelRequest(index); - view.setImage(null); - view.setError(null, null); - } - - @Override - public String getError() { - return mProvider.getError(); - } - - @Override - public int size() { - return mProvider.size(); - } - - @Override - public void onDataChanged() { - mGalleryView.onDataChanged(); - } - - @Override - public void onPageWait(int index) { - GalleryPageView page = findPageByIndex(index); - if (page != null) { - page.showInfo(); - page.setImage(null); - if (mShowIndex) { - page.setPage(index + 1); - } else { - page.hidePage(); - } - page.setProgress(GalleryPageView.PROGRESS_INDETERMINATE); - page.setError(null, null); - } - } - - @Override - public void onPagePercent(int index, float percent) { - GalleryPageView page = findPageByIndex(index); - if (page != null) { - page.showInfo(); - page.setImage(null); - if (mShowIndex) { - page.setPage(index + 1); - } else { - page.hidePage(); - } - page.setProgress(percent); - page.setError(null, null); - } - } - - @Override - public void onPageSucceed(int index, ImageWrapper image) { - GalleryPageView page = findPageByIndex(index); - if (page != null) { - if (image.obtain()) { - ImageTexture imageTexture = new ImageTexture(image); - mUploader.addTexture(imageTexture); - page.showImage(); - page.setImage(imageTexture); - if (mShowIndex) { - page.setPage(index + 1); - } else { - page.hidePage(); - } - page.setProgress(GalleryPageView.PROGRESS_GONE); - page.setError(null, null); - } else { - // The image is recycled, request again. - // TODO request loop ? - mProvider.request(index); - } - } - } - - @Override - public void onPageFailed(int index, String error) { - GalleryPageView page = findPageByIndex(index); - if (page != null) { - page.showInfo(); - page.setImage(null); - if (mShowIndex) { - page.setPage(index + 1); - } else { - page.hidePage(); - } - page.setProgress(GalleryPageView.PROGRESS_GONE); - page.setError(error, mGalleryView); - } - } - - @Override - public void onDataChanged(int index) { - GalleryPageView page = findPageByIndex(index); - if (page != null) { - mProvider.request(index); - } - } - - private GalleryPageView findPageByIndex(int index) { - return mGalleryView != null ? mGalleryView.findPageByIndex(index) : null; - } -} diff --git a/app/src/main/java/com/hippo/glview/anim/AlphaAnimation.java b/app/src/main/java/com/hippo/glview/anim/AlphaAnimation.java deleted file mode 100644 index 38f4be23d..000000000 --- a/app/src/main/java/com/hippo/glview/anim/AlphaAnimation.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.anim; - -import com.hippo.glview.glrenderer.GLCanvas; -import com.hippo.yorozuya.MathUtils; - -public class AlphaAnimation extends CanvasAnimation { - private final float mStartAlpha; - private final float mEndAlpha; - private float mCurrentAlpha; - - public AlphaAnimation(float from, float to) { - mStartAlpha = from; - mEndAlpha = to; - mCurrentAlpha = from; - } - - @Override - public void apply(GLCanvas canvas) { - canvas.multiplyAlpha(mCurrentAlpha); - } - - @Override - public int getCanvasSaveFlags() { - return GLCanvas.SAVE_FLAG_ALPHA; - } - - @Override - protected void onCalculate(float progress) { - mCurrentAlpha = MathUtils.clamp(mStartAlpha + - (mEndAlpha - mStartAlpha) * progress, 0f, 1f); - } -} diff --git a/app/src/main/java/com/hippo/glview/anim/Animation.java b/app/src/main/java/com/hippo/glview/anim/Animation.java deleted file mode 100644 index 5af48b333..000000000 --- a/app/src/main/java/com/hippo/glview/anim/Animation.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.anim; - -import android.os.SystemClock; -import android.view.animation.Interpolator; - -import com.hippo.yorozuya.MathUtils; - -// Animation calculates a value according to the current input time. -// -// 1. First we need to use setDuration(int) to set the duration of the -// animation. The duration is in milliseconds. -// 2. Then we should call start(). The actual start time is the first value -// passed to calculate(long). -// 3. Each time we want to get an animation value, we call -// calculate(long currentTimeMillis) to ask the Animation to calculate it. -// The parameter passed to calculate(long) should be nonnegative. -// 4. Use get() to get that value. -// -// In step 3, onCalculate(float progress) is called so subclasses can calculate -// the value according to progress (progress is a value in [0,1]). -// -// Before onCalculate(float) is called, There is an optional interpolator which -// can change the progress value. The interpolator can be set by -// setInterpolator(Interpolator). If the interpolator is used, the value passed -// to onCalculate may be (for example, the overshoot effect). -// -// The isActive() method returns true after the animation start() is called and -// before calculate is passed a value which reaches the duration of the -// animation. -// -// The start() method can be called again to restart the Animation. -// -abstract public class Animation { - /** - * When the animation reaches the end and repeatCount is INFINITE - * or a positive value, the animation restarts from the beginning. - */ - public static final int RESTART = 1; - /** - * When the animation reaches the end and repeatCount is INFINITE - * or a positive value, the animation reverses direction on every iteration. - */ - public static final int REVERSE = 2; - /** - * This value used used with the {@link #setRepeatCount(int)} property to repeat - * the animation indefinitely. - */ - public static final int INFINITE = -1; - private static final long ANIMATION_START = -1; - private static final long NO_ANIMATION = -2; - private long mStartTime = NO_ANIMATION; - private long mDuration; - private Interpolator mInterpolator; - private int mRepeatCount; - private int mRepeatMode; - - private int mRunnedCount; - private long mLastFrameTime; - - public void setInterpolator(Interpolator interpolator) { - mInterpolator = interpolator; - } - - public void setDuration(long duration) { - mDuration = duration; - } - - public void setRepeatCount(int repeatCount) { - mRepeatCount = repeatCount; - } - - public void setRepeatMode(int repeatMode) { - mRepeatMode = repeatMode; - } - - public boolean isRunning() { - return mStartTime > 0 || mStartTime == ANIMATION_START; - } - - public long getLastFrameTime() { - return mLastFrameTime; - } - - public void start() { - if (mStartTime == NO_ANIMATION) { - mStartTime = ANIMATION_START; - mRunnedCount = 0; - mLastFrameTime = 0; - } - } - - public void startAt(long time) { - start(); - setStartTime(time); - } - - public void startNow() { - start(); - setStartTime(SystemClock.uptimeMillis()); - } - - public void setStartTime(long time) { - mStartTime = time; - } - - public void cancel() { - mStartTime = NO_ANIMATION; - } - - public void reset() { - mStartTime = ANIMATION_START; - mRunnedCount = 0; - mLastFrameTime = 0; - } - - // TODO mRepeatMode - public boolean calculate(long currentTimeMillis) { - if (mStartTime == NO_ANIMATION) { - return false; - } - if (mStartTime == ANIMATION_START) { - mStartTime = currentTimeMillis; - } - mLastFrameTime = currentTimeMillis; - - long elapse = currentTimeMillis - mStartTime; - float x = MathUtils.clamp(mDuration == 0.0f ? 1.0f : (float) elapse / mDuration, 0.0f, 1.0f); // Avoid NaN - Interpolator i = mInterpolator; - onCalculate(i != null ? i.getInterpolation(x) : x); - - // It is ok to call cancel() in onCalculate() - if (mStartTime != NO_ANIMATION && elapse >= mDuration) { - mRunnedCount++; - if (mRunnedCount >= mRepeatCount && mRepeatCount != INFINITE) { - onFinish(); - mStartTime = NO_ANIMATION; - } else { - mStartTime += elapse; - } - } - - return mStartTime != NO_ANIMATION; - } - - abstract protected void onCalculate(float progress); - - protected void onFinish() { - } -} diff --git a/app/src/main/java/com/hippo/glview/anim/CanvasAnimation.java b/app/src/main/java/com/hippo/glview/anim/CanvasAnimation.java deleted file mode 100644 index f6ec87d1b..000000000 --- a/app/src/main/java/com/hippo/glview/anim/CanvasAnimation.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.anim; - -import com.hippo.glview.glrenderer.GLCanvas; - -public abstract class CanvasAnimation extends Animation { - - public abstract int getCanvasSaveFlags(); - - public abstract void apply(GLCanvas canvas); -} diff --git a/app/src/main/java/com/hippo/glview/anim/FloatAnimation.java b/app/src/main/java/com/hippo/glview/anim/FloatAnimation.java deleted file mode 100644 index 1336ba8d1..000000000 --- a/app/src/main/java/com/hippo/glview/anim/FloatAnimation.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.anim; - -import com.hippo.yorozuya.MathUtils; - -public class FloatAnimation extends Animation { - - private float mFrom; - private float mTo; - private float mCurrent; - - public void setRange(float from, float to) { - mFrom = from; - mTo = to; - } - - @Override - protected void onCalculate(float progress) { - mCurrent = MathUtils.lerp(mFrom, mTo, progress); - } - - public float get() { - return mCurrent; - } -} diff --git a/app/src/main/java/com/hippo/glview/annotation/RenderThread.java b/app/src/main/java/com/hippo/glview/annotation/RenderThread.java deleted file mode 100644 index 7410230ce..000000000 --- a/app/src/main/java/com/hippo/glview/annotation/RenderThread.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Retention(RetentionPolicy.SOURCE) -@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE}) -public @interface RenderThread { -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/BasicTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/BasicTexture.java deleted file mode 100644 index d6c3a9eba..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/BasicTexture.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.glrenderer; - -import android.graphics.RectF; -import android.util.Log; - -import com.hippo.yorozuya.MathUtils; - -import java.util.WeakHashMap; - -// BasicTexture is a Texture corresponds to a real GL texture. -// The state of a BasicTexture indicates whether its data is loaded to GL memory. -// If a BasicTexture is loaded into GL memory, it has a GL texture id. -public abstract class BasicTexture implements Texture { - - protected static final int UNSPECIFIED = -1; - protected static final int STATE_UNLOADED = 0; - protected static final int STATE_LOADED = 1; - protected static final int STATE_ERROR = -1; - private static final String TAG = "BasicTexture"; - // Log a warning if a texture is larger along a dimension - private static final int MAX_TEXTURE_SIZE = 4096; - private final static WeakHashMap sAllTextures - = new WeakHashMap<>(); - private static final ThreadLocal sInFinalizer = new ThreadLocal<>(); - protected int mId = -1; - protected int mState; - protected int mWidth = UNSPECIFIED; - protected int mHeight = UNSPECIFIED; - protected int mTextureWidth; - protected int mTextureHeight; - protected GLCanvas mCanvasRef = null; - private boolean mHasBorder; - - protected BasicTexture(GLCanvas canvas, int id, int state) { - setAssociatedCanvas(canvas); - mId = id; - mState = state; - synchronized (sAllTextures) { - sAllTextures.put(this, null); - } - } - - protected BasicTexture() { - this(null, 0, STATE_UNLOADED); - } - - // This is for deciding if we can call Bitmap's recycle(). - // We cannot call Bitmap's recycle() in finalizer because at that point - // the finalizer of Bitmap may already be called so recycle() will crash. - public static boolean inFinalizer() { - return sInFinalizer.get() != null; - } - - public static void yieldAllTextures() { - synchronized (sAllTextures) { - for (BasicTexture t : sAllTextures.keySet()) { - t.yield(); - } - } - } - - public static void invalidateAllTextures() { - synchronized (sAllTextures) { - for (BasicTexture t : sAllTextures.keySet()) { - t.mState = STATE_UNLOADED; - t.setAssociatedCanvas(null); - } - } - } - - protected void setAssociatedCanvas(GLCanvas canvas) { - mCanvasRef = canvas; - } - - /** - * Sets the content size of this texture. In OpenGL, the actual texture - * size must be of power of 2, the size of the content may be smaller. - */ - public void setSize(int width, int height) { - mWidth = width; - mHeight = height; - mTextureWidth = width > 0 ? MathUtils.nextPowerOf2(width) : 0; - mTextureHeight = height > 0 ? MathUtils.nextPowerOf2(height) : 0; - if (mTextureWidth > MAX_TEXTURE_SIZE || mTextureHeight > MAX_TEXTURE_SIZE) { - Log.w(TAG, String.format("texture is too large: %d x %d", - mTextureWidth, mTextureHeight), new Exception()); - } - } - - public boolean isFlippedVertically() { - return false; - } - - public int getId() { - return mId; - } - - @Override - public int getWidth() { - return mWidth; - } - - @Override - public int getHeight() { - return mHeight; - } - - // Returns the width rounded to the next power of 2. - public int getTextureWidth() { - return mTextureWidth; - } - - // Returns the height rounded to the next power of 2. - public int getTextureHeight() { - return mTextureHeight; - } - - // Returns true if the texture has one pixel transparent border around the - // actual content. This is used to avoid jigged edges. - // - // The jigged edges appear because we use GL_CLAMP_TO_EDGE for texture wrap - // mode (GL_CLAMP is not available in OpenGL ES), so a pixel partially - // covered by the texture will use the color of the edge texel. If we add - // the transparent border, the color of the edge texel will be mixed with - // appropriate amount of transparent. - // - // Currently our background is black, so we can draw the thumbnails without - // enabling blending. - public boolean hasBorder() { - return mHasBorder; - } - - protected void setBorder(boolean hasBorder) { - mHasBorder = hasBorder; - } - - @Override - public void draw(GLCanvas canvas, int x, int y) { - canvas.drawTexture(this, x, y, getWidth(), getHeight()); - } - - @Override - public void draw(GLCanvas canvas, int x, int y, int w, int h) { - canvas.drawTexture(this, x, y, w, h); - } - - @Override - public void draw(GLCanvas canvas, RectF source, RectF target) { - canvas.drawTexture(this, source, target); - } - - // onBind is called before GLCanvas binds this texture. - // It should make sure the data is uploaded to GL memory. - abstract protected boolean onBind(GLCanvas canvas); - - // Returns the GL texture target for this texture (e.g. GL_TEXTURE_2D). - abstract protected int getTarget(); - - public boolean isLoaded() { - return mState == STATE_LOADED; - } - - // recycle() is called when the texture will never be used again, - // so it can free all resources. - public void recycle() { - freeResource(); - } - - // yield() is called when the texture will not be used temporarily, - // so it can free some resources. - // The default implementation unloads the texture from GL memory, so - // the subclass should make sure it can reload the texture to GL memory - // later, or it will have to override this method. - public void yield() { - freeResource(); - } - - private void freeResource() { - GLCanvas canvas = mCanvasRef; - if (canvas != null && mId != -1) { - canvas.unloadTexture(this); - mId = -1; // Don't free it again. - } - mState = STATE_UNLOADED; - setAssociatedCanvas(null); - } - - @Override - protected void finalize() throws Throwable { - try { - sInFinalizer.set(BasicTexture.class); - recycle(); - sInFinalizer.set(null); - } finally { - super.finalize(); - } - } -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/BitmapTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/BitmapTexture.java deleted file mode 100644 index 643f7a4fb..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/BitmapTexture.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.glrenderer; - -import android.graphics.Bitmap; - -import com.hippo.yorozuya.AssertUtils; - -// BitmapTexture is a texture whose content is specified by a fixed Bitmap. -// -// The texture does not own the Bitmap. The user should make sure the Bitmap -// is valid during the texture's lifetime. When the texture is recycled, it -// does not free the Bitmap. -public class BitmapTexture extends UploadedTexture { - protected Bitmap mContentBitmap; - - public BitmapTexture(Bitmap bitmap) { - this(bitmap, false); - } - - public BitmapTexture(Bitmap bitmap, boolean hasBorder) { - super(hasBorder); - AssertUtils.assertTrue(bitmap != null && !bitmap.isRecycled()); - mContentBitmap = bitmap; - } - - @Override - protected void onFreeBitmap(Bitmap bitmap) { - // Do nothing. - } - - @Override - protected Bitmap onGetBitmap() { - return mContentBitmap; - } - - public Bitmap getBitmap() { - return mContentBitmap; - } -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/CanvasTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/CanvasTexture.java deleted file mode 100644 index 403d67d02..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/CanvasTexture.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.glrenderer; - -import android.graphics.Bitmap; -import android.graphics.Bitmap.Config; -import android.graphics.Canvas; - -// CanvasTexture is a texture whose content is the drawing on a Canvas. -// The subclasses should override onDraw() to draw on the bitmap. -// By default CanvasTexture is not opaque. -abstract class CanvasTexture extends UploadedTexture { - private final Config mConfig; - - public CanvasTexture(int width, int height) { - mConfig = Config.ARGB_8888; - setSize(width, height); - setOpaque(false); - } - - @Override - protected Bitmap onGetBitmap() { - Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, mConfig); - Canvas canvas = new Canvas(bitmap); - onDraw(canvas, bitmap); - return bitmap; - } - - @Override - protected void onFreeBitmap(Bitmap bitmap) { - if (!inFinalizer()) { - bitmap.recycle(); - } - } - - abstract protected void onDraw(Canvas canvas, Bitmap backing); -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/ColorTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/ColorTexture.java deleted file mode 100644 index 3519a4a89..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/ColorTexture.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.glrenderer; - -import android.graphics.RectF; - -import com.hippo.yorozuya.ColorUtils; - -// ColorTexture is a texture which fills the rectangle with the specified color. -public class ColorTexture implements Texture { - - private final int mColor; - private int mWidth = 0; - private int mHeight = 0; - - public ColorTexture(int color) { - mColor = color; - } - - private boolean isValidSize() { - return mWidth > 0 && mHeight > 0; - } - - @Override - public void draw(GLCanvas canvas, int x, int y) { - if (isValidSize()) { - draw(canvas, x, y, mWidth, mHeight); - } - } - - @Override - public void draw(GLCanvas canvas, int x, int y, int w, int h) { - canvas.fillRect(x, y, w, h, mColor); - } - - @Override - public void draw(GLCanvas canvas, RectF source, RectF target) { - canvas.fillRect(target.left, target.top, target.width(), target.height(), mColor); - } - - @Override - public boolean isOpaque() { - return ColorUtils.isOpaque(mColor); - } - - public void setSize(int width, int height) { - mWidth = width; - mHeight = height; - } - - @Override - public int getWidth() { - return mWidth; - } - - @Override - public int getHeight() { - return mHeight; - } -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/ExtTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/ExtTexture.java deleted file mode 100644 index 69c98874b..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/ExtTexture.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.glrenderer; - -// ExtTexture is a texture whose content comes from a external texture. -// Before drawing, setSize() should be called. -public class ExtTexture extends BasicTexture { - - private final int mTarget; - - public ExtTexture(GLCanvas canvas, int target) { - GLId glId = canvas.getGLId(); - mId = glId.generateTexture(); - mTarget = target; - } - - private void uploadToCanvas(GLCanvas canvas) { - canvas.setTextureParameters(this); - setAssociatedCanvas(canvas); - mState = STATE_LOADED; - } - - @Override - protected boolean onBind(GLCanvas canvas) { - if (!isLoaded()) { - uploadToCanvas(canvas); - } - - return true; - } - - @Override - public int getTarget() { - return mTarget; - } - - @Override - public boolean isOpaque() { - return true; - } - - @Override - public void yield() { - // we cannot free the texture because we have no backup. - } -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/FadeInTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/FadeInTexture.java deleted file mode 100644 index 8802889ce..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/FadeInTexture.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.glrenderer; - - -import android.graphics.RectF; - -// FadeInTexture is a texture which begins with a color, then gradually animates -// into a given texture. -public class FadeInTexture extends FadeTexture implements Texture { - - private final int mColor; - private final TiledTexture mTexture; - - public FadeInTexture(int color, TiledTexture texture) { - super(texture.getWidth(), texture.getHeight(), texture.isOpaque()); - mColor = color; - mTexture = texture; - } - - @Override - public void draw(GLCanvas canvas, int x, int y, int w, int h) { - if (isAnimating()) { - mTexture.drawMixed(canvas, mColor, getRatio(), x, y, w, h); - } else { - mTexture.draw(canvas, x, y, w, h); - } - } - - @Override - public void draw(GLCanvas canvas, RectF source, RectF target) { - if (isAnimating()) { - mTexture.drawMixed(canvas, mColor, getRatio(), source, target); - } else { - mTexture.draw(canvas, source, target); - } - } -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/FadeOutTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/FadeOutTexture.java deleted file mode 100644 index 09c70e662..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/FadeOutTexture.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.glrenderer; - - -import android.graphics.RectF; - -// FadeOutTexture is a texture which begins with a given texture, then gradually animates -// into fading out totally. -public class FadeOutTexture extends FadeTexture { - - private final BasicTexture mTexture; - - public FadeOutTexture(BasicTexture texture) { - super(texture.getWidth(), texture.getHeight(), texture.isOpaque()); - mTexture = texture; - } - - @Override - public void draw(GLCanvas canvas, int x, int y, int w, int h) { - if (isAnimating()) { - canvas.save(GLCanvas.SAVE_FLAG_ALPHA); - canvas.setAlpha(getRatio()); - mTexture.draw(canvas, x, y, w, h); - canvas.restore(); - } - } - - @Override - public void draw(GLCanvas canvas, RectF source, RectF target) { - if (isAnimating()) { - canvas.save(GLCanvas.SAVE_FLAG_ALPHA); - canvas.setAlpha(getRatio()); - mTexture.draw(canvas, source, target); - canvas.restore(); - } - } -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/FadeTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/FadeTexture.java deleted file mode 100644 index c8783343f..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/FadeTexture.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.glrenderer; - -import com.hippo.glview.view.AnimationTime; -import com.hippo.yorozuya.MathUtils; - -// FadeTexture is a texture which fades the given texture along the time. -public abstract class FadeTexture implements Texture { - - // The duration of the fading animation in milliseconds - public static final int DURATION = 180; - - private final long mStartTime; - private final int mWidth; - private final int mHeight; - private final boolean mIsOpaque; - private boolean mIsAnimating; - - public FadeTexture(int width, int height, boolean opaque) { - mWidth = width; - mHeight = height; - mIsOpaque = opaque; - mStartTime = now(); - mIsAnimating = true; - } - - @Override - public void draw(GLCanvas canvas, int x, int y) { - draw(canvas, x, y, mWidth, mHeight); - } - - @Override - public boolean isOpaque() { - return mIsOpaque; - } - - @Override - public int getWidth() { - return mWidth; - } - - @Override - public int getHeight() { - return mHeight; - } - - public boolean isAnimating() { - if (mIsAnimating) { - if (now() - mStartTime >= DURATION) { - mIsAnimating = false; - } - } - return mIsAnimating; - } - - protected float getRatio() { - float r = (float) (now() - mStartTime) / DURATION; - return MathUtils.clamp(1.0f - r, 0.0f, 1.0f); - } - - private long now() { - return AnimationTime.get(); - } -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/GLCanvas.java b/app/src/main/java/com/hippo/glview/glrenderer/GLCanvas.java deleted file mode 100644 index 56fd025bc..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/GLCanvas.java +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.glrenderer; - -import android.graphics.Bitmap; -import android.graphics.Rect; -import android.graphics.RectF; - -// -// GLCanvas gives a convenient interface to draw using OpenGL. -// -// When a rectangle is specified in this interface, it means the region -// [x, x+width) * [y, y+height) -// -public interface GLCanvas { - - int SAVE_FLAG_ALL = 0xFFFFFFFF; - int SAVE_FLAG_ALPHA = 0x01; - int SAVE_FLAG_MATRIX = 0x02; - - GLId getGLId(); - - // Tells GLCanvas the size of the underlying GL surface. This should be - // called before first drawing and when the size of GL surface is changed. - // This is called by GLRoot and should not be called by the clients - // who only want to draw on the GLCanvas. Both width and height must be - // nonnegative. - void setSize(int width, int height); - - // Clear the drawing buffers. This should only be used by GLRoot. - void clearBuffer(); - - void clearBuffer(float[] argb); - - float getAlpha(); - - // Sets and gets the current alpha, alpha must be in [0, 1]. - void setAlpha(float alpha); - - // (current alpha) = (current alpha) * alpha - void multiplyAlpha(float alpha); - - // Change the current transform matrix. - void translate(float x, float y, float z); - - void translate(float x, float y); - - void scale(float sx, float sy, float sz); - - void rotate(float angle, float x, float y, float z); - - void multiplyMatrix(float[] mMatrix, int offset); - - // Pushes the configuration state (matrix, and alpha) onto - // a private stack. - void save(); - - // Same as save(), but only save those specified in saveFlags. - void save(int saveFlags); - - // Pops from the top of the stack as current configuration state (matrix, - // alpha, and clip). This call balances a previous call to save(), and is - // used to remove all modifications to the configuration state since the - // last save call. - void restore(); - - // Draws a line using the specified paint from (x1, y1) to (x2, y2). - // (Both end points are included). - void drawLine(float x1, float y1, float x2, float y2, GLPaint paint); - - // Draws a rectangle using the specified paint from (x1, y1) to (x2, y2). - // (Both end points are included). - void drawRect(float x1, float y1, float x2, float y2, GLPaint paint); - - // Draws a oval using the specified paint for (cx, cy, radiusX, radiusY) - // (Both end points are included). - void drawOval(float cx, float cy, float radiusX, float radiusY, - GLPaint paint); - - // Draw a arc inside a rect - void drawArc(float cx, float cy, float radiusX, float radiusY, - float sweepAngle, GLPaint paint); - - // Fills the specified rectangle with the specified color. - void fillRect(float x, float y, float width, float height, int color); - - // Fills the specified oval with the specified color. - void fillOval(float cx, float cy, float radiusX, float radiusY, int color); - - // Fills the specified sector with the specified color. - void fillSector(float cx, float cy, float radiusX, float radiusY, float sweepAngle, int color); - - // Draws a texture to the specified rectangle. - void drawTexture(BasicTexture texture, int x, int y, int width, int height); - - void drawMesh(BasicTexture tex, int x, int y, int xyBuffer, int uvBuffer, int indexBuffer, int indexCount); - - // Draws the source rectangle part of the texture to the target rectangle. - void drawTexture(BasicTexture texture, RectF source, RectF target); - - // Draw a texture with a specified texture transform. - void drawTexture(BasicTexture texture, float[] mTextureTransform, int x, int y, int w, int h); - - // Draw two textures to the specified rectangle. The actual texture used is - // from * (1 - ratio) + to * ratio - // The two textures must have the same size. - void drawMixed(BasicTexture from, int toColor, float ratio, int x, int y, int w, int h); - - // Draw a region of a texture and a specified color to the specified - // rectangle. The actual color used is from * (1 - ratio) + to * ratio. - // The region of the texture is defined by parameter "src". The target - // rectangle is specified by parameter "target". - void drawMixed(BasicTexture from, int toColor, float ratio, RectF src, RectF target); - - // Unloads the specified texture from the canvas. The resource allocated - // to draw the texture will be released. The specified texture will return - // to the unloaded state. This function should be called only from - // BasicTexture or its descendant - boolean unloadTexture(BasicTexture texture); - - // Delete the specified buffer object, similar to unloadTexture. - void deleteBuffer(int bufferId); - - // Delete the textures and buffers in GL side. This function should only be - // called in the GL thread. - void deleteRecycledResources(); - - // Dump statistics information and clear the counters. For debug only. - void dumpStatisticsAndClear(); - - void beginRenderTarget(RawTexture texture); - - void endRenderTarget(); - - /** - * Sets texture parameters to use GL_CLAMP_TO_EDGE for both - * GL_TEXTURE_WRAP_S and GL_TEXTURE_WRAP_T. Sets texture parameters to be - * GL_LINEAR for GL_TEXTURE_MIN_FILTER and GL_TEXTURE_MAG_FILTER. - * bindTexture() must be called prior to this. - * - * @param texture The texture to set parameters on. - */ - void setTextureParameters(BasicTexture texture); - - /** - * Initializes the texture to a size by calling texImage2D on it. - * - * @param texture The texture to initialize the size. - * @param format The texture format (e.g. GL_RGBA) - * @param type The texture type (e.g. GL_UNSIGNED_BYTE) - */ - void initializeTextureSize(BasicTexture texture, int format, int type); - - /** - * Initializes the texture to a size by calling texImage2D on it. - * - * @param texture The texture to initialize the size. - * @param bitmap The bitmap to initialize the bitmap with. - */ - void initializeTexture(BasicTexture texture, Bitmap bitmap); - - /** - * Calls glTexSubImage2D to upload a bitmap to the texture. - * - * @param texture The target texture to write to. - * @param xOffset Specifies a texel offset in the x direction within the - * texture array. - * @param yOffset Specifies a texel offset in the y direction within the - * texture array. - * @param format The texture format (e.g. GL_RGBA) - * @param type The texture type (e.g. GL_UNSIGNED_BYTE) - */ - void texSubImage2D(BasicTexture texture, int xOffset, int yOffset, Bitmap bitmap, int format, int type); - - /** - * Generates buffers and uploads the buffer data. - * - * @param buffer The buffer to upload - * @return The buffer ID that was generated. - */ - int uploadBuffer(java.nio.FloatBuffer buffer); - - /** - * Generates buffers and uploads the element array buffer data. - * - * @param buffer The buffer to upload - * @return The buffer ID that was generated. - */ - int uploadBuffer(java.nio.ByteBuffer buffer); - - /** - * After LightCycle makes GL calls, this method is called to restore the GL - * configuration to the one expected by GLCanvas. - */ - void recoverFromLightCycle(); - - /** - * Gets the bounds given by x, y, width, and height as well as the internal - * matrix state. There is no special handling for non-90-degree rotations. - * It only considers the lower-left and upper-right corners as the bounds. - * - * @param bounds The output bounds to write to. - * @param x The left side of the input rectangle. - * @param y The bottom of the input rectangle. - * @param width The width of the input rectangle. - * @param height The height of the input rectangle. - */ - void getBounds(Rect bounds, int x, int y, int width, int height); -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/GLES11Canvas.java b/app/src/main/java/com/hippo/glview/glrenderer/GLES11Canvas.java deleted file mode 100644 index 55c9ffcc9..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/GLES11Canvas.java +++ /dev/null @@ -1,1071 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.glrenderer; - -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.Rect; -import android.graphics.RectF; -import android.opengl.GLU; -import android.opengl.GLUtils; -import android.opengl.Matrix; -import android.util.Log; - -import com.hippo.yorozuya.MathUtils; -import com.hippo.yorozuya.collect.IntList; - -import java.nio.Buffer; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.FloatBuffer; -import java.util.ArrayList; - -import javax.microedition.khronos.opengles.GL10; -import javax.microedition.khronos.opengles.GL11; -import javax.microedition.khronos.opengles.GL11Ext; -import javax.microedition.khronos.opengles.GL11ExtensionPack; - -public class GLES11Canvas implements GLCanvas { - @SuppressWarnings("unused") - private static final String TAG = "GLCanvasImp"; - - private static final float OPAQUE_ALPHA = 0.95f; - - private static final int COUNT_FILL_VERTEX = 4; - private static final int COUNT_LINE_VERTEX = 2; - private static final int COUNT_RECT_VERTEX = 4; - private static final int COUNT_CIRCLE_VERTEX = 120; // multiple of 4 - private static final int OFFSET_FILL_RECT = 0; - private static final int OFFSET_DRAW_LINE = OFFSET_FILL_RECT + COUNT_FILL_VERTEX; - private static final int OFFSET_DRAW_RECT = OFFSET_DRAW_LINE + COUNT_LINE_VERTEX; - private static final int OFFSET_FILL_CIRCLE = OFFSET_DRAW_RECT + COUNT_RECT_VERTEX; - private static final int OFFSET_DRAW_CIRCLE = OFFSET_FILL_CIRCLE + 1; - private static final int OFFSET_LAST = OFFSET_DRAW_CIRCLE + COUNT_CIRCLE_VERTEX + 1; - - private static final float[] BOX_COORDINATES = new float[OFFSET_LAST * 2]; - // TODO: the code only work for 2D should get fixed for 3D or removed - private static final int MSKEW_X = 4; - private static final int MSKEW_Y = 1; - private static final int MSCALE_X = 0; - private static final int MSCALE_Y = 5; - private static final float[] sCropRect = new float[4]; - private static final GLId mGLId = new GLES11IdImpl(); - - static { - float[] temp = { - 0, 0, // Fill rectangle - 1, 0, - 0, 1, - 1, 1, - 0, 0, // Draw line - 1, 1, - 0, 0, // Draw rectangle outline - 0, 1, - 1, 1, - 1, 0, - 0.5f, 0.5f // Fill circle - }; - System.arraycopy(temp, 0, BOX_COORDINATES, 0, temp.length); - - // Draw circle - int arrayOffset = OFFSET_DRAW_CIRCLE * 2; - for (int i = 0, n = COUNT_CIRCLE_VERTEX / 4; i <= n; i++) { - float value = (float) Math.sin(MathUtils.radians(90f / n * i)) / 2; - float positive = value + 0.5f; - float negative = -value + 0.5f; - BOX_COORDINATES[arrayOffset + i * 2 + 1] = positive; - BOX_COORDINATES[arrayOffset + n * 2 - i * 2] = positive; - BOX_COORDINATES[arrayOffset + n * 2 + i * 2] = negative; - BOX_COORDINATES[arrayOffset + n * 4 - i * 2 + 1] = positive; - BOX_COORDINATES[arrayOffset + n * 4 + i * 2 + 1] = negative; - BOX_COORDINATES[arrayOffset + n * 6 - i * 2] = negative; - BOX_COORDINATES[arrayOffset + n * 6 + i * 2] = positive; - BOX_COORDINATES[arrayOffset + n * 8 - i * 2 + 1] = negative; - } - } - - private final float[] mMatrixValues = new float[16]; - private final float[] mTextureMatrixValues = new float[16]; - // The results of mapPoints are stored in this buffer, and the order is - // x1, y1, x2, y2. - private final float[] mMapPointsBuffer = new float[4]; - private final float[] mTextureColor = new float[4]; - private final ArrayList mTargetStack = new ArrayList(); - private final ArrayList mRestoreStack = new ArrayList(); - private final RectF mDrawTextureSourceRect = new RectF(); - private final RectF mDrawTextureTargetRect = new RectF(); - private final float[] mTempMatrix = new float[32]; - private final IntList mUnboundTextures = new IntList(); - private final IntList mDeleteBuffers = new IntList(); - private final GL11 mGL; - private final int mBoxCoords; - private final GLState mGLState; - private final boolean mBlendEnabled = true; - private final int[] mFrameBuffer = new int[1]; - // Drawing statistics - int mCountDrawLine; - int mCountFillRect; - int mCountDrawMesh; - int mCountTextureRect; - int mCountTextureOES; - private float mAlpha; - private ConfigState mRecycledRestoreAction; - private int mScreenWidth; - private int mScreenHeight; - private RawTexture mTargetTexture; - - public GLES11Canvas(GL11 gl) { - mGL = gl; - mGLState = new GLState(gl); - // First create an nio buffer, then create a VBO from it. - int size = BOX_COORDINATES.length * Float.SIZE / Byte.SIZE; - FloatBuffer xyBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer(); - xyBuffer.put(BOX_COORDINATES, 0, BOX_COORDINATES.length).position(0); - - int[] name = new int[1]; - mGLId.glGenBuffers(1, name, 0); - mBoxCoords = name[0]; - - gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords); - gl.glBufferData(GL11.GL_ARRAY_BUFFER, xyBuffer.capacity() * (Float.SIZE / Byte.SIZE), - xyBuffer, GL11.GL_STATIC_DRAW); - - gl.glVertexPointer(2, GL11.GL_FLOAT, 0, 0); - gl.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0); - - // Enable the texture coordinate array for Texture 1 - gl.glClientActiveTexture(GL11.GL_TEXTURE1); - gl.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0); - gl.glClientActiveTexture(GL11.GL_TEXTURE0); - gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); - - // mMatrixValues and mAlpha will be initialized in setSize() - } - - private static ByteBuffer allocateDirectNativeOrderBuffer(int size) { - return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder()); - } - - // This function changes the source coordinate to the texture coordinates. - // It also clips the source and target coordinates if it is beyond the - // bound of the texture. - private static void convertCoordinate(RectF source, RectF target, - BasicTexture texture) { - - int width = texture.getWidth(); - int height = texture.getHeight(); - int texWidth = texture.getTextureWidth(); - int texHeight = texture.getTextureHeight(); - // Convert to texture coordinates - source.left /= texWidth; - source.right /= texWidth; - source.top /= texHeight; - source.bottom /= texHeight; - - // Clip if the rendering range is beyond the bound of the texture. - float xBound = (float) width / texWidth; - if (source.right > xBound) { - target.right = target.left + target.width() * - (xBound - source.left) / source.width(); - source.right = xBound; - } - float yBound = (float) height / texHeight; - if (source.bottom > yBound) { - target.bottom = target.top + target.height() * - (yBound - source.top) / source.height(); - source.bottom = yBound; - } - } - - private static boolean isMatrixRotatedOrFlipped(float[] matrix) { - final float eps = 1e-5f; - return Math.abs(matrix[MSKEW_X]) > eps - || Math.abs(matrix[MSKEW_Y]) > eps - || matrix[MSCALE_X] < -eps - || matrix[MSCALE_Y] > eps; - } - - private static void checkFramebufferStatus(GL11ExtensionPack gl11ep) { - int status = gl11ep.glCheckFramebufferStatusOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES); - if (status != GL11ExtensionPack.GL_FRAMEBUFFER_COMPLETE_OES) { - String msg = ""; - switch (status) { - case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_FORMATS_OES: - msg = "FRAMEBUFFER_FORMATS"; - break; - case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_OES: - msg = "FRAMEBUFFER_ATTACHMENT"; - break; - case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_OES: - msg = "FRAMEBUFFER_MISSING_ATTACHMENT"; - break; - case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_OES: - msg = "FRAMEBUFFER_DRAW_BUFFER"; - break; - case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_OES: - msg = "FRAMEBUFFER_READ_BUFFER"; - break; - case GL11ExtensionPack.GL_FRAMEBUFFER_UNSUPPORTED_OES: - msg = "FRAMEBUFFER_UNSUPPORTED"; - break; - case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_OES: - msg = "FRAMEBUFFER_INCOMPLETE_DIMENSIONS"; - break; - } - throw new RuntimeException(msg + ":" + Integer.toHexString(status)); - } - } - - @Override - public void setSize(int width, int height) { - if (mTargetTexture == null) { - mScreenWidth = width; - mScreenHeight = height; - } - mAlpha = 1.0f; - - GL11 gl = mGL; - gl.glViewport(0, 0, width, height); - gl.glMatrixMode(GL11.GL_PROJECTION); - gl.glLoadIdentity(); - GLU.gluOrtho2D(gl, 0, width, 0, height); - - gl.glMatrixMode(GL11.GL_MODELVIEW); - gl.glLoadIdentity(); - - float[] matrix = mMatrixValues; - Matrix.setIdentityM(matrix, 0); - // to match the graphic coordinate system in android, we flip it vertically. - if (mTargetTexture == null) { - Matrix.translateM(matrix, 0, 0, height, 0); - Matrix.scaleM(matrix, 0, 1, -1, 1); - } - } - - @Override - public float getAlpha() { - return mAlpha; - } - - @Override - public void setAlpha(float alpha) { - mAlpha = alpha; - } - - @Override - public void multiplyAlpha(float alpha) { - mAlpha *= alpha; - } - - @Override - public void drawRect(float x, float y, float width, float height, GLPaint paint) { - GL11 gl = mGL; - - mGLState.setColorMode(paint.getColor(), mAlpha); - mGLState.setLineWidth(paint.getLineWidth()); - - saveTransform(); - translate(x, y); - scale(width, height, 1); - - gl.glLoadMatrixf(mMatrixValues, 0); - gl.glDrawArrays(GL11.GL_LINE_LOOP, OFFSET_DRAW_RECT, COUNT_RECT_VERTEX); - - restoreTransform(); - mCountDrawLine++; - } - - @Override - public void drawLine(float x1, float y1, float x2, float y2, GLPaint paint) { - GL11 gl = mGL; - - mGLState.setColorMode(paint.getColor(), mAlpha); - mGLState.setLineWidth(paint.getLineWidth()); - - saveTransform(); - translate(x1, y1); - scale(x2 - x1, y2 - y1, 1); - - gl.glLoadMatrixf(mMatrixValues, 0); - gl.glDrawArrays(GL11.GL_LINE_STRIP, OFFSET_DRAW_LINE, COUNT_LINE_VERTEX); - - restoreTransform(); - mCountDrawLine++; - } - - @Override - public void drawOval(float cx, float cy, float radiusX, float radiusY, GLPaint paint) { - float halfLineWidth = paint.getLineWidth() / 2; - fillOval(cx, cy, radiusX + halfLineWidth, radiusY + halfLineWidth, paint.getColor()); - fillOval(cx, cy, radiusX - halfLineWidth, radiusY - halfLineWidth, paint.getBackgroundColor()); - } - - @Override - public void drawArc(float cx, float cy, float radiusX, float radiusY, float sweepAngle, GLPaint paint) { - float halfLineWidth = paint.getLineWidth() / 2; - fillSector(cx, cy, radiusX + halfLineWidth, radiusY + halfLineWidth, sweepAngle, paint.getColor()); - fillOval(cx, cy, radiusX - halfLineWidth, radiusY - halfLineWidth, paint.getBackgroundColor()); - } - - @Override - public void fillRect(float x, float y, float width, float height, int color) { - mGLState.setColorMode(color, mAlpha); - GL11 gl = mGL; - - saveTransform(); - translate(x, y); - scale(width, height, 1); - - gl.glLoadMatrixf(mMatrixValues, 0); - gl.glDrawArrays(GL11.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, COUNT_FILL_VERTEX); - - restoreTransform(); - mCountFillRect++; - } - - @Override - public void fillOval(float cx, float cy, float radiusX, float radiusY, int color) { - mGLState.setColorMode(color, mAlpha); - GL11 gl = mGL; - - saveTransform(); - translate(cx - radiusX, cy - radiusY); - scale(radiusX * 2, radiusY * 2, 1); - - gl.glLoadMatrixf(mMatrixValues, 0); - gl.glDrawArrays(GL11.GL_TRIANGLE_FAN, OFFSET_FILL_CIRCLE, OFFSET_LAST - OFFSET_FILL_CIRCLE); - - restoreTransform(); - mCountFillRect++; - } - - @Override - public void fillSector(float cx, float cy, float radiusX, float radiusY, float sweepAngle, int color) { - float conjugateAngle = Math.abs(360 - MathUtils.positiveModulo(sweepAngle, 360)); - int conjugateCount = Math.round(COUNT_CIRCLE_VERTEX * conjugateAngle / 360); - if (conjugateCount == 0) { - // It is a circle - fillOval(cx, cy, radiusX, radiusY, color); - } else if (conjugateCount < COUNT_CIRCLE_VERTEX) { - mGLState.setColorMode(color, mAlpha); - GL11 gl = mGL; - - saveTransform(); - translate(cx - radiusX, cy - radiusY); - scale(radiusX * 2, radiusY * 2, 1); - - gl.glLoadMatrixf(mMatrixValues, 0); - gl.glDrawArrays(GL11.GL_TRIANGLE_FAN, OFFSET_FILL_CIRCLE, OFFSET_LAST - OFFSET_FILL_CIRCLE - conjugateCount); - - restoreTransform(); - mCountFillRect++; - } - } - - @Override - public void translate(float x, float y, float z) { - Matrix.translateM(mMatrixValues, 0, x, y, z); - } - - // This is a faster version of translate(x, y, z) because - // (1) we knows z = 0, (2) we inline the Matrix.translateM call, - // (3) we unroll the loop - @Override - public void translate(float x, float y) { - float[] m = mMatrixValues; - m[12] += m[0] * x + m[4] * y; - m[13] += m[1] * x + m[5] * y; - m[14] += m[2] * x + m[6] * y; - m[15] += m[3] * x + m[7] * y; - } - - @Override - public void scale(float sx, float sy, float sz) { - Matrix.scaleM(mMatrixValues, 0, sx, sy, sz); - } - - @Override - public void rotate(float angle, float x, float y, float z) { - if (angle == 0) return; - float[] temp = mTempMatrix; - Matrix.setRotateM(temp, 0, angle, x, y, z); - Matrix.multiplyMM(temp, 16, mMatrixValues, 0, temp, 0); - System.arraycopy(temp, 16, mMatrixValues, 0, 16); - } - - @Override - public void multiplyMatrix(float[] matrix, int offset) { - float[] temp = mTempMatrix; - Matrix.multiplyMM(temp, 0, mMatrixValues, 0, matrix, offset); - System.arraycopy(temp, 0, mMatrixValues, 0, 16); - } - - private void textureRect(float x, float y, float width, float height) { - GL11 gl = mGL; - - saveTransform(); - translate(x, y); - scale(width, height, 1); - - gl.glLoadMatrixf(mMatrixValues, 0); - gl.glDrawArrays(GL11.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, 4); - - restoreTransform(); - mCountTextureRect++; - } - - @Override - public void drawMesh(BasicTexture tex, int x, int y, int xyBuffer, - int uvBuffer, int indexBuffer, int indexCount) { - float alpha = mAlpha; - if (!bindTexture(tex)) return; - - mGLState.setBlendEnabled(mBlendEnabled - && (!tex.isOpaque() || alpha < OPAQUE_ALPHA)); - mGLState.setTextureAlpha(alpha); - - // Reset the texture matrix. We will set our own texture coordinates - // below. - setTextureCoords(0, 0, 1, 1); - - saveTransform(); - translate(x, y); - - mGL.glLoadMatrixf(mMatrixValues, 0); - - mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, xyBuffer); - mGL.glVertexPointer(2, GL11.GL_FLOAT, 0, 0); - - mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, uvBuffer); - mGL.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0); - - mGL.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, indexBuffer); - mGL.glDrawElements(GL11.GL_TRIANGLE_STRIP, - indexCount, GL11.GL_UNSIGNED_BYTE, 0); - - mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords); - mGL.glVertexPointer(2, GL11.GL_FLOAT, 0, 0); - mGL.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0); - - restoreTransform(); - mCountDrawMesh++; - } - - // Transforms two points by the given matrix m. The result - // {x1', y1', x2', y2'} are stored in mMapPointsBuffer and also returned. - private float[] mapPoints(float[] m, int x1, int y1, int x2, int y2) { - float[] r = mMapPointsBuffer; - - // Multiply m and (x1 y1 0 1) to produce (x3 y3 z3 w3). z3 is unused. - float x3 = m[0] * x1 + m[4] * y1 + m[12]; - float y3 = m[1] * x1 + m[5] * y1 + m[13]; - float w3 = m[3] * x1 + m[7] * y1 + m[15]; - r[0] = x3 / w3; - r[1] = y3 / w3; - - // Same for x2 y2. - float x4 = m[0] * x2 + m[4] * y2 + m[12]; - float y4 = m[1] * x2 + m[5] * y2 + m[13]; - float w4 = m[3] * x2 + m[7] * y2 + m[15]; - r[2] = x4 / w4; - r[3] = y4 / w4; - - return r; - } - - private void drawBoundTexture( - BasicTexture texture, int x, int y, int width, int height) { - // Test whether it has been rotated or flipped, if so, glDrawTexiOES - // won't work - if (isMatrixRotatedOrFlipped(mMatrixValues)) { - if (texture.hasBorder()) { - setTextureCoords( - 1.0f / texture.getTextureWidth(), - 1.0f / texture.getTextureHeight(), - (texture.getWidth() - 1.0f) / texture.getTextureWidth(), - (texture.getHeight() - 1.0f) / texture.getTextureHeight()); - } else { - setTextureCoords(0, 0, - (float) texture.getWidth() / texture.getTextureWidth(), - (float) texture.getHeight() / texture.getTextureHeight()); - } - textureRect(x, y, width, height); - } else { - // draw the rect from bottom-left to top-right - float[] points = mapPoints( - mMatrixValues, x, y + height, x + width, y); - x = (int) (points[0] + 0.5f); - y = (int) (points[1] + 0.5f); - width = (int) (points[2] + 0.5f) - x; - height = (int) (points[3] + 0.5f) - y; - if (width > 0 && height > 0) { - ((GL11Ext) mGL).glDrawTexiOES(x, y, 0, width, height); - mCountTextureOES++; - } - } - } - - @Override - public void drawTexture( - BasicTexture texture, int x, int y, int width, int height) { - drawTexture(texture, x, y, width, height, mAlpha); - } - - private void drawTexture(BasicTexture texture, - int x, int y, int width, int height, float alpha) { - if (width <= 0 || height <= 0) return; - - mGLState.setBlendEnabled(mBlendEnabled - && (!texture.isOpaque() || alpha < OPAQUE_ALPHA)); - if (!bindTexture(texture)) return; - mGLState.setTextureAlpha(alpha); - drawBoundTexture(texture, x, y, width, height); - } - - @Override - public void drawTexture(BasicTexture texture, RectF source, RectF target) { - if (target.width() <= 0 || target.height() <= 0) return; - - // Copy the input to avoid changing it. - mDrawTextureSourceRect.set(source); - mDrawTextureTargetRect.set(target); - source = mDrawTextureSourceRect; - target = mDrawTextureTargetRect; - - mGLState.setBlendEnabled(mBlendEnabled - && (!texture.isOpaque() || mAlpha < OPAQUE_ALPHA)); - if (!bindTexture(texture)) return; - convertCoordinate(source, target, texture); - setTextureCoords(source); - mGLState.setTextureAlpha(mAlpha); - textureRect(target.left, target.top, target.width(), target.height()); - } - - @Override - public void drawTexture(BasicTexture texture, float[] mTextureTransform, - int x, int y, int w, int h) { - mGLState.setBlendEnabled(mBlendEnabled - && (!texture.isOpaque() || mAlpha < OPAQUE_ALPHA)); - if (!bindTexture(texture)) return; - setTextureCoords(mTextureTransform); - mGLState.setTextureAlpha(mAlpha); - textureRect(x, y, w, h); - } - - @Override - public void drawMixed(BasicTexture from, - int toColor, float ratio, int x, int y, int w, int h) { - drawMixed(from, toColor, ratio, x, y, w, h, mAlpha); - } - - private boolean bindTexture(BasicTexture texture) { - if (!texture.onBind(this)) return false; - int target = texture.getTarget(); - mGLState.setTextureTarget(target); - mGL.glBindTexture(target, texture.getId()); - return true; - } - - private void setTextureColor(float r, float g, float b, float alpha) { - float[] color = mTextureColor; - color[0] = r; - color[1] = g; - color[2] = b; - color[3] = alpha; - } - - private void setMixedColor(int toColor, float ratio, float alpha) { - // - // The formula we want: - // alpha * ((1 - ratio) * from + ratio * to) - // - // The formula that GL supports is in the form of: - // combo * from + (1 - combo) * to * scale - // - // So, we have combo = alpha * (1 - ratio) - // and scale = alpha * ratio / (1 - combo) - // - float combo = alpha * (1 - ratio); - float scale = alpha * ratio / (1 - combo); - - // Specify the interpolation factor via the alpha component of - // GL_TEXTURE_ENV_COLORs. - // RGB component are get from toColor and will used as SRC1 - float colorScale = scale * (toColor >>> 24) / (0xff * 0xff); - setTextureColor(((toColor >>> 16) & 0xff) * colorScale, - ((toColor >>> 8) & 0xff) * colorScale, - (toColor & 0xff) * colorScale, combo); - GL11 gl = mGL; - gl.glTexEnvfv(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_COLOR, mTextureColor, 0); - - gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_RGB, GL11.GL_INTERPOLATE); - gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_ALPHA, GL11.GL_INTERPOLATE); - gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_RGB, GL11.GL_CONSTANT); - gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_RGB, GL11.GL_SRC_COLOR); - gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_ALPHA, GL11.GL_CONSTANT); - gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_ALPHA, GL11.GL_SRC_ALPHA); - - // Wire up the interpolation factor for RGB. - gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_RGB, GL11.GL_CONSTANT); - gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_RGB, GL11.GL_SRC_ALPHA); - - // Wire up the interpolation factor for alpha. - gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_ALPHA, GL11.GL_CONSTANT); - gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_ALPHA, GL11.GL_SRC_ALPHA); - - } - - @Override - public void drawMixed(BasicTexture from, int toColor, float ratio, - RectF source, RectF target) { - if (target.width() <= 0 || target.height() <= 0) return; - - if (ratio <= 0.01f) { - drawTexture(from, source, target); - return; - } else if (ratio >= 1) { - fillRect(target.left, target.top, target.width(), target.height(), toColor); - return; - } - - float alpha = mAlpha; - - // Copy the input to avoid changing it. - mDrawTextureSourceRect.set(source); - mDrawTextureTargetRect.set(target); - source = mDrawTextureSourceRect; - target = mDrawTextureTargetRect; - - mGLState.setBlendEnabled(mBlendEnabled && (!from.isOpaque() - || Color.alpha(toColor) != 255 || alpha < OPAQUE_ALPHA)); - - if (!bindTexture(from)) return; - - // Interpolate the RGB and alpha values between both textures. - mGLState.setTexEnvMode(GL11.GL_COMBINE); - setMixedColor(toColor, ratio, alpha); - convertCoordinate(source, target, from); - setTextureCoords(source); - textureRect(target.left, target.top, target.width(), target.height()); - mGLState.setTexEnvMode(GL11.GL_REPLACE); - } - - private void drawMixed(BasicTexture from, int toColor, - float ratio, int x, int y, int width, int height, float alpha) { - // change from 0 to 0.01f to prevent getting divided by zero below - if (ratio <= 0.01f) { - drawTexture(from, x, y, width, height, alpha); - return; - } else if (ratio >= 1) { - fillRect(x, y, width, height, toColor); - return; - } - - mGLState.setBlendEnabled(mBlendEnabled && (!from.isOpaque() - || Color.alpha(toColor) != 255 || alpha < OPAQUE_ALPHA)); - - final GL11 gl = mGL; - if (!bindTexture(from)) return; - - // Interpolate the RGB and alpha values between both textures. - mGLState.setTexEnvMode(GL11.GL_COMBINE); - setMixedColor(toColor, ratio, alpha); - - drawBoundTexture(from, x, y, width, height); - mGLState.setTexEnvMode(GL11.GL_REPLACE); - } - - @Override - public void clearBuffer(float[] argb) { - if (argb != null && argb.length == 4) { - mGL.glClearColor(argb[1], argb[2], argb[3], argb[0]); - } else { - mGL.glClearColor(0, 0, 0, 1); - } - mGL.glClear(GL10.GL_COLOR_BUFFER_BIT); - } - - @Override - public void clearBuffer() { - clearBuffer(null); - } - - private void setTextureCoords(RectF source) { - setTextureCoords(source.left, source.top, source.right, source.bottom); - } - - private void setTextureCoords(float left, float top, - float right, float bottom) { - mGL.glMatrixMode(GL11.GL_TEXTURE); - mTextureMatrixValues[0] = right - left; - mTextureMatrixValues[5] = bottom - top; - mTextureMatrixValues[10] = 1; - mTextureMatrixValues[12] = left; - mTextureMatrixValues[13] = top; - mTextureMatrixValues[15] = 1; - mGL.glLoadMatrixf(mTextureMatrixValues, 0); - mGL.glMatrixMode(GL11.GL_MODELVIEW); - } - - private void setTextureCoords(float[] mTextureTransform) { - mGL.glMatrixMode(GL11.GL_TEXTURE); - mGL.glLoadMatrixf(mTextureTransform, 0); - mGL.glMatrixMode(GL11.GL_MODELVIEW); - } - - // unloadTexture and deleteBuffer can be called from the finalizer thread, - // so we synchronized on the mUnboundTextures object. - @Override - public boolean unloadTexture(BasicTexture t) { - synchronized (mUnboundTextures) { - if (!t.isLoaded()) return false; - mUnboundTextures.add(t.mId); - return true; - } - } - - @Override - public void deleteBuffer(int bufferId) { - synchronized (mUnboundTextures) { - mDeleteBuffers.add(bufferId); - } - } - - @Override - public void deleteRecycledResources() { - synchronized (mUnboundTextures) { - IntList ids = mUnboundTextures; - if (ids.size() > 0) { - mGLId.glDeleteTextures(mGL, ids.size(), ids.getInternalArray(), 0); - ids.clear(); - } - - ids = mDeleteBuffers; - if (ids.size() > 0) { - mGLId.glDeleteBuffers(mGL, ids.size(), ids.getInternalArray(), 0); - ids.clear(); - } - } - } - - @Override - public void save() { - save(SAVE_FLAG_ALL); - } - - @Override - public void save(int saveFlags) { - ConfigState config = obtainRestoreConfig(); - - if ((saveFlags & SAVE_FLAG_ALPHA) != 0) { - config.mAlpha = mAlpha; - } else { - config.mAlpha = -1; - } - - if ((saveFlags & SAVE_FLAG_MATRIX) != 0) { - System.arraycopy(mMatrixValues, 0, config.mMatrix, 0, 16); - } else { - config.mMatrix[0] = Float.NEGATIVE_INFINITY; - } - - mRestoreStack.add(config); - } - - @Override - public void restore() { - if (mRestoreStack.isEmpty()) throw new IllegalStateException(); - ConfigState config = mRestoreStack.remove(mRestoreStack.size() - 1); - config.restore(this); - freeRestoreConfig(config); - } - - private void freeRestoreConfig(ConfigState action) { - action.mNextFree = mRecycledRestoreAction; - mRecycledRestoreAction = action; - } - - private ConfigState obtainRestoreConfig() { - if (mRecycledRestoreAction != null) { - ConfigState result = mRecycledRestoreAction; - mRecycledRestoreAction = result.mNextFree; - return result; - } - return new ConfigState(); - } - - @Override - public void dumpStatisticsAndClear() { - String line = String.format( - "MESH:%d, TEX_OES:%d, TEX_RECT:%d, FILL_RECT:%d, LINE:%d", - mCountDrawMesh, mCountTextureRect, mCountTextureOES, - mCountFillRect, mCountDrawLine); - mCountDrawMesh = 0; - mCountTextureRect = 0; - mCountTextureOES = 0; - mCountFillRect = 0; - mCountDrawLine = 0; - Log.d(TAG, line); - } - - private void saveTransform() { - System.arraycopy(mMatrixValues, 0, mTempMatrix, 0, 16); - } - - private void restoreTransform() { - System.arraycopy(mTempMatrix, 0, mMatrixValues, 0, 16); - } - - private void setRenderTarget(RawTexture texture) { - GL11ExtensionPack gl11ep = (GL11ExtensionPack) mGL; - - if (mTargetTexture == null && texture != null) { - mGLId.glGenBuffers(1, mFrameBuffer, 0); - gl11ep.glBindFramebufferOES( - GL11ExtensionPack.GL_FRAMEBUFFER_OES, mFrameBuffer[0]); - } - if (mTargetTexture != null && texture == null) { - gl11ep.glBindFramebufferOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES, 0); - gl11ep.glDeleteFramebuffersOES(1, mFrameBuffer, 0); - } - - mTargetTexture = texture; - if (texture == null) { - setSize(mScreenWidth, mScreenHeight); - } else { - setSize(texture.getWidth(), texture.getHeight()); - - if (!texture.isLoaded()) texture.prepare(this); - - gl11ep.glFramebufferTexture2DOES( - GL11ExtensionPack.GL_FRAMEBUFFER_OES, - GL11ExtensionPack.GL_COLOR_ATTACHMENT0_OES, - GL11.GL_TEXTURE_2D, texture.getId(), 0); - - checkFramebufferStatus(gl11ep); - } - } - - @Override - public void endRenderTarget() { - RawTexture texture = mTargetStack.remove(mTargetStack.size() - 1); - setRenderTarget(texture); - restore(); // restore matrix and alpha - } - - @Override - public void beginRenderTarget(RawTexture texture) { - save(); // save matrix and alpha - mTargetStack.add(mTargetTexture); - setRenderTarget(texture); - } - - @Override - public void setTextureParameters(BasicTexture texture) { - int width = texture.getWidth(); - int height = texture.getHeight(); - // Define a vertically flipped crop rectangle for OES_draw_texture. - // The four values in sCropRect are: left, bottom, width, and - // height. Negative value of width or height means flip. - sCropRect[0] = 0; - sCropRect[1] = height; - sCropRect[2] = width; - sCropRect[3] = -height; - - // Set texture parameters. - int target = texture.getTarget(); - mGL.glBindTexture(target, texture.getId()); - mGL.glTexParameterfv(target, GL11Ext.GL_TEXTURE_CROP_RECT_OES, sCropRect, 0); - mGL.glTexParameteri(target, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP_TO_EDGE); - mGL.glTexParameteri(target, GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP_TO_EDGE); - mGL.glTexParameterf(target, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); - mGL.glTexParameterf(target, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); - } - - @Override - public void initializeTextureSize(BasicTexture texture, int format, int type) { - int target = texture.getTarget(); - mGL.glBindTexture(target, texture.getId()); - int width = texture.getTextureWidth(); - int height = texture.getTextureHeight(); - mGL.glTexImage2D(target, 0, format, width, height, 0, format, type, null); - } - - @Override - public void initializeTexture(BasicTexture texture, Bitmap bitmap) { - int target = texture.getTarget(); - mGL.glBindTexture(target, texture.getId()); - GLUtils.texImage2D(target, 0, bitmap, 0); - } - - @Override - public void texSubImage2D(BasicTexture texture, int xOffset, int yOffset, Bitmap bitmap, - int format, int type) { - int target = texture.getTarget(); - mGL.glBindTexture(target, texture.getId()); - GLUtils.texSubImage2D(target, 0, xOffset, yOffset, bitmap, format, type); - } - - @Override - public int uploadBuffer(FloatBuffer buf) { - return uploadBuffer(buf, Float.SIZE / Byte.SIZE); - } - - @Override - public int uploadBuffer(ByteBuffer buf) { - return uploadBuffer(buf, 1); - } - - private int uploadBuffer(Buffer buf, int elementSize) { - int[] bufferIds = new int[1]; - mGLId.glGenBuffers(bufferIds.length, bufferIds, 0); - int bufferId = bufferIds[0]; - mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, bufferId); - mGL.glBufferData(GL11.GL_ARRAY_BUFFER, buf.capacity() * elementSize, buf, - GL11.GL_STATIC_DRAW); - return bufferId; - } - - @Override - public void recoverFromLightCycle() { - // This is only required for GLES20 - } - - @Override - public void getBounds(Rect bounds, int x, int y, int width, int height) { - // This is only required for GLES20 - } - - @Override - public GLId getGLId() { - return mGLId; - } - - private static class GLState { - - private final GL11 mGL; - private final boolean mLineSmooth = false; - private int mTexEnvMode = GL11.GL_REPLACE; - private float mTextureAlpha = 1.0f; - private int mTextureTarget = GL11.GL_TEXTURE_2D; - private boolean mBlendEnabled = true; - private float mLineWidth = 1.0f; - - public GLState(GL11 gl) { - mGL = gl; - - // Disable unused state - gl.glDisable(GL11.GL_LIGHTING); - - // Enable used features - gl.glEnable(GL11.GL_DITHER); - - gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); - gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); - gl.glEnable(GL11.GL_TEXTURE_2D); - - gl.glTexEnvf(GL11.GL_TEXTURE_ENV, - GL11.GL_TEXTURE_ENV_MODE, GL11.GL_REPLACE); - - // Set the background color - gl.glClearColor(0f, 0f, 0f, 0f); - - gl.glEnable(GL11.GL_BLEND); - gl.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA); - - // We use 565 or 8888 format, so set the alignment to 2 bytes/pixel. - gl.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 2); - } - - public void setTexEnvMode(int mode) { - if (mTexEnvMode == mode) return; - mTexEnvMode = mode; - mGL.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, mode); - } - - public void setLineWidth(float width) { - if (mLineWidth == width) return; - mLineWidth = width; - mGL.glLineWidth(width); - } - - public void setTextureAlpha(float alpha) { - if (mTextureAlpha == alpha) return; - mTextureAlpha = alpha; - if (alpha >= OPAQUE_ALPHA) { - // The alpha is need for those texture without alpha channel - mGL.glColor4f(1, 1, 1, 1); - setTexEnvMode(GL11.GL_REPLACE); - } else { - mGL.glColor4f(alpha, alpha, alpha, alpha); - setTexEnvMode(GL11.GL_MODULATE); - } - } - - public void setColorMode(int color, float alpha) { - setBlendEnabled(Color.alpha(color) != 255 || alpha < OPAQUE_ALPHA); - - // Set mTextureAlpha to an invalid value, so that it will reset - // again in setTextureAlpha(float) later. - mTextureAlpha = -1.0f; - - setTextureTarget(0); - - float prealpha = (color >>> 24) * alpha * 65535f / 255f / 255f; - mGL.glColor4x( - Math.round(((color >> 16) & 0xFF) * prealpha), - Math.round(((color >> 8) & 0xFF) * prealpha), - Math.round((color & 0xFF) * prealpha), - Math.round(255 * prealpha)); - } - - // target is a value like GL_TEXTURE_2D. If target = 0, texturing is disabled. - public void setTextureTarget(int target) { - if (mTextureTarget == target) return; - if (mTextureTarget != 0) { - mGL.glDisable(mTextureTarget); - } - mTextureTarget = target; - if (mTextureTarget != 0) { - mGL.glEnable(mTextureTarget); - } - } - - public void setBlendEnabled(boolean enabled) { - if (mBlendEnabled == enabled) return; - mBlendEnabled = enabled; - if (enabled) { - mGL.glEnable(GL11.GL_BLEND); - } else { - mGL.glDisable(GL11.GL_BLEND); - } - } - } - - private static class ConfigState { - float mAlpha; - float[] mMatrix = new float[16]; - ConfigState mNextFree; - - public void restore(GLES11Canvas canvas) { - if (mAlpha >= 0) canvas.setAlpha(mAlpha); - if (mMatrix[0] != Float.NEGATIVE_INFINITY) { - System.arraycopy(mMatrix, 0, canvas.mMatrixValues, 0, 16); - } - } - } -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/GLES11IdImpl.java b/app/src/main/java/com/hippo/glview/glrenderer/GLES11IdImpl.java deleted file mode 100644 index 06713cdd2..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/GLES11IdImpl.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hippo.glview.glrenderer; - -import javax.microedition.khronos.opengles.GL11; -import javax.microedition.khronos.opengles.GL11ExtensionPack; - -/** - * Open GL ES 1.1 implementation for generating and destroying texture IDs and - * buffer IDs - */ -public class GLES11IdImpl implements GLId { - - // Mutex for sNextId - private final static Object sLock = new Object(); - private static int sNextId = 1; - - @Override - public int generateTexture() { - synchronized (sLock) { - return sNextId++; - } - } - - @Override - public void glGenBuffers(int n, int[] buffers, int offset) { - synchronized (sLock) { - while (n-- > 0) { - buffers[offset + n] = sNextId++; - } - } - } - - @Override - public void glDeleteTextures(GL11 gl, int n, int[] textures, int offset) { - synchronized (sLock) { - gl.glDeleteTextures(n, textures, offset); - } - } - - @Override - public void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset) { - synchronized (sLock) { - gl.glDeleteBuffers(n, buffers, offset); - } - } - - @Override - public void glDeleteFramebuffers(GL11ExtensionPack gl11ep, int n, int[] buffers, int offset) { - synchronized (sLock) { - gl11ep.glDeleteFramebuffersOES(n, buffers, offset); - } - } -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/GLES20Canvas.java b/app/src/main/java/com/hippo/glview/glrenderer/GLES20Canvas.java deleted file mode 100644 index 58c801292..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/GLES20Canvas.java +++ /dev/null @@ -1,1017 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hippo.glview.glrenderer; - -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.Rect; -import android.graphics.RectF; -import android.opengl.GLES20; -import android.opengl.GLUtils; -import android.opengl.Matrix; -import android.util.Log; - -import com.hippo.yorozuya.MathUtils; -import com.hippo.yorozuya.collect.IntList; - -import java.nio.Buffer; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.FloatBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Locale; - -public class GLES20Canvas implements GLCanvas { - // ************** Constants ********************** - private static final String TAG = GLES20Canvas.class.getSimpleName(); - private static final int FLOAT_SIZE = Float.SIZE / Byte.SIZE; - private static final float OPAQUE_ALPHA = 0.95f; - - private static final int COORDS_PER_VERTEX = 2; - private static final int VERTEX_STRIDE = COORDS_PER_VERTEX * FLOAT_SIZE; - - private static final int COUNT_FILL_VERTEX = 4; - private static final int COUNT_LINE_VERTEX = 2; - private static final int COUNT_RECT_VERTEX = 4; - private static final int COUNT_CIRCLE_VERTEX = 120; // multiple of 4 - private static final int OFFSET_FILL_RECT = 0; - private static final int OFFSET_DRAW_LINE = OFFSET_FILL_RECT + COUNT_FILL_VERTEX; - private static final int OFFSET_DRAW_RECT = OFFSET_DRAW_LINE + COUNT_LINE_VERTEX; - private static final int OFFSET_FILL_CIRCLE = OFFSET_DRAW_RECT + COUNT_RECT_VERTEX; - private static final int OFFSET_DRAW_CIRCLE = OFFSET_FILL_CIRCLE + 1; - private static final int OFFSET_LAST = OFFSET_DRAW_CIRCLE + COUNT_CIRCLE_VERTEX + 1; - - private static final float[] BOX_COORDINATES = new float[OFFSET_LAST * 2]; - private static final float[] BOUNDS_COORDINATES = { - 0, 0, 0, 1, - 1, 1, 0, 1, - }; - private static final String POSITION_ATTRIBUTE = "aPosition"; - private static final String COLOR_UNIFORM = "uColor"; - private static final String MATRIX_UNIFORM = "uMatrix"; - private static final String TEXTURE_MATRIX_UNIFORM = "uTextureMatrix"; - private static final String TEXTURE_SAMPLER_UNIFORM = "uTextureSampler"; - private static final String ALPHA_UNIFORM = "uAlpha"; - private static final String TEXTURE_COORD_ATTRIBUTE = "aTextureCoordinate"; - private static final String DRAW_VERTEX_SHADER = "" - + "uniform mat4 " + MATRIX_UNIFORM + ";\n" - + "attribute vec2 " + POSITION_ATTRIBUTE + ";\n" - + "void main() {\n" - + " vec4 pos = vec4(" + POSITION_ATTRIBUTE + ", 0.0, 1.0);\n" - + " gl_Position = " + MATRIX_UNIFORM + " * pos;\n" - + "}\n"; - private static final String DRAW_FRAGMENT_SHADER = "" - + "precision mediump float;\n" - + "uniform vec4 " + COLOR_UNIFORM + ";\n" - + "void main() {\n" - + " gl_FragColor = " + COLOR_UNIFORM + ";\n" - + "}\n"; - private static final String TEXTURE_VERTEX_SHADER = "" - + "uniform mat4 " + MATRIX_UNIFORM + ";\n" - + "uniform mat4 " + TEXTURE_MATRIX_UNIFORM + ";\n" - + "attribute vec2 " + POSITION_ATTRIBUTE + ";\n" - + "varying vec2 vTextureCoord;\n" - + "void main() {\n" - + " vec4 pos = vec4(" + POSITION_ATTRIBUTE + ", 0.0, 1.0);\n" - + " gl_Position = " + MATRIX_UNIFORM + " * pos;\n" - + " vTextureCoord = (" + TEXTURE_MATRIX_UNIFORM + " * pos).xy;\n" - + "}\n"; - private static final String MESH_VERTEX_SHADER = "" - + "uniform mat4 " + MATRIX_UNIFORM + ";\n" - + "attribute vec2 " + POSITION_ATTRIBUTE + ";\n" - + "attribute vec2 " + TEXTURE_COORD_ATTRIBUTE + ";\n" - + "varying vec2 vTextureCoord;\n" - + "void main() {\n" - + " vec4 pos = vec4(" + POSITION_ATTRIBUTE + ", 0.0, 1.0);\n" - + " gl_Position = " + MATRIX_UNIFORM + " * pos;\n" - + " vTextureCoord = " + TEXTURE_COORD_ATTRIBUTE + ";\n" - + "}\n"; - private static final String TEXTURE_FRAGMENT_SHADER = "" - + "precision mediump float;\n" - + "varying vec2 vTextureCoord;\n" - + "uniform float " + ALPHA_UNIFORM + ";\n" - + "uniform sampler2D " + TEXTURE_SAMPLER_UNIFORM + ";\n" - + "void main() {\n" - + " gl_FragColor = texture2D(" + TEXTURE_SAMPLER_UNIFORM + ", vTextureCoord);\n" - + " gl_FragColor *= " + ALPHA_UNIFORM + ";\n" - + "}\n"; - private static final int INITIAL_RESTORE_STATE_SIZE = 8; - private static final int MATRIX_SIZE = 16; - // Handle indices -- common - private static final int INDEX_POSITION = 0; - private static final int INDEX_MATRIX = 1; - // Handle indices -- draw - private static final int INDEX_COLOR = 2; - // Handle indices -- texture - private static final int INDEX_TEXTURE_MATRIX = 2; - private static final int INDEX_TEXTURE_SAMPLER = 3; - private static final int INDEX_ALPHA = 4; - // Handle indices -- mesh - private static final int INDEX_TEXTURE_COORD = 2; - private static final GLId mGLId = new GLES20IdImpl(); - - static { - float[] temp = { - 0, 0, // Fill rectangle - 1, 0, - 0, 1, - 1, 1, - 0, 0, // Draw line - 1, 1, - 0, 0, // Draw rectangle outline - 0, 1, - 1, 1, - 1, 0, - 0.5f, 0.5f // Fill circle - }; - System.arraycopy(temp, 0, BOX_COORDINATES, 0, temp.length); - - // Draw circle - int arrayOffset = OFFSET_DRAW_CIRCLE * 2; - for (int i = 0, n = COUNT_CIRCLE_VERTEX / 4; i <= n; i++) { - float value = (float) Math.sin(MathUtils.radians(90f / n * i)) / 2; - float positive = value + 0.5f; - float negative = -value + 0.5f; - BOX_COORDINATES[arrayOffset + i * 2 + 1] = positive; - BOX_COORDINATES[arrayOffset + n * 2 - i * 2] = positive; - BOX_COORDINATES[arrayOffset + n * 2 + i * 2] = negative; - BOX_COORDINATES[arrayOffset + n * 4 - i * 2 + 1] = positive; - BOX_COORDINATES[arrayOffset + n * 4 + i * 2 + 1] = negative; - BOX_COORDINATES[arrayOffset + n * 6 - i * 2] = negative; - BOX_COORDINATES[arrayOffset + n * 6 + i * 2] = positive; - BOX_COORDINATES[arrayOffset + n * 8 - i * 2 + 1] = negative; - } - } - - private final IntList mSaveFlags = new IntList(); - // Projection matrix - private final float[] mProjectionMatrix = new float[MATRIX_SIZE]; - // GL programs - private final int mDrawProgram; - private final int mTextureProgram; - private final int mMeshProgram; - - // GL buffer containing BOX_COORDINATES - private final int mBoxCoordinates; - private final IntList mUnboundTextures = new IntList(); - private final IntList mDeleteBuffers = new IntList(); - // Buffer for framebuffer IDs -- we keep track so we can switch the attached - // texture. - private final int[] mFrameBuffer = new int[1]; - // Bound textures. - private final ArrayList mTargetTextures = new ArrayList<>(); - // Temporary variables used within calculations - private final float[] mTempMatrix = new float[32]; - private final float[] mTempColor = new float[4]; - private final RectF mTempSourceRect = new RectF(); - private final RectF mTempTargetRect = new RectF(); - private final float[] mTempTextureMatrix = new float[MATRIX_SIZE]; - private final int[] mTempIntArray = new int[1]; - ShaderParameter[] mDrawParameters = { - new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION - new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX - new UniformShaderParameter(COLOR_UNIFORM), // INDEX_COLOR - }; - ShaderParameter[] mTextureParameters = { - new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION - new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX - new UniformShaderParameter(TEXTURE_MATRIX_UNIFORM), // INDEX_TEXTURE_MATRIX - new UniformShaderParameter(TEXTURE_SAMPLER_UNIFORM), // INDEX_TEXTURE_SAMPLER - new UniformShaderParameter(ALPHA_UNIFORM), // INDEX_ALPHA - }; - ShaderParameter[] mMeshParameters = { - new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION - new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX - new AttributeShaderParameter(TEXTURE_COORD_ATTRIBUTE), // INDEX_TEXTURE_COORD - new UniformShaderParameter(TEXTURE_SAMPLER_UNIFORM), // INDEX_TEXTURE_SAMPLER - new UniformShaderParameter(ALPHA_UNIFORM), // INDEX_ALPHA - }; - // Keep track of restore state - private float[] mMatrices = new float[INITIAL_RESTORE_STATE_SIZE * MATRIX_SIZE]; - private float[] mAlphas = new float[INITIAL_RESTORE_STATE_SIZE]; - private int mCurrentAlphaIndex = 0; - private int mCurrentMatrixIndex = 0; - // Viewport size - private int mWidth; - private int mHeight; - // Screen size for when we aren't bound to a texture - private int mScreenWidth; - private int mScreenHeight; - // Keep track of statistics for debugging - private int mCountDrawMesh = 0; - private int mCountTextureRect = 0; - private int mCountFillRect = 0; - private int mCountDrawLine = 0; - - public GLES20Canvas() { - Matrix.setIdentityM(mTempTextureMatrix, 0); - Matrix.setIdentityM(mMatrices, mCurrentMatrixIndex); - mAlphas[mCurrentAlphaIndex] = 1f; - mTargetTextures.add(null); - - FloatBuffer boxBuffer = createBuffer(BOX_COORDINATES); - mBoxCoordinates = uploadBuffer(boxBuffer); - - int drawVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, DRAW_VERTEX_SHADER); - int textureVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, TEXTURE_VERTEX_SHADER); - int meshVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, MESH_VERTEX_SHADER); - int drawFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, DRAW_FRAGMENT_SHADER); - int textureFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, TEXTURE_FRAGMENT_SHADER); - - mDrawProgram = assembleProgram(drawVertexShader, drawFragmentShader, mDrawParameters); - mTextureProgram = assembleProgram(textureVertexShader, textureFragmentShader, - mTextureParameters); - mMeshProgram = assembleProgram(meshVertexShader, textureFragmentShader, mMeshParameters); - - GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA); - checkError(); - } - - private static FloatBuffer createBuffer(float[] values) { - // First create an nio buffer, then create a VBO from it. - int size = values.length * FLOAT_SIZE; - FloatBuffer buffer = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder()) - .asFloatBuffer(); - buffer.put(values, 0, values.length).position(0); - return buffer; - } - - private static int loadShader(int type, String shaderCode) { - // create a vertex shader type (GLES20.GL_VERTEX_SHADER) - // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER) - int shader = GLES20.glCreateShader(type); - - // add the source code to the shader and compile it - GLES20.glShaderSource(shader, shaderCode); - checkError(); - GLES20.glCompileShader(shader); - checkError(); - - return shader; - } - - private static void copyTextureCoordinates(BasicTexture texture, RectF outRect) { - int left = 0; - int top = 0; - int right = texture.getWidth(); - int bottom = texture.getHeight(); - if (texture.hasBorder()) { - left = 1; - top = 1; - right -= 1; - bottom -= 1; - } - outRect.set(left, top, right, bottom); - } - - // This function changes the source coordinate to the texture coordinates. - // It also clips the source and target coordinates if it is beyond the - // bound of the texture. - private static void convertCoordinate(RectF source, RectF target, BasicTexture texture) { - int width = texture.getWidth(); - int height = texture.getHeight(); - int texWidth = texture.getTextureWidth(); - int texHeight = texture.getTextureHeight(); - // Convert to texture coordinates - source.left /= texWidth; - source.right /= texWidth; - source.top /= texHeight; - source.bottom /= texHeight; - - // Clip if the rendering range is beyond the bound of the texture. - float xBound = (float) width / texWidth; - if (source.right > xBound) { - target.right = target.left + target.width() * (xBound - source.left) / source.width(); - source.right = xBound; - } - float yBound = (float) height / texHeight; - if (source.bottom > yBound) { - target.bottom = target.top + target.height() * (yBound - source.top) / source.height(); - source.bottom = yBound; - } - } - - private static void checkFramebufferStatus() { - int status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER); - if (status != GLES20.GL_FRAMEBUFFER_COMPLETE) { - String msg = ""; - switch (status) { - case GLES20.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: - msg = "GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT"; - break; - case GLES20.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS: - msg = "GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS"; - break; - case GLES20.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: - msg = "GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT"; - break; - case GLES20.GL_FRAMEBUFFER_UNSUPPORTED: - msg = "GL_FRAMEBUFFER_UNSUPPORTED"; - break; - } - throw new RuntimeException(msg + ":" + Integer.toHexString(status)); - } - } - - public static void checkError() { - int error = GLES20.glGetError(); - if (error != 0) { - Throwable t = new Throwable(); - Log.e(TAG, "GL error: " + error, t); - } - } - - @SuppressWarnings("unused") - private static void printMatrix(String message, float[] m, int offset) { - StringBuilder b = new StringBuilder(message); - for (int i = 0; i < MATRIX_SIZE; i++) { - b.append(' '); - if (i % 4 == 0) { - b.append('\n'); - } - b.append(m[offset + i]); - } - Log.v(TAG, b.toString()); - } - - private int assembleProgram(int vertexShader, int fragmentShader, ShaderParameter[] params) { - int program = GLES20.glCreateProgram(); - checkError(); - if (program == 0) { - throw new RuntimeException("Cannot create GL program: " + GLES20.glGetError()); - } - GLES20.glAttachShader(program, vertexShader); - checkError(); - GLES20.glAttachShader(program, fragmentShader); - checkError(); - GLES20.glLinkProgram(program); - checkError(); - int[] mLinkStatus = mTempIntArray; - GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, mLinkStatus, 0); - if (mLinkStatus[0] != GLES20.GL_TRUE) { - Log.e(TAG, "Could not link program: "); - Log.e(TAG, GLES20.glGetProgramInfoLog(program)); - GLES20.glDeleteProgram(program); - program = 0; - } - for (int i = 0; i < params.length; i++) { - params[i].loadHandle(program); - } - return program; - } - - @Override - public void setSize(int width, int height) { - mWidth = width; - mHeight = height; - GLES20.glViewport(0, 0, mWidth, mHeight); - checkError(); - Matrix.setIdentityM(mMatrices, mCurrentMatrixIndex); - Matrix.orthoM(mProjectionMatrix, 0, 0, width, 0, height, -1, 1); - if (getTargetTexture() == null) { - mScreenWidth = width; - mScreenHeight = height; - Matrix.translateM(mMatrices, mCurrentMatrixIndex, 0, height, 0); - Matrix.scaleM(mMatrices, mCurrentMatrixIndex, 1, -1, 1); - } - } - - @Override - public void clearBuffer() { - GLES20.glClearColor(0f, 0f, 0f, 1f); - checkError(); - GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); - checkError(); - } - - @Override - public void clearBuffer(float[] argb) { - GLES20.glClearColor(argb[1], argb[2], argb[3], argb[0]); - checkError(); - GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); - checkError(); - } - - @Override - public float getAlpha() { - return mAlphas[mCurrentAlphaIndex]; - } - - @Override - public void setAlpha(float alpha) { - mAlphas[mCurrentAlphaIndex] = alpha; - } - - @Override - public void multiplyAlpha(float alpha) { - setAlpha(getAlpha() * alpha); - } - - @Override - public void translate(float x, float y, float z) { - Matrix.translateM(mMatrices, mCurrentMatrixIndex, x, y, z); - } - - // This is a faster version of translate(x, y, z) because - // (1) we knows z = 0, (2) we inline the Matrix.translateM call, - // (3) we unroll the loop - @Override - public void translate(float x, float y) { - int index = mCurrentMatrixIndex; - float[] m = mMatrices; - m[index + 12] += m[index] * x + m[index + 4] * y; - m[index + 13] += m[index + 1] * x + m[index + 5] * y; - m[index + 14] += m[index + 2] * x + m[index + 6] * y; - m[index + 15] += m[index + 3] * x + m[index + 7] * y; - } - - @Override - public void scale(float sx, float sy, float sz) { - Matrix.scaleM(mMatrices, mCurrentMatrixIndex, sx, sy, sz); - } - - @Override - public void rotate(float angle, float x, float y, float z) { - if (angle == 0f) { - return; - } - float[] temp = mTempMatrix; - Matrix.setRotateM(temp, 0, angle, x, y, z); - float[] matrix = mMatrices; - int index = mCurrentMatrixIndex; - Matrix.multiplyMM(temp, MATRIX_SIZE, matrix, index, temp, 0); - System.arraycopy(temp, MATRIX_SIZE, matrix, index, MATRIX_SIZE); - } - - @Override - public void multiplyMatrix(float[] matrix, int offset) { - float[] temp = mTempMatrix; - float[] currentMatrix = mMatrices; - int index = mCurrentMatrixIndex; - Matrix.multiplyMM(temp, 0, currentMatrix, index, matrix, offset); - System.arraycopy(temp, 0, currentMatrix, index, 16); - } - - @Override - public void save() { - save(SAVE_FLAG_ALL); - } - - @Override - public void save(int saveFlags) { - boolean saveAlpha = (saveFlags & SAVE_FLAG_ALPHA) == SAVE_FLAG_ALPHA; - if (saveAlpha) { - float currentAlpha = getAlpha(); - mCurrentAlphaIndex++; - if (mAlphas.length <= mCurrentAlphaIndex) { - mAlphas = Arrays.copyOf(mAlphas, mAlphas.length * 2); - } - mAlphas[mCurrentAlphaIndex] = currentAlpha; - } - boolean saveMatrix = (saveFlags & SAVE_FLAG_MATRIX) == SAVE_FLAG_MATRIX; - if (saveMatrix) { - int currentIndex = mCurrentMatrixIndex; - mCurrentMatrixIndex += MATRIX_SIZE; - if (mMatrices.length <= mCurrentMatrixIndex) { - mMatrices = Arrays.copyOf(mMatrices, mMatrices.length * 2); - } - System.arraycopy(mMatrices, currentIndex, mMatrices, mCurrentMatrixIndex, MATRIX_SIZE); - } - mSaveFlags.add(saveFlags); - } - - @Override - public void restore() { - int restoreFlags = mSaveFlags.removeAt(mSaveFlags.size() - 1); - boolean restoreAlpha = (restoreFlags & SAVE_FLAG_ALPHA) == SAVE_FLAG_ALPHA; - if (restoreAlpha) { - mCurrentAlphaIndex--; - } - boolean restoreMatrix = (restoreFlags & SAVE_FLAG_MATRIX) == SAVE_FLAG_MATRIX; - if (restoreMatrix) { - mCurrentMatrixIndex -= MATRIX_SIZE; - } - } - - @Override - public void drawLine(float x1, float y1, float x2, float y2, GLPaint paint) { - draw(GLES20.GL_LINE_STRIP, OFFSET_DRAW_LINE, COUNT_LINE_VERTEX, x1, y1, x2 - x1, y2 - y1, - paint); - mCountDrawLine++; - } - - @Override - public void drawRect(float x, float y, float width, float height, GLPaint paint) { - draw(GLES20.GL_LINE_LOOP, OFFSET_DRAW_RECT, COUNT_RECT_VERTEX, x, y, width, height, paint); - mCountDrawLine++; - } - - @Override - public void drawOval(float cx, float cy, float radiusX, float radiusY, GLPaint paint) { - float halfLineWidth = paint.getLineWidth() / 2; - fillOval(cx, cy, radiusX + halfLineWidth, radiusY + halfLineWidth, paint.getColor()); - fillOval(cx, cy, radiusX - halfLineWidth, radiusY - halfLineWidth, paint.getBackgroundColor()); - } - - @Override - public void drawArc(float cx, float cy, float radiusX, float radiusY, - float sweepAngle, GLPaint paint) { - float halfLineWidth = paint.getLineWidth() / 2; - fillSector(cx, cy, radiusX + halfLineWidth, radiusY + halfLineWidth, sweepAngle, paint.getColor()); - fillOval(cx, cy, radiusX - halfLineWidth, radiusY - halfLineWidth, paint.getBackgroundColor()); - } - - private void draw(int type, int offset, int count, float x, float y, float width, float height, - GLPaint paint) { - draw(type, offset, count, x, y, width, height, paint.getColor(), paint.getLineWidth()); - } - - private void draw(int type, int offset, int count, float x, float y, float width, float height, - int color, float lineWidth) { - prepareDraw(offset, color, lineWidth); - draw(mDrawParameters, type, count, x, y, width, height); - } - - private void prepareDraw(int offset, int color, float lineWidth) { - GLES20.glUseProgram(mDrawProgram); - checkError(); - if (lineWidth > 0) { - GLES20.glLineWidth(lineWidth); - checkError(); - } - float[] colorArray = getColor(color); - boolean blendingEnabled = (colorArray[3] < 1f); - enableBlending(blendingEnabled); - if (blendingEnabled) { - GLES20.glBlendColor(colorArray[0], colorArray[1], colorArray[2], colorArray[3]); - checkError(); - } - - GLES20.glUniform4fv(mDrawParameters[INDEX_COLOR].handle, 1, colorArray, 0); - setPosition(mDrawParameters, offset); - checkError(); - } - - private float[] getColor(int color) { - float alpha = ((color >>> 24) & 0xFF) / 255f * getAlpha(); - float red = ((color >>> 16) & 0xFF) / 255f * alpha; - float green = ((color >>> 8) & 0xFF) / 255f * alpha; - float blue = (color & 0xFF) / 255f * alpha; - mTempColor[0] = red; - mTempColor[1] = green; - mTempColor[2] = blue; - mTempColor[3] = alpha; - return mTempColor; - } - - private void enableBlending(boolean enableBlending) { - if (enableBlending) { - GLES20.glEnable(GLES20.GL_BLEND); - checkError(); - } else { - GLES20.glDisable(GLES20.GL_BLEND); - checkError(); - } - } - - private void setPosition(ShaderParameter[] params, int offset) { - GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mBoxCoordinates); - checkError(); - GLES20.glVertexAttribPointer(params[INDEX_POSITION].handle, COORDS_PER_VERTEX, - GLES20.GL_FLOAT, false, VERTEX_STRIDE, offset * VERTEX_STRIDE); - checkError(); - GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); - checkError(); - } - - private void draw(ShaderParameter[] params, int type, int count, float x, float y, float width, - float height) { - setMatrix(params, x, y, width, height); - int positionHandle = params[INDEX_POSITION].handle; - GLES20.glEnableVertexAttribArray(positionHandle); - checkError(); - GLES20.glDrawArrays(type, 0, count); - checkError(); - GLES20.glDisableVertexAttribArray(positionHandle); - checkError(); - } - - private void setMatrix(ShaderParameter[] params, float x, float y, float width, float height) { - Matrix.translateM(mTempMatrix, 0, mMatrices, mCurrentMatrixIndex, x, y, 0f); - Matrix.scaleM(mTempMatrix, 0, width, height, 1f); - Matrix.multiplyMM(mTempMatrix, MATRIX_SIZE, mProjectionMatrix, 0, mTempMatrix, 0); - GLES20.glUniformMatrix4fv(params[INDEX_MATRIX].handle, 1, false, mTempMatrix, MATRIX_SIZE); - checkError(); - } - - @Override - public void fillRect(float x, float y, float width, float height, int color) { - draw(GLES20.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, COUNT_FILL_VERTEX, x, y, width, height, - color, 0f); - mCountFillRect++; - } - - @Override - public void fillOval(float cx, float cy, float radiusX, float radiusY, int color) { - draw(GLES20.GL_TRIANGLE_FAN, OFFSET_FILL_CIRCLE, OFFSET_LAST - OFFSET_FILL_CIRCLE, - cx - radiusX, cy - radiusY, radiusX * 2, radiusY * 2, color, 0f); - mCountFillRect++; - } - - @Override - public void fillSector(float cx, float cy, float radiusX, float radiusY, float sweepAngle, int color) { - float conjugateAngle = Math.abs(360 - MathUtils.positiveModulo(sweepAngle, 360)); - int conjugateCount = Math.round(COUNT_CIRCLE_VERTEX * conjugateAngle / 360); - if (conjugateCount == 0) { - // It is a circle - fillOval(cx, cy, radiusX, radiusY, color); - } else if (conjugateCount < COUNT_CIRCLE_VERTEX) { - draw(GLES20.GL_TRIANGLE_FAN, OFFSET_FILL_CIRCLE, OFFSET_LAST - OFFSET_FILL_CIRCLE - conjugateCount, - cx - radiusX, cy - radiusY, radiusX * 2, radiusY * 2, color, 0f); - mCountFillRect++; - } - } - - @Override - public void drawTexture(BasicTexture texture, int x, int y, int width, int height) { - if (width <= 0 || height <= 0) { - return; - } - copyTextureCoordinates(texture, mTempSourceRect); - mTempTargetRect.set(x, y, x + width, y + height); - convertCoordinate(mTempSourceRect, mTempTargetRect, texture); - drawTextureRect(texture, mTempSourceRect, mTempTargetRect); - } - - @Override - public void drawTexture(BasicTexture texture, RectF source, RectF target) { - if (target.width() <= 0 || target.height() <= 0) { - return; - } - mTempSourceRect.set(source); - mTempTargetRect.set(target); - - convertCoordinate(mTempSourceRect, mTempTargetRect, texture); - drawTextureRect(texture, mTempSourceRect, mTempTargetRect); - } - - @Override - public void drawTexture(BasicTexture texture, float[] textureTransform, int x, int y, int w, - int h) { - if (w <= 0 || h <= 0) { - return; - } - mTempTargetRect.set(x, y, x + w, y + h); - drawTextureRect(texture, textureTransform, mTempTargetRect); - } - - private void drawTextureRect(BasicTexture texture, RectF source, RectF target) { - setTextureMatrix(source); - drawTextureRect(texture, mTempTextureMatrix, target); - } - - private void setTextureMatrix(RectF source) { - mTempTextureMatrix[0] = source.width(); - mTempTextureMatrix[5] = source.height(); - mTempTextureMatrix[12] = source.left; - mTempTextureMatrix[13] = source.top; - } - - private void drawTextureRect(BasicTexture texture, float[] textureMatrix, RectF target) { - ShaderParameter[] params = mTextureParameters; - prepareTexture(texture, mTextureProgram, params); - setPosition(params, OFFSET_FILL_RECT); - GLES20.glUniformMatrix4fv(params[INDEX_TEXTURE_MATRIX].handle, 1, false, textureMatrix, 0); - checkError(); - if (texture.isFlippedVertically()) { - save(SAVE_FLAG_MATRIX); - translate(0, target.centerY()); - scale(1, -1, 1); - translate(0, -target.centerY()); - } - draw(params, GLES20.GL_TRIANGLE_STRIP, COUNT_FILL_VERTEX, target.left, target.top, - target.width(), target.height()); - if (texture.isFlippedVertically()) { - restore(); - } - mCountTextureRect++; - } - - private void prepareTexture(BasicTexture texture, int program, ShaderParameter[] params) { - GLES20.glUseProgram(program); - checkError(); - enableBlending(!texture.isOpaque() || getAlpha() < OPAQUE_ALPHA); - GLES20.glActiveTexture(GLES20.GL_TEXTURE0); - checkError(); - texture.onBind(this); - GLES20.glBindTexture(texture.getTarget(), texture.getId()); - checkError(); - GLES20.glUniform1i(params[INDEX_TEXTURE_SAMPLER].handle, 0); - checkError(); - GLES20.glUniform1f(params[INDEX_ALPHA].handle, getAlpha()); - checkError(); - } - - @Override - public void drawMesh(BasicTexture texture, int x, int y, int xyBuffer, int uvBuffer, - int indexBuffer, int indexCount) { - prepareTexture(texture, mMeshProgram, mMeshParameters); - - GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, indexBuffer); - checkError(); - - GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, xyBuffer); - checkError(); - int positionHandle = mMeshParameters[INDEX_POSITION].handle; - GLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, - VERTEX_STRIDE, 0); - checkError(); - - GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, uvBuffer); - checkError(); - int texCoordHandle = mMeshParameters[INDEX_TEXTURE_COORD].handle; - GLES20.glVertexAttribPointer(texCoordHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, - false, VERTEX_STRIDE, 0); - checkError(); - GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); - checkError(); - - GLES20.glEnableVertexAttribArray(positionHandle); - checkError(); - GLES20.glEnableVertexAttribArray(texCoordHandle); - checkError(); - - setMatrix(mMeshParameters, x, y, 1, 1); - GLES20.glDrawElements(GLES20.GL_TRIANGLE_STRIP, indexCount, GLES20.GL_UNSIGNED_BYTE, 0); - checkError(); - - GLES20.glDisableVertexAttribArray(positionHandle); - checkError(); - GLES20.glDisableVertexAttribArray(texCoordHandle); - checkError(); - GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0); - checkError(); - mCountDrawMesh++; - } - - @Override - public void drawMixed(BasicTexture texture, int toColor, float ratio, int x, int y, int w, int h) { - copyTextureCoordinates(texture, mTempSourceRect); - mTempTargetRect.set(x, y, x + w, y + h); - drawMixed(texture, toColor, ratio, mTempSourceRect, mTempTargetRect); - } - - @Override - public void drawMixed(BasicTexture texture, int toColor, float ratio, RectF source, RectF target) { - if (target.width() <= 0 || target.height() <= 0) { - return; - } - save(SAVE_FLAG_ALPHA); - - float currentAlpha = getAlpha(); - float cappedRatio = Math.min(1f, Math.max(0f, ratio)); - - float textureAlpha = (1f - cappedRatio) * currentAlpha; - setAlpha(textureAlpha); - drawTexture(texture, source, target); - - if (0 != Color.alpha(toColor)) { - float colorAlpha = cappedRatio * currentAlpha; - setAlpha(colorAlpha); - fillRect(target.left, target.top, target.width(), target.height(), toColor); - } - - restore(); - } - - @Override - public boolean unloadTexture(BasicTexture texture) { - boolean unload = texture.isLoaded(); - if (unload) { - synchronized (mUnboundTextures) { - mUnboundTextures.add(texture.getId()); - } - } - return unload; - } - - @Override - public void deleteBuffer(int bufferId) { - synchronized (mUnboundTextures) { - mDeleteBuffers.add(bufferId); - } - } - - @Override - public void deleteRecycledResources() { - synchronized (mUnboundTextures) { - IntList ids = mUnboundTextures; - if (mUnboundTextures.size() > 0) { - mGLId.glDeleteTextures(null, ids.size(), ids.getInternalArray(), 0); - ids.clear(); - } - - ids = mDeleteBuffers; - if (ids.size() > 0) { - mGLId.glDeleteBuffers(null, ids.size(), ids.getInternalArray(), 0); - ids.clear(); - } - } - } - - @Override - public void dumpStatisticsAndClear() { - String line = String.format(Locale.US, "MESH:%d, TEX_RECT:%d, FILL_RECT:%d, LINE:%d", mCountDrawMesh, - mCountTextureRect, mCountFillRect, mCountDrawLine); - mCountDrawMesh = 0; - mCountTextureRect = 0; - mCountFillRect = 0; - mCountDrawLine = 0; - Log.d(TAG, line); - } - - @Override - public void endRenderTarget() { - RawTexture oldTexture = mTargetTextures.remove(mTargetTextures.size() - 1); - RawTexture texture = getTargetTexture(); - setRenderTarget(oldTexture, texture); - restore(); // restore matrix and alpha - } - - @Override - public void beginRenderTarget(RawTexture texture) { - save(); // save matrix and alpha and blending - RawTexture oldTexture = getTargetTexture(); - mTargetTextures.add(texture); - setRenderTarget(oldTexture, texture); - } - - private RawTexture getTargetTexture() { - return mTargetTextures.get(mTargetTextures.size() - 1); - } - - private void setRenderTarget(BasicTexture oldTexture, RawTexture texture) { - if (oldTexture == null && texture != null) { - GLES20.glGenFramebuffers(1, mFrameBuffer, 0); - checkError(); - GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffer[0]); - checkError(); - } else if (oldTexture != null && texture == null) { - GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); - checkError(); - GLES20.glDeleteFramebuffers(1, mFrameBuffer, 0); - checkError(); - } - - if (texture == null) { - setSize(mScreenWidth, mScreenHeight); - } else { - setSize(texture.getWidth(), texture.getHeight()); - - if (!texture.isLoaded()) { - texture.prepare(this); - } - - GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, - texture.getTarget(), texture.getId(), 0); - checkError(); - - checkFramebufferStatus(); - } - } - - @Override - public void setTextureParameters(BasicTexture texture) { - int target = texture.getTarget(); - GLES20.glBindTexture(target, texture.getId()); - checkError(); - GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); - GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); - GLES20.glTexParameterf(target, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); - GLES20.glTexParameterf(target, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); - } - - @Override - public void initializeTextureSize(BasicTexture texture, int format, int type) { - int target = texture.getTarget(); - GLES20.glBindTexture(target, texture.getId()); - checkError(); - int width = texture.getTextureWidth(); - int height = texture.getTextureHeight(); - GLES20.glTexImage2D(target, 0, format, width, height, 0, format, type, null); - } - - @Override - public void initializeTexture(BasicTexture texture, Bitmap bitmap) { - int target = texture.getTarget(); - GLES20.glBindTexture(target, texture.getId()); - checkError(); - GLUtils.texImage2D(target, 0, bitmap, 0); - } - - @Override - public void texSubImage2D(BasicTexture texture, int xOffset, int yOffset, Bitmap bitmap, - int format, int type) { - int target = texture.getTarget(); - GLES20.glBindTexture(target, texture.getId()); - checkError(); - GLUtils.texSubImage2D(target, 0, xOffset, yOffset, bitmap, format, type); - } - - @Override - public int uploadBuffer(FloatBuffer buf) { - return uploadBuffer(buf, FLOAT_SIZE); - } - - @Override - public int uploadBuffer(ByteBuffer buf) { - return uploadBuffer(buf, 1); - } - - private int uploadBuffer(Buffer buffer, int elementSize) { - mGLId.glGenBuffers(1, mTempIntArray, 0); - checkError(); - int bufferId = mTempIntArray[0]; - GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferId); - checkError(); - GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, buffer.capacity() * elementSize, buffer, - GLES20.GL_STATIC_DRAW); - checkError(); - return bufferId; - } - - @Override - public void recoverFromLightCycle() { - GLES20.glViewport(0, 0, mWidth, mHeight); - GLES20.glDisable(GLES20.GL_DEPTH_TEST); - GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA); - checkError(); - } - - @Override - public void getBounds(Rect bounds, int x, int y, int width, int height) { - Matrix.translateM(mTempMatrix, 0, mMatrices, mCurrentMatrixIndex, x, y, 0f); - Matrix.scaleM(mTempMatrix, 0, width, height, 1f); - Matrix.multiplyMV(mTempMatrix, MATRIX_SIZE, mTempMatrix, 0, BOUNDS_COORDINATES, 0); - Matrix.multiplyMV(mTempMatrix, MATRIX_SIZE + 4, mTempMatrix, 0, BOUNDS_COORDINATES, 4); - bounds.left = Math.round(mTempMatrix[MATRIX_SIZE]); - bounds.right = Math.round(mTempMatrix[MATRIX_SIZE + 4]); - bounds.top = Math.round(mTempMatrix[MATRIX_SIZE + 1]); - bounds.bottom = Math.round(mTempMatrix[MATRIX_SIZE + 5]); - bounds.sort(); - } - - @Override - public GLId getGLId() { - return mGLId; - } - - private abstract static class ShaderParameter { - protected final String mName; - public int handle; - - public ShaderParameter(String name) { - mName = name; - } - - public abstract void loadHandle(int program); - } - - private static class UniformShaderParameter extends ShaderParameter { - public UniformShaderParameter(String name) { - super(name); - } - - @Override - public void loadHandle(int program) { - handle = GLES20.glGetUniformLocation(program, mName); - checkError(); - } - } - - private static class AttributeShaderParameter extends ShaderParameter { - public AttributeShaderParameter(String name) { - super(name); - } - - @Override - public void loadHandle(int program) { - handle = GLES20.glGetAttribLocation(program, mName); - checkError(); - } - } -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/GLES20IdImpl.java b/app/src/main/java/com/hippo/glview/glrenderer/GLES20IdImpl.java deleted file mode 100644 index a7adccc74..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/GLES20IdImpl.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2022 Tarsin Norbin - * - * This file is part of EhViewer - * - * EhViewer is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * EhViewer is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with EhViewer. - * If not, see . - */ - -package com.hippo.glview.glrenderer; - -import android.opengl.GLES20; - -import javax.microedition.khronos.opengles.GL11; -import javax.microedition.khronos.opengles.GL11ExtensionPack; - -public class GLES20IdImpl implements GLId { - private final int[] mTempIntArray = new int[1]; - - @Override - public int generateTexture() { - GLES20.glGenTextures(1, mTempIntArray, 0); - GLES20Canvas.checkError(); - return mTempIntArray[0]; - } - - @Override - public void glGenBuffers(int n, int[] buffers, int offset) { - GLES20.glGenBuffers(n, buffers, offset); - GLES20Canvas.checkError(); - } - - @Override - public void glDeleteTextures(GL11 gl, int n, int[] textures, int offset) { - GLES20.glDeleteTextures(n, textures, offset); - GLES20Canvas.checkError(); - } - - - @Override - public void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset) { - GLES20.glDeleteBuffers(n, buffers, offset); - GLES20Canvas.checkError(); - } - - @Override - public void glDeleteFramebuffers(GL11ExtensionPack gl11ep, int n, int[] buffers, int offset) { - GLES20.glDeleteFramebuffers(n, buffers, offset); - GLES20Canvas.checkError(); - } -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/GLId.java b/app/src/main/java/com/hippo/glview/glrenderer/GLId.java deleted file mode 100644 index dc578fe6b..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/GLId.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.glrenderer; - -import javax.microedition.khronos.opengles.GL11; -import javax.microedition.khronos.opengles.GL11ExtensionPack; - -// This mimics corresponding GL functions. -public interface GLId { - int generateTexture(); - - void glGenBuffers(int n, int[] buffers, int offset); - - void glDeleteTextures(GL11 gl, int n, int[] textures, int offset); - - void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset); - - void glDeleteFramebuffers(GL11ExtensionPack gl11ep, int n, int[] buffers, int offset); -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/GLPaint.java b/app/src/main/java/com/hippo/glview/glrenderer/GLPaint.java deleted file mode 100644 index 75044387a..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/GLPaint.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.glrenderer; - -public class GLPaint { - private float mLineWidth = 1f; - private int mColor = 0; - private int mBackgroundColor = 0; - - public int getColor() { - return mColor; - } - - public void setColor(int color) { - mColor = color; - } - - public int getBackgroundColor() { - return mBackgroundColor; - } - - public void setBackgroundColor(int backgroundColor) { - mBackgroundColor = backgroundColor; - } - - public float getLineWidth() { - return mLineWidth; - } - - public void setLineWidth(float width) { - if (width < 0) { - throw new IllegalArgumentException("width < 0"); - } - mLineWidth = width; - } -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/MovableTextTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/MovableTextTexture.java deleted file mode 100644 index c6ac505fa..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/MovableTextTexture.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright 2022 Tarsin Norbin - * - * This file is part of EhViewer - * - * EhViewer is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * EhViewer is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with EhViewer. - * If not, see . - */ - -package com.hippo.glview.glrenderer; - -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Typeface; - -import java.util.Arrays; - -// TODO support multiline - -/** - * Works like movable type.
- *
- * Only support single line now - */ -public final class MovableTextTexture extends SpriteTexture { - - private final char[] mCharacters; - private final float[] mWidths; - private final float mHeight; - private final float mMaxWidth; - - private MovableTextTexture(Bitmap bitmap, int count, int[] rects, char[] characters, - float[] widths, float height, float maxWidth) { - super(bitmap, false, count, rects); - mCharacters = characters; - mWidths = widths; - mHeight = height; - mMaxWidth = maxWidth; - } - - /** - * Create a TextTexture to draw text - * - * @param typeface the typeface - * @param size text size - * @param characters all Characters - * @return the TextTexture - */ - public static MovableTextTexture create(Typeface typeface, int size, int color, char[] characters) { - Paint paint = new Paint(); - paint.setAntiAlias(true); - paint.setTextSize(size); - paint.setColor(color); - paint.setTypeface(typeface); - - Paint.FontMetricsInt fmi = paint.getFontMetricsInt(); - int fixed = fmi.bottom; - int height = fmi.bottom - fmi.top; - - int length = characters.length; - float[] widths = new float[length]; - paint.getTextWidths(characters, 0, length, widths); - - // Calculate bitmap size - float maxWidth = 0.0f; - for (float f : widths) { - maxWidth = Math.max(maxWidth, f); - } - int hCount = (int) Math.ceil(Math.sqrt(height / maxWidth * length)); - int vCount = (int) Math.ceil(Math.sqrt(maxWidth / height * length)); - if (hCount * (vCount - 1) > length) { - vCount--; - } - if ((hCount - 1) * vCount > length) { - hCount--; - } - - Bitmap bitmap = Bitmap.createBitmap((int) Math.ceil(hCount * maxWidth), - (int) Math.ceil(vCount * height), Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - canvas.translate(0, height - fixed); - - // Draw - int[] rects = new int[length * 4]; - int x = 0; - int y = 0; - for (int i = 0; i < length; i++) { - int offset = i * 4; - rects[offset] = x; - rects[offset + 1] = y; - rects[offset + 2] = (int) widths[i]; - rects[offset + 3] = height; - - canvas.drawText(characters, i, 1, x, y, paint); - - if (i % hCount == hCount - 1) { - // The end of row - x = 0; - y += height; - } else { - x += maxWidth; - } - } - - return new MovableTextTexture(bitmap, length, rects, characters, widths, height, maxWidth); - } - - public int[] getTextIndexes(String text) { - char[] characters = mCharacters; - - int length = text.length(); - int[] indexes = new int[length]; - for (int i = 0; i < length; i++) { - char ch = text.charAt(i); - indexes[i] = Arrays.binarySearch(characters, ch); - } - - return indexes; - } - - public float getTextWidth(String text) { - char[] characters = mCharacters; - float[] widths = mWidths; - float width = 0.0f; - - for (int i = 0, n = text.length(); i < n; i++) { - char ch = text.charAt(i); - int index = Arrays.binarySearch(characters, ch); - if (index >= 0) { - width += widths[index]; - } else { - width += mMaxWidth; - } - } - - return width; - } - - public float getTextWidth(int[] indexes) { - float[] widths = mWidths; - float width = 0.0f; - int length = indexes.length; - for (int i = 0; i < length; i++) { - int index = indexes[i]; - if (index >= 0) { - width += widths[index]; - } else { - width += mMaxWidth; - } - } - - return width; - } - - public float getMaxWidth() { - return mMaxWidth; - } - - public float getTextHeight() { - return mHeight; - } - - public void drawText(GLCanvas canvas, String text, int x, int y) { - char[] characters = mCharacters; - float[] widths = mWidths; - - for (int i = 0, n = text.length(); i < n; i++) { - char ch = text.charAt(i); - int index = Arrays.binarySearch(characters, ch); - if (index >= 0) { - drawSprite(canvas, index, x, y); - x += widths[index]; - } else { - x += mMaxWidth; - } - } - } - - public void drawText(GLCanvas canvas, int[] indexes, int x, int y) { - float[] widths = mWidths; - - for (int i = 0, n = indexes.length; i < n; i++) { - int index = indexes[i]; - if (index >= 0) { - drawSprite(canvas, index, x, y); - x += widths[index]; - } else { - x += mMaxWidth; - } - } - } -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/MultiLineTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/MultiLineTexture.java deleted file mode 100644 index d7600c892..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/MultiLineTexture.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.glrenderer; - -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.text.Layout; -import android.text.StaticLayout; -import android.text.TextPaint; - - -// MultiLineTexture is a texture shows the content of a specified String. -// -// To create a MultiLineTexture, use the newInstance() method and specify -// the String, the font size, and the color. -class MultiLineTexture extends CanvasTexture { - private final Layout mLayout; - - private MultiLineTexture(Layout layout) { - super(layout.getWidth(), layout.getHeight()); - mLayout = layout; - } - - public static MultiLineTexture newInstance( - String text, int maxWidth, float textSize, int color, - Layout.Alignment alignment) { - TextPaint paint = StringTexture.getDefaultPaint(textSize, color); - Layout layout = new StaticLayout(text, 0, text.length(), paint, - maxWidth, alignment, 1, 0, true, null, 0); - - return new MultiLineTexture(layout); - } - - @Override - protected void onDraw(Canvas canvas, Bitmap backing) { - mLayout.draw(canvas); - } -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/NativeTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/NativeTexture.java deleted file mode 100644 index beea42c69..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/NativeTexture.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.glrenderer; - -import android.opengl.GLES20; -import android.util.Log; - -import javax.microedition.khronos.opengles.GL11; - -public abstract class NativeTexture extends BasicTexture { - - private static final String TAG = NativeTexture.class.getSimpleName(); - - private boolean mContentValid = true; - - private boolean mOpaque = true; - - public static void checkError() { - int error = GLES20.glGetError(); - if (error != 0) { - Throwable t = new Throwable(); - Log.e(TAG, "GL error: " + error, t); - } - } - - public void invalidateContent() { - mContentValid = false; - } - - protected abstract void texImage(boolean init); - - private void uploadToCanvas(GLCanvas canvas) { - // Get id - mId = canvas.getGLId().generateTexture(); - - // Prepare - canvas.setTextureParameters(this); - - // Call glTexImage2D - GLES20.glBindTexture(getTarget(), mId); - checkError(); - texImage(true); - checkError(); - - setAssociatedCanvas(canvas); - mState = STATE_LOADED; - mContentValid = true; - } - - public void updateContent(GLCanvas canvas) { - if (!isLoaded()) { - uploadToCanvas(canvas); - } else if (!mContentValid) { - // Call glTexSubImage2D - GLES20.glBindTexture(getTarget(), mId); - checkError(); - texImage(false); - checkError(); - mContentValid = true; - } - } - - /** - * Whether the content on GPU is valid. - */ - public boolean isContentValid() { - return isLoaded() && mContentValid; - } - - @Override - protected boolean onBind(GLCanvas canvas) { - updateContent(canvas); - return isContentValid(); - } - - @Override - protected int getTarget() { - return GL11.GL_TEXTURE_2D; - } - - @Override - public boolean isOpaque() { - return mOpaque; - } - - public void setOpaque(boolean isOpaque) { - mOpaque = isOpaque; - } -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/NinePatchChunk.java b/app/src/main/java/com/hippo/glview/glrenderer/NinePatchChunk.java deleted file mode 100644 index dd1ad6c8c..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/NinePatchChunk.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.glrenderer; - -import android.graphics.Rect; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -// See "frameworks/base/include/utils/ResourceTypes.h" for the format of -// NinePatch chunk. -class NinePatchChunk { - - public static final int NO_COLOR = 0x00000001; - public static final int TRANSPARENT_COLOR = 0x00000000; - - public Rect mPaddings = new Rect(); - - public int[] mDivX; - public int[] mDivY; - public int[] mColor; - - private static void readIntArray(int[] data, ByteBuffer buffer) { - for (int i = 0, n = data.length; i < n; ++i) { - data[i] = buffer.getInt(); - } - } - - private static void checkDivCount(int length) { - if (length == 0 || (length & 0x01) != 0) { - throw new RuntimeException("invalid nine-patch: " + length); - } - } - - public static NinePatchChunk deserialize(byte[] data) { - ByteBuffer byteBuffer = - ByteBuffer.wrap(data).order(ByteOrder.nativeOrder()); - - byte wasSerialized = byteBuffer.get(); - if (wasSerialized == 0) return null; - - NinePatchChunk chunk = new NinePatchChunk(); - chunk.mDivX = new int[byteBuffer.get()]; - chunk.mDivY = new int[byteBuffer.get()]; - chunk.mColor = new int[byteBuffer.get()]; - - checkDivCount(chunk.mDivX.length); - checkDivCount(chunk.mDivY.length); - - // skip 8 bytes - byteBuffer.getInt(); - byteBuffer.getInt(); - - chunk.mPaddings.left = byteBuffer.getInt(); - chunk.mPaddings.right = byteBuffer.getInt(); - chunk.mPaddings.top = byteBuffer.getInt(); - chunk.mPaddings.bottom = byteBuffer.getInt(); - - // skip 4 bytes - byteBuffer.getInt(); - - readIntArray(chunk.mDivX, byteBuffer); - readIntArray(chunk.mDivY, byteBuffer); - readIntArray(chunk.mColor, byteBuffer); - - return chunk; - } -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/NinePatchTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/NinePatchTexture.java deleted file mode 100644 index 4d055f15f..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/NinePatchTexture.java +++ /dev/null @@ -1,424 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.glrenderer; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Rect; - -import com.hippo.yorozuya.MathUtils; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.FloatBuffer; - -// NinePatchTexture is a texture backed by a NinePatch resource. -// -// getPaddings() returns paddings specified in the NinePatch. -// getNinePatchChunk() returns the layout data specified in the NinePatch. -// -public class NinePatchTexture extends ResourceTexture { - @SuppressWarnings("unused") - private static final String TAG = "NinePatchTexture"; - private final SmallCache mInstanceCache - = new SmallCache(); - private NinePatchChunk mChunk; - - public NinePatchTexture(Context context, int resId) { - super(context, resId); - } - - @Override - protected Bitmap onGetBitmap() { - if (mBitmap != null) return mBitmap; - - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inPreferredConfig = Bitmap.Config.ARGB_8888; - Bitmap bitmap = BitmapFactory.decodeResource( - mContext.getResources(), mResId, options); - mBitmap = bitmap; - setSize(bitmap.getWidth(), bitmap.getHeight()); - byte[] chunkData = bitmap.getNinePatchChunk(); - mChunk = chunkData == null - ? null - : NinePatchChunk.deserialize(bitmap.getNinePatchChunk()); - if (mChunk == null) { - throw new RuntimeException("invalid nine-patch image: " + mResId); - } - return bitmap; - } - - public Rect getPaddings() { - // get the paddings from nine patch - if (mChunk == null) onGetBitmap(); - return mChunk.mPaddings; - } - - public NinePatchChunk getNinePatchChunk() { - if (mChunk == null) onGetBitmap(); - return mChunk; - } - - private NinePatchInstance findInstance(GLCanvas canvas, int w, int h) { - int key = w; - key = (key << 16) | h; - NinePatchInstance instance = mInstanceCache.get(key); - - if (instance == null) { - instance = new NinePatchInstance(this, w, h); - NinePatchInstance removed = mInstanceCache.put(key, instance); - if (removed != null) { - removed.recycle(canvas); - } - } - - return instance; - } - - @Override - public void draw(GLCanvas canvas, int x, int y, int w, int h) { - if (!isLoaded()) { - mInstanceCache.clear(); - } - - if (w != 0 && h != 0) { - findInstance(canvas, w, h).draw(canvas, this, x, y); - } - } - - @Override - public void recycle() { - super.recycle(); - GLCanvas canvas = mCanvasRef; - if (canvas == null) return; - int n = mInstanceCache.size(); - for (int i = 0; i < n; i++) { - NinePatchInstance instance = mInstanceCache.valueAt(i); - instance.recycle(canvas); - } - mInstanceCache.clear(); - } - - // This is a simple cache for a small number of things. Linear search - // is used because the cache is small. It also tries to remove less used - // item when the cache is full by moving the often-used items to the front. - private static class SmallCache { - private static final int CACHE_SIZE = 16; - private static final int CACHE_SIZE_START_MOVE = CACHE_SIZE / 2; - private final int[] mKey = new int[CACHE_SIZE]; - private final V[] mValue = (V[]) new Object[CACHE_SIZE]; - private int mCount; // number of items in this cache - - // Puts a value into the cache. If the cache is full, also returns - // a less used item, otherwise returns null. - public V put(int key, V value) { - if (mCount == CACHE_SIZE) { - V old = mValue[CACHE_SIZE - 1]; // remove the last item - mKey[CACHE_SIZE - 1] = key; - mValue[CACHE_SIZE - 1] = value; - return old; - } else { - mKey[mCount] = key; - mValue[mCount] = value; - mCount++; - return null; - } - } - - public V get(int key) { - for (int i = 0; i < mCount; i++) { - if (mKey[i] == key) { - // Move the accessed item one position to the front, so it - // will less likely to be removed when cache is full. Only - // do this if the cache is starting to get full. - if (mCount > CACHE_SIZE_START_MOVE && i > 0) { - int tmpKey = mKey[i]; - mKey[i] = mKey[i - 1]; - mKey[i - 1] = tmpKey; - - V tmpValue = mValue[i]; - mValue[i] = mValue[i - 1]; - mValue[i - 1] = tmpValue; - } - return mValue[i]; - } - } - return null; - } - - public void clear() { - for (int i = 0; i < mCount; i++) { - mValue[i] = null; // make sure it's can be garbage-collected. - } - mCount = 0; - } - - public int size() { - return mCount; - } - - public V valueAt(int i) { - return mValue[i]; - } - } -} - -// This keeps data for a specialization of NinePatchTexture with the size -// (width, height). We pre-compute the coordinates for efficiency. -class NinePatchInstance { - - @SuppressWarnings("unused") - private static final String TAG = "NinePatchInstance"; - - // We need 16 vertices for a normal nine-patch image (the 4x4 vertices) - private static final int VERTEX_BUFFER_SIZE = 16 * 2; - - // We need 22 indices for a normal nine-patch image, plus 2 for each - // transparent region. Current there are at most 1 transparent region. - private static final int INDEX_BUFFER_SIZE = 22 + 2; - - private FloatBuffer mXyBuffer; - private FloatBuffer mUvBuffer; - private ByteBuffer mIndexBuffer; - - // Names for buffer names: xy, uv, index. - private int mXyBufferName = -1; - private int mUvBufferName; - private int mIndexBufferName; - - private int mIdxCount; - - public NinePatchInstance(NinePatchTexture tex, int width, int height) { - NinePatchChunk chunk = tex.getNinePatchChunk(); - - if (width <= 0 || height <= 0) { - throw new RuntimeException("invalid dimension"); - } - - // The code should be easily extended to handle the general cases by - // allocating more space for buffers. But let's just handle the only - // use case. - if (chunk.mDivX.length != 2 || chunk.mDivY.length != 2) { - throw new RuntimeException("unsupported nine patch"); - } - - float[] divX = new float[4]; - float[] divY = new float[4]; - float[] divU = new float[4]; - float[] divV = new float[4]; - - int nx = stretch(divX, divU, chunk.mDivX, tex.getWidth(), width); - int ny = stretch(divY, divV, chunk.mDivY, tex.getHeight(), height); - - prepareVertexData(divX, divY, divU, divV, nx, ny, chunk.mColor); - } - - /** - * Stretches the texture according to the nine-patch rules. It will - * linearly distribute the strechy parts defined in the nine-patch chunk to - * the target area. - * - *
-     *                      source
-     *          /--------------^---------------\
-     *         u0    u1       u2  u3     u4   u5
-     * div ---> |fffff|ssssssss|fff|ssssss|ffff| ---> u
-     *          |    div0    div1 div2   div3  |
-     *          |     |       /   /      /    /
-     *          |     |      /   /     /    /
-     *          |     |     /   /    /    /
-     *          |fffff|ssss|fff|sss|ffff| ---> x
-     *         x0    x1   x2  x3  x4   x5
-     *          \----------v------------/
-     *                  target
-     *
-     * f: fixed segment
-     * s: stretchy segment
-     * 
- * - * @param div the stretch parts defined in nine-patch chunk - * @param source the length of the texture - * @param target the length on the drawing plan - * @param u output, the positions of these dividers in the texture - * coordinate - * @param x output, the corresponding position of these dividers on the - * drawing plan - * @return the number of these dividers. - */ - private static int stretch( - float[] x, float[] u, int[] div, int source, int target) { - int textureSize = MathUtils.nextPowerOf2(source); - float textureBound = (float) source / textureSize; - - float stretch = 0; - for (int i = 0, n = div.length; i < n; i += 2) { - stretch += div[i + 1] - div[i]; - } - - float remaining = target - source + stretch; - - float lastX = 0; - float lastU = 0; - - x[0] = 0; - u[0] = 0; - for (int i = 0, n = div.length; i < n; i += 2) { - // Make the stretchy segment a little smaller to prevent sampling - // on neighboring fixed segments. - // fixed segment - x[i + 1] = lastX + (div[i] - lastU) + 0.5f; - u[i + 1] = Math.min((div[i] + 0.5f) / textureSize, textureBound); - - // stretchy segment - float partU = div[i + 1] - div[i]; - float partX = remaining * partU / stretch; - remaining -= partX; - stretch -= partU; - - lastX = x[i + 1] + partX; - lastU = div[i + 1]; - x[i + 2] = lastX - 0.5f; - u[i + 2] = Math.min((lastU - 0.5f) / textureSize, textureBound); - } - // the last fixed segment - x[div.length + 1] = target; - u[div.length + 1] = textureBound; - - // remove segments with length 0. - int last = 0; - for (int i = 1, n = div.length + 2; i < n; ++i) { - if ((x[i] - x[last]) < 1f) continue; - x[++last] = x[i]; - u[last] = u[i]; - } - return last + 1; - } - - private static ByteBuffer allocateDirectNativeOrderBuffer(int size) { - return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder()); - } - - private void prepareVertexData(float[] x, float[] y, float[] u, float[] v, - int nx, int ny, int[] color) { - /* - * Given a 3x3 nine-patch image, the vertex order is defined as the - * following graph: - * - * (0) (1) (2) (3) - * | /| /| /| - * | / | / | / | - * (4) (5) (6) (7) - * | \ | \ | \ | - * | \| \| \| - * (8) (9) (A) (B) - * | /| /| /| - * | / | / | / | - * (C) (D) (E) (F) - * - * And we draw the triangle strip in the following index order: - * - * index: 04152637B6A5948C9DAEBF - */ - int pntCount = 0; - float[] xy = new float[VERTEX_BUFFER_SIZE]; - float[] uv = new float[VERTEX_BUFFER_SIZE]; - for (int j = 0; j < ny; ++j) { - for (int i = 0; i < nx; ++i) { - int xIndex = (pntCount++) << 1; - int yIndex = xIndex + 1; - xy[xIndex] = x[i]; - xy[yIndex] = y[j]; - uv[xIndex] = u[i]; - uv[yIndex] = v[j]; - } - } - - int idxCount = 1; - boolean isForward = false; - byte[] index = new byte[INDEX_BUFFER_SIZE]; - for (int row = 0; row < ny - 1; row++) { - --idxCount; - isForward = !isForward; - - int start, end, inc; - if (isForward) { - start = 0; - end = nx; - inc = 1; - } else { - start = nx - 1; - end = -1; - inc = -1; - } - - for (int col = start; col != end; col += inc) { - int k = row * nx + col; - if (col != start) { - int colorIdx = row * (nx - 1) + col; - if (isForward) colorIdx--; - if (color[colorIdx] == NinePatchChunk.TRANSPARENT_COLOR) { - index[idxCount] = index[idxCount - 1]; - ++idxCount; - index[idxCount++] = (byte) k; - } - } - - index[idxCount++] = (byte) k; - index[idxCount++] = (byte) (k + nx); - } - } - - mIdxCount = idxCount; - - int size = (pntCount * 2) * (Float.SIZE / Byte.SIZE); - mXyBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer(); - mUvBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer(); - mIndexBuffer = allocateDirectNativeOrderBuffer(mIdxCount); - - mXyBuffer.put(xy, 0, pntCount * 2).position(0); - mUvBuffer.put(uv, 0, pntCount * 2).position(0); - mIndexBuffer.put(index, 0, idxCount).position(0); - } - - private void prepareBuffers(GLCanvas canvas) { - mXyBufferName = canvas.uploadBuffer(mXyBuffer); - mUvBufferName = canvas.uploadBuffer(mUvBuffer); - mIndexBufferName = canvas.uploadBuffer(mIndexBuffer); - - // These buffers are never used again. - mXyBuffer = null; - mUvBuffer = null; - mIndexBuffer = null; - } - - public void draw(GLCanvas canvas, NinePatchTexture tex, int x, int y) { - if (mXyBufferName == -1) { - prepareBuffers(canvas); - } - canvas.drawMesh(tex, x, y, mXyBufferName, mUvBufferName, mIndexBufferName, mIdxCount); - } - - public void recycle(GLCanvas canvas) { - if (mXyBuffer == null) { - canvas.deleteBuffer(mXyBufferName); - canvas.deleteBuffer(mUvBufferName); - canvas.deleteBuffer(mIndexBufferName); - mXyBufferName = -1; - } - } -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/RawTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/RawTexture.java deleted file mode 100644 index d54b42a1c..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/RawTexture.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.glrenderer; - -import android.util.Log; - -import javax.microedition.khronos.opengles.GL11; - -public class RawTexture extends BasicTexture { - private static final String TAG = "RawTexture"; - - private final boolean mOpaque; - private boolean mIsFlipped; - - public RawTexture(int width, int height, boolean opaque) { - mOpaque = opaque; - setSize(width, height); - } - - @Override - public boolean isOpaque() { - return mOpaque; - } - - @Override - public boolean isFlippedVertically() { - return mIsFlipped; - } - - public void setIsFlippedVertically(boolean isFlipped) { - mIsFlipped = isFlipped; - } - - protected void prepare(GLCanvas canvas) { - GLId glId = canvas.getGLId(); - mId = glId.generateTexture(); - canvas.initializeTextureSize(this, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE); - canvas.setTextureParameters(this); - mState = STATE_LOADED; - setAssociatedCanvas(canvas); - } - - @Override - protected boolean onBind(GLCanvas canvas) { - if (isLoaded()) return true; - Log.w(TAG, "lost the content due to context change"); - return false; - } - - @Override - public void yield() { - // we cannot free the texture because we have no backup. - } - - @Override - protected int getTarget() { - return GL11.GL_TEXTURE_2D; - } -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/ResourceTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/ResourceTexture.java deleted file mode 100644 index 613a32ac3..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/ResourceTexture.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.glrenderer; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; - -// ResourceTexture is a texture whose Bitmap is decoded from a resource. -// By default ResourceTexture is not opaque. -public class ResourceTexture extends UploadedTexture { - - protected final Context mContext; - protected final int mResId; - - public ResourceTexture(Context context, int resId) { - if (context == null) { - throw new IllegalArgumentException("context == null"); - } - mContext = context; - mResId = resId; - setOpaque(false); - } - - @Override - protected Bitmap onGetBitmap() { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inPreferredConfig = Bitmap.Config.ARGB_8888; - return BitmapFactory.decodeResource( - mContext.getResources(), mResId, options); - } - - @Override - protected void onFreeBitmap(Bitmap bitmap) { - if (!inFinalizer()) { - bitmap.recycle(); - } - } -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/SpriteTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/SpriteTexture.java deleted file mode 100644 index 81b54e369..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/SpriteTexture.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2022 Tarsin Norbin - * - * This file is part of EhViewer - * - * EhViewer is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * EhViewer is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with EhViewer. - * If not, see . - */ - -package com.hippo.glview.glrenderer; - -import android.graphics.Bitmap; -import android.graphics.RectF; - -import com.hippo.yorozuya.AssertUtils; - -public class SpriteTexture extends TiledTexture { - - private final int mCount; - private final int[] mRects; - - private final RectF mTempSource = new RectF(); - private final RectF mTempTarget = new RectF(); - - public SpriteTexture(Bitmap bitmap, boolean isOpaque, int count, int[] rects) { - super(bitmap, isOpaque); - - AssertUtils.assertEquals("rects.length must be count * 4", count * 4, rects.length); - mCount = count; - mRects = rects; - } - - public int getCount() { - return mCount; - } - - public void drawSprite(GLCanvas canvas, int index, int x, int y) { - int[] rects = mRects; - int offset = index * 4; - int sourceX = rects[offset]; - int sourceY = rects[offset + 1]; - int sourceWidth = rects[offset + 2]; - int sourceHeight = rects[offset + 3]; - mTempSource.set(sourceX, sourceY, sourceX + sourceWidth, sourceY + sourceHeight); - mTempTarget.set(x, y, x + sourceWidth, y + sourceHeight); - draw(canvas, mTempSource, mTempTarget); - } - - public void drawSprite(GLCanvas canvas, int index, int x, int y, int width, int height) { - int[] rects = mRects; - int offset = index * 4; - int sourceX = rects[offset]; - int sourceY = rects[offset + 1]; - mTempSource.set(sourceX, sourceY, sourceX + rects[offset + 2], sourceY + rects[offset + 3]); - mTempTarget.set(x, y, x + width, y + height); - draw(canvas, mTempSource, mTempTarget); - } - - public void drawSpriteMixed(GLCanvas canvas, int index, int color, float ratio, int x, int y) { - int[] rects = mRects; - int offset = index * 4; - int sourceX = rects[offset]; - int sourceY = rects[offset + 1]; - int sourceWidth = rects[offset + 2]; - int sourceHeight = rects[offset + 3]; - mTempSource.set(sourceX, sourceY, sourceX + sourceWidth, sourceY + sourceHeight); - mTempTarget.set(x, y, x + sourceWidth, y + sourceHeight); - drawMixed(canvas, color, ratio, mTempSource, mTempTarget); - } - - public void drawSpriteMixed(GLCanvas canvas, int index, int color, float ratio, - int x, int y, int width, int height) { - int[] rects = mRects; - int offset = index * 4; - int sourceX = rects[offset]; - int sourceY = rects[offset + 1]; - mTempSource.set(sourceX, sourceY, sourceX + rects[offset + 2], - sourceY + rects[offset + 3]); - mTempTarget.set(x, y, x + width, y + height); - drawMixed(canvas, color, ratio, mTempSource, mTempTarget); - } -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/StringTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/StringTexture.java deleted file mode 100644 index a898a2f28..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/StringTexture.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.glrenderer; - -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint.FontMetricsInt; -import android.graphics.Typeface; -import android.text.TextPaint; -import android.text.TextUtils; - -// StringTexture is a texture shows the content of a specified String. -// -// To create a StringTexture, use the newInstance() method and specify -// the String, the font size, and the color. -public class StringTexture extends CanvasTexture { - private final String mText; - private final TextPaint mPaint; - private final FontMetricsInt mMetrics; - - private StringTexture(String text, TextPaint paint, - FontMetricsInt metrics, int width, int height) { - super(width, height); - mText = text; - mPaint = paint; - mMetrics = metrics; - } - - public static TextPaint getDefaultPaint(float textSize, int color) { - TextPaint paint = new TextPaint(); - paint.setTextSize(textSize); - paint.setAntiAlias(true); - paint.setColor(color); - paint.setShadowLayer(2f, 0f, 0f, Color.BLACK); - return paint; - } - - public static StringTexture newInstance( - String text, float textSize, int color) { - return newInstance(text, getDefaultPaint(textSize, color)); - } - - public static StringTexture newInstance( - String text, float textSize, int color, - float lengthLimit, boolean isBold) { - TextPaint paint = getDefaultPaint(textSize, color); - if (isBold) { - paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD)); - } - if (lengthLimit > 0) { - text = TextUtils.ellipsize( - text, paint, lengthLimit, TextUtils.TruncateAt.END).toString(); - } - return newInstance(text, paint); - } - - private static StringTexture newInstance(String text, TextPaint paint) { - FontMetricsInt metrics = paint.getFontMetricsInt(); - int width = (int) Math.ceil(paint.measureText(text)); - int height = metrics.bottom - metrics.top; - // The texture size needs to be at least 1x1. - if (width <= 0) width = 1; - if (height <= 0) height = 1; - return new StringTexture(text, paint, metrics, width, height); - } - - @Override - protected void onDraw(Canvas canvas, Bitmap backing) { - canvas.translate(0, -mMetrics.ascent); - canvas.drawText(mText, 0, 0, mPaint); - } -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/Texture.java b/app/src/main/java/com/hippo/glview/glrenderer/Texture.java deleted file mode 100644 index 91df4239b..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/Texture.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.glrenderer; - - -import android.graphics.RectF; - -// Texture is a rectangular image which can be drawn on GLCanvas. -// The isOpaque() function gives a hint about whether the texture is opaque, -// so the drawing can be done faster. -// -// This is the current texture hierarchy: -// -// Texture -// -- ColorTexture -// -- FadeInTexture -// -- BasicTexture -// -- UploadedTexture -// -- BitmapTexture -// -- Tile -// -- ResourceTexture -// -- NinePatchTexture -// -- CanvasTexture -// -- StringTexture -// -public interface Texture { - int getWidth(); - - int getHeight(); - - void draw(GLCanvas canvas, int x, int y); - - void draw(GLCanvas canvas, int x, int y, int w, int h); - - void draw(GLCanvas canvas, RectF source, RectF target); - - boolean isOpaque(); -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/TextureUploader.java b/app/src/main/java/com/hippo/glview/glrenderer/TextureUploader.java deleted file mode 100644 index 7cf83719c..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/TextureUploader.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.glrenderer; - -import com.hippo.glview.view.GLRoot; - -import java.util.ArrayDeque; - -public class TextureUploader implements GLRoot.OnGLIdleListener { - private static final int INIT_CAPACITY = 64; - private static final int QUOTA_PER_FRAME = 1; - - private final ArrayDeque mFgTextures = - new ArrayDeque<>(INIT_CAPACITY); - private final ArrayDeque mBgTextures = - new ArrayDeque<>(INIT_CAPACITY); - private final GLRoot mGLRoot; - private volatile boolean mIsQueued = false; - - public TextureUploader(GLRoot root) { - mGLRoot = root; - } - - public synchronized void clear() { - while (!mFgTextures.isEmpty()) { - mFgTextures.pop().setIsUploading(false); - } - while (!mBgTextures.isEmpty()) { - mBgTextures.pop().setIsUploading(false); - } - } - - // caller should hold synchronized on "this" - private void queueSelfIfNeed() { - if (mIsQueued) return; - mIsQueued = true; - mGLRoot.addOnGLIdleListener(this); - } - - public synchronized void addBgTexture(UploadedTexture t) { - if (t.isContentValid()) return; - mBgTextures.addLast(t); - t.setIsUploading(true); - queueSelfIfNeed(); - } - - public synchronized void addFgTexture(UploadedTexture t) { - if (t.isContentValid()) return; - mFgTextures.addLast(t); - t.setIsUploading(true); - queueSelfIfNeed(); - } - - private int upload(GLCanvas canvas, ArrayDeque deque, - int uploadQuota, boolean isBackground) { - while (uploadQuota > 0) { - UploadedTexture t; - synchronized (this) { - if (deque.isEmpty()) break; - t = deque.removeFirst(); - t.setIsUploading(false); - if (t.isContentValid()) continue; - - // this has to be protected by the synchronized block - // to prevent the inner bitmap get recycled - t.updateContent(canvas); - } - - // It will took some more time for a texture to be drawn for - // the first time. - // Thus, when scrolling, if a new column appears on screen, - // it may cause a UI jank even these textures are uploaded. - if (isBackground) t.draw(canvas, 0, 0); - --uploadQuota; - } - return uploadQuota; - } - - @Override - public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) { - int uploadQuota = QUOTA_PER_FRAME; - uploadQuota = upload(canvas, mFgTextures, uploadQuota, false); - if (uploadQuota < QUOTA_PER_FRAME) mGLRoot.requestRender(); - upload(canvas, mBgTextures, uploadQuota, true); - synchronized (this) { - mIsQueued = !mFgTextures.isEmpty() || !mBgTextures.isEmpty(); - return mIsQueued; - } - } -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/TiledTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/TiledTexture.java deleted file mode 100644 index bc8794e6a..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/TiledTexture.java +++ /dev/null @@ -1,370 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.glrenderer; - -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.RectF; -import android.os.SystemClock; - -import com.hippo.glview.view.GLRoot; - -import java.util.ArrayDeque; -import java.util.ArrayList; - -// This class is similar to BitmapTexture, except the bitmap is -// split into tiles. By doing so, we may increase the time required to -// upload the whole bitmap but we reduce the time of uploading each tile -// so it make the animation more smooth and prevents jank. -public class TiledTexture implements Texture { - private static final int CONTENT_SIZE = 254; - private static final int BORDER_SIZE = 1; - private static final int TILE_SIZE = CONTENT_SIZE + 2 * BORDER_SIZE; - private static final int INIT_CAPACITY = 8; - - // We are targeting at 60fps, so we have 16ms for each frame. - // In this 16ms, we use about 4~8 ms to upload tiles. - private static final long UPLOAD_TILE_LIMIT = 4; // ms - private static final Object sFreeTileLock = new Object(); - private static final Bitmap sUploadBitmap; - private static final Canvas sCanvas; - private static final Paint sBitmapPaint; - private static final Paint sPaint; - private static Tile sFreeTileHead = null; - - static { - sUploadBitmap = Bitmap.createBitmap(TILE_SIZE, TILE_SIZE, Bitmap.Config.ARGB_8888); - sCanvas = new Canvas(sUploadBitmap); - sBitmapPaint = new Paint(Paint.FILTER_BITMAP_FLAG); - sBitmapPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); - sPaint = new Paint(); - sPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); - sPaint.setColor(Color.TRANSPARENT); - } - - private final Tile[] mTiles; // Can be modified in different threads. - // Should be protected by "synchronized." - private final int mWidth; - private final int mHeight; - private final RectF mSrcRect = new RectF(); - private final RectF mDestRect = new RectF(); - private int mUploadIndex = 0; - - public TiledTexture(Bitmap bitmap, boolean isOpaque) { - mWidth = bitmap.getWidth(); - mHeight = bitmap.getHeight(); - ArrayList list = new ArrayList<>(); - - for (int x = 0, w = mWidth; x < w; x += CONTENT_SIZE) { - for (int y = 0, h = mHeight; y < h; y += CONTENT_SIZE) { - Tile tile = obtainTile(); - tile.offsetX = x; - tile.offsetY = y; - tile.bitmap = bitmap; - tile.setSize( - Math.min(CONTENT_SIZE, mWidth - x), - Math.min(CONTENT_SIZE, mHeight - y)); - tile.setOpaque(isOpaque); - list.add(tile); - } - } - mTiles = list.toArray(new Tile[list.size()]); - } - - private static void freeTile(Tile tile) { - tile.invalidateContent(); - tile.bitmap = null; - synchronized (sFreeTileLock) { - tile.nextFreeTile = sFreeTileHead; - sFreeTileHead = tile; - } - } - - private static Tile obtainTile() { - synchronized (sFreeTileLock) { - Tile result = sFreeTileHead; - if (result == null) return new Tile(); - sFreeTileHead = result.nextFreeTile; - result.nextFreeTile = null; - return result; - } - } - - // We want to draw the "source" on the "target". - // This method is to find the "output" rectangle which is - // the corresponding area of the "src". - // (x,y) target - // (x0,y0) source +---------------+ - // +----------+ | | - // | src | | output | - // | +--+ | linear map | +----+ | - // | +--+ | ----------> | | | | - // | | by (scaleX, scaleY) | +----+ | - // +----------+ | | - // Texture +---------------+ - // Canvas - private static void mapRect(RectF output, - RectF src, float x0, float y0, float x, float y, float scaleX, - float scaleY) { - output.set(x + (src.left - x0) * scaleX, - y + (src.top - y0) * scaleY, - x + (src.right - x0) * scaleX, - y + (src.bottom - y0) * scaleY); - } - - private boolean uploadNextTile(GLCanvas canvas) { - if (mUploadIndex == mTiles.length) return true; - - synchronized (mTiles) { - Tile next = mTiles[mUploadIndex++]; - - // Make sure tile has not already been recycled by the time - // this is called (race condition in onGLIdle) - if (next.bitmap != null) { - boolean hasBeenLoad = next.isLoaded(); - next.updateContent(canvas); - - // It will take some time for a texture to be drawn for the first - // time. When scrolling, we need to draw several tiles on the screen - // at the same time. It may cause a UI jank even these textures has - // been uploaded. - if (!hasBeenLoad) next.draw(canvas, 0, 0); - } - } - return mUploadIndex == mTiles.length; - } - - public boolean isReady() { - return mUploadIndex == mTiles.length; - } - - // Can be called in UI thread. - public void recycle() { - synchronized (mTiles) { - for (int i = 0, n = mTiles.length; i < n; ++i) { - freeTile(mTiles[i]); - } - } - } - - // Draws a mixed color of this texture and a specified color onto the - // a rectangle. The used color is: from * (1 - ratio) + to * ratio. - public void drawMixed(GLCanvas canvas, int color, float ratio, - int x, int y, int width, int height) { - RectF src = mSrcRect; - RectF dest = mDestRect; - float scaleX = (float) width / mWidth; - float scaleY = (float) height / mHeight; - synchronized (mTiles) { - for (int i = 0, n = mTiles.length; i < n; ++i) { - Tile t = mTiles[i]; - src.set(0, 0, t.contentWidth, t.contentHeight); - src.offset(t.offsetX, t.offsetY); - mapRect(dest, src, 0, 0, x, y, scaleX, scaleY); - src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY); - canvas.drawMixed(t, color, ratio, mSrcRect, mDestRect); - } - } - } - - public void drawMixed(GLCanvas canvas, int color, float ratio, - RectF source, RectF target) { - RectF src = mSrcRect; - RectF dest = mDestRect; - float x0 = source.left; - float y0 = source.top; - float x = target.left; - float y = target.top; - float scaleX = target.width() / source.width(); - float scaleY = target.height() / source.height(); - - synchronized (mTiles) { - for (int i = 0, n = mTiles.length; i < n; ++i) { - Tile t = mTiles[i]; - src.set(0, 0, t.contentWidth, t.contentHeight); - src.offset(t.offsetX, t.offsetY); - if (!src.intersect(source)) continue; - mapRect(dest, src, x0, y0, x, y, scaleX, scaleY); - src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY); - canvas.drawMixed(t, color, ratio, mSrcRect, mDestRect); - } - } - } - - // Draws the texture on to the specified rectangle. - @Override - public void draw(GLCanvas canvas, int x, int y, int width, int height) { - RectF src = mSrcRect; - RectF dest = mDestRect; - float scaleX = (float) width / mWidth; - float scaleY = (float) height / mHeight; - synchronized (mTiles) { - for (int i = 0, n = mTiles.length; i < n; ++i) { - Tile t = mTiles[i]; - src.set(0, 0, t.contentWidth, t.contentHeight); - src.offset(t.offsetX, t.offsetY); - mapRect(dest, src, 0, 0, x, y, scaleX, scaleY); - src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY); - canvas.drawTexture(t, mSrcRect, mDestRect); - } - } - } - - // Draws a sub region of this texture on to the specified rectangle. - @Override - public void draw(GLCanvas canvas, RectF source, RectF target) { - RectF src = mSrcRect; - RectF dest = mDestRect; - float x0 = source.left; - float y0 = source.top; - float x = target.left; - float y = target.top; - float scaleX = target.width() / source.width(); - float scaleY = target.height() / source.height(); - - synchronized (mTiles) { - for (int i = 0, n = mTiles.length; i < n; ++i) { - Tile t = mTiles[i]; - src.set(0, 0, t.contentWidth, t.contentHeight); - src.offset(t.offsetX, t.offsetY); - if (!src.intersect(source)) continue; - mapRect(dest, src, x0, y0, x, y, scaleX, scaleY); - src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY); - canvas.drawTexture(t, src, dest); - } - } - } - - @Override - public int getWidth() { - return mWidth; - } - - @Override - public int getHeight() { - return mHeight; - } - - @Override - public void draw(GLCanvas canvas, int x, int y) { - draw(canvas, x, y, mWidth, mHeight); - } - - @Override - public boolean isOpaque() { - return false; - } - - public static class Uploader implements GLRoot.OnGLIdleListener { - private final ArrayDeque mTextures = - new ArrayDeque<>(INIT_CAPACITY); - - private final GLRoot mGlRoot; - private boolean mIsQueued = false; - - public Uploader(GLRoot glRoot) { - mGlRoot = glRoot; - } - - public synchronized void clear() { - mTextures.clear(); - } - - public synchronized void addTexture(TiledTexture t) { - if (t.isReady()) return; - mTextures.addLast(t); - - if (mIsQueued) return; - mIsQueued = true; - mGlRoot.addOnGLIdleListener(this); - } - - @Override - public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) { - ArrayDeque deque = mTextures; - synchronized (this) { - long now = SystemClock.uptimeMillis(); - long dueTime = now + UPLOAD_TILE_LIMIT; - while (now < dueTime && !deque.isEmpty()) { - TiledTexture t = deque.peekFirst(); - if (t.uploadNextTile(canvas)) { - deque.removeFirst(); - mGlRoot.requestRender(); - } - now = SystemClock.uptimeMillis(); - } - mIsQueued = !mTextures.isEmpty(); - - // return true to keep this listener in the queue - return mIsQueued; - } - } - } - - private static class Tile extends UploadedTexture { - public int offsetX; - public int offsetY; - public Bitmap bitmap; - public Tile nextFreeTile; - public int contentWidth; - public int contentHeight; - - @Override - public void setSize(int width, int height) { - contentWidth = width; - contentHeight = height; - mWidth = width + 2 * BORDER_SIZE; - mHeight = height + 2 * BORDER_SIZE; - mTextureWidth = TILE_SIZE; - mTextureHeight = TILE_SIZE; - } - - @Override - protected Bitmap onGetBitmap() { - // make a local copy of the reference to the bitmap, - // since it might be null'd in a different thread. b/8694871 - Bitmap localBitmapRef = bitmap; - bitmap = null; - - if (localBitmapRef != null) { - int x = BORDER_SIZE - offsetX; - int y = BORDER_SIZE - offsetY; - int r = localBitmapRef.getWidth() + x; - int b = localBitmapRef.getHeight() + y; - sCanvas.drawBitmap(localBitmapRef, x, y, sBitmapPaint); - localBitmapRef = null; - - // draw borders if need - if (x > 0) sCanvas.drawLine(x - 1, 0, x - 1, TILE_SIZE, sPaint); - if (y > 0) sCanvas.drawLine(0, y - 1, TILE_SIZE, y - 1, sPaint); - if (r < CONTENT_SIZE) sCanvas.drawLine(r, 0, r, TILE_SIZE, sPaint); - if (b < CONTENT_SIZE) sCanvas.drawLine(0, b, TILE_SIZE, b, sPaint); - } - - return sUploadBitmap; - } - - @Override - protected void onFreeBitmap(Bitmap bitmap) { - // do nothing - } - } -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/UploadedTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/UploadedTexture.java deleted file mode 100644 index a1e791475..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/UploadedTexture.java +++ /dev/null @@ -1,296 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.glrenderer; - -import android.graphics.Bitmap; -import android.graphics.Bitmap.Config; -import android.opengl.GLUtils; - -import java.util.HashMap; - -import javax.microedition.khronos.opengles.GL11; - -// UploadedTextures use a Bitmap for the content of the texture. -// -// Subclasses should implement onGetBitmap() to provide the Bitmap and -// implement onFreeBitmap(mBitmap) which will be called when the Bitmap -// is not needed anymore. -// -// isContentValid() is meaningful only when the isLoaded() returns true. -// It means whether the content needs to be updated. -// -// The user of this class should call recycle() when the texture is not -// needed anymore. -// -// By default an UploadedTexture is opaque (so it can be drawn faster without -// blending). The user or subclass can override it using setOpaque(). -public abstract class UploadedTexture extends BasicTexture { - - // To prevent keeping allocation the borders, we store those used borders here. - // Since the length will be power of two, it won't use too much memory. - private static final HashMap sBorderLines = - new HashMap<>(); - private static final BorderKey sBorderKey = new BorderKey(); - - @SuppressWarnings("unused") - private static final String TAG = "Texture"; - private static final int UPLOAD_LIMIT = 100; - private static int sUploadedCount; - protected Bitmap mBitmap; - private boolean mContentValid = true; - // indicate this textures is being uploaded in background - private boolean mIsUploading = false; - private boolean mOpaque = true; - private boolean mThrottled = false; - private int mBorder; - - protected UploadedTexture() { - this(false); - } - - protected UploadedTexture(boolean hasBorder) { - super(null, 0, STATE_UNLOADED); - if (hasBorder) { - setBorder(true); - mBorder = 1; - } - } - - private static Bitmap getBorderLine( - boolean vertical, Config config, int length) { - BorderKey key = sBorderKey; - key.vertical = vertical; - key.config = config; - key.length = length; - Bitmap bitmap = sBorderLines.get(key); - if (bitmap == null) { - bitmap = vertical - ? Bitmap.createBitmap(1, length, config) - : Bitmap.createBitmap(length, 1, config); - sBorderLines.put(key.clone(), bitmap); - } - return bitmap; - } - - public static void resetUploadLimit() { - sUploadedCount = 0; - } - - public static boolean uploadLimitReached() { - return sUploadedCount > UPLOAD_LIMIT; - } - - protected void setIsUploading(boolean uploading) { - mIsUploading = uploading; - } - - public boolean isUploading() { - return mIsUploading; - } - - protected void setThrottled(boolean throttled) { - mThrottled = throttled; - } - - private Bitmap getBitmap() { - if (mBitmap == null) { - mBitmap = onGetBitmap(); - if (mWidth == UNSPECIFIED) { - int w = mBitmap.getWidth() + mBorder * 2; - int h = mBitmap.getHeight() + mBorder * 2; - setSize(w, h); - } - } - return mBitmap; - } - - private void freeBitmap() { - if (mBitmap == null) { - throw new IllegalStateException("mBitmap == null"); - } - onFreeBitmap(mBitmap); - mBitmap = null; - } - - @Override - public int getWidth() { - if (mWidth == UNSPECIFIED) getBitmap(); - return mWidth; - } - - @Override - public int getHeight() { - if (mWidth == UNSPECIFIED) getBitmap(); - return mHeight; - } - - protected abstract Bitmap onGetBitmap(); - - protected abstract void onFreeBitmap(Bitmap bitmap); - - public void invalidateContent() { - if (mBitmap != null) freeBitmap(); - mContentValid = false; - mWidth = UNSPECIFIED; - mHeight = UNSPECIFIED; - } - - /** - * Whether the content on GPU is valid. - */ - public boolean isContentValid() { - return isLoaded() && mContentValid; - } - - /** - * Updates the content on GPU's memory. - * - * @param canvas - */ - public void updateContent(GLCanvas canvas) { - if (!isLoaded()) { - if (mThrottled && ++sUploadedCount > UPLOAD_LIMIT) { - return; - } - uploadToCanvas(canvas); - } else if (!mContentValid) { - Bitmap bitmap = getBitmap(); - int format = GLUtils.getInternalFormat(bitmap); - int type = GLUtils.getType(bitmap); - canvas.texSubImage2D(this, mBorder, mBorder, bitmap, format, type); - freeBitmap(); - mContentValid = true; - } - } - - private void uploadToCanvas(GLCanvas canvas) { - - Bitmap bitmap = getBitmap(); - if (bitmap != null) { - try { - int bWidth = bitmap.getWidth(); - int bHeight = bitmap.getHeight(); - int texWidth = getTextureWidth(); - int texHeight = getTextureHeight(); - - if (bWidth > texWidth || bHeight > texHeight) { - throw new IllegalStateException("bWidth > texWidth || bHeight > texHeight"); - } - - // Upload the bitmap to a new texture. - mId = canvas.getGLId().generateTexture(); - canvas.setTextureParameters(this); - - if (bWidth == texWidth && bHeight == texHeight) { - canvas.initializeTexture(this, bitmap); - } else { - int format = GLUtils.getInternalFormat(bitmap); - int type = GLUtils.getType(bitmap); - Config config = bitmap.getConfig(); - - canvas.initializeTextureSize(this, format, type); - canvas.texSubImage2D(this, mBorder, mBorder, bitmap, format, type); - - if (mBorder > 0) { - // Left border - Bitmap line = getBorderLine(true, config, texHeight); - canvas.texSubImage2D(this, 0, 0, line, format, type); - - // Top border - line = getBorderLine(false, config, texWidth); - canvas.texSubImage2D(this, 0, 0, line, format, type); - } - - // Right border - if (mBorder + bWidth < texWidth) { - Bitmap line = getBorderLine(true, config, texHeight); - canvas.texSubImage2D(this, mBorder + bWidth, 0, line, format, type); - } - - // Bottom border - if (mBorder + bHeight < texHeight) { - Bitmap line = getBorderLine(false, config, texWidth); - canvas.texSubImage2D(this, 0, mBorder + bHeight, line, format, type); - } - } - } finally { - freeBitmap(); - } - // Update texture state. - setAssociatedCanvas(canvas); - mState = STATE_LOADED; - mContentValid = true; - } else { - mState = STATE_ERROR; - throw new RuntimeException("Texture load fail, no bitmap"); - } - } - - @Override - protected boolean onBind(GLCanvas canvas) { - updateContent(canvas); - return isContentValid(); - } - - @Override - protected int getTarget() { - return GL11.GL_TEXTURE_2D; - } - - @Override - public boolean isOpaque() { - return mOpaque; - } - - public void setOpaque(boolean isOpaque) { - mOpaque = isOpaque; - } - - @Override - public void recycle() { - super.recycle(); - if (mBitmap != null) freeBitmap(); - } - - private static class BorderKey implements Cloneable { - public boolean vertical; - public Config config; - public int length; - - @Override - public int hashCode() { - int x = config.hashCode() ^ length; - return vertical ? x : -x; - } - - @Override - public boolean equals(Object object) { - if (!(object instanceof BorderKey o)) return false; - return vertical == o.vertical - && config == o.config && length == o.length; - } - - @Override - public BorderKey clone() { - try { - return (BorderKey) super.clone(); - } catch (CloneNotSupportedException e) { - throw new AssertionError(e); - } - } - } -} diff --git a/app/src/main/java/com/hippo/glview/glrenderer/XmlResourceTexture.java b/app/src/main/java/com/hippo/glview/glrenderer/XmlResourceTexture.java deleted file mode 100644 index c4c8989f2..000000000 --- a/app/src/main/java/com/hippo/glview/glrenderer/XmlResourceTexture.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2015 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.glrenderer; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.drawable.Drawable; - -import androidx.annotation.DrawableRes; -import androidx.core.content.ContextCompat; - -public class XmlResourceTexture extends UploadedTexture { - - private final Context mContext; - private final int mResId; - private int mWidth; - private int mHeight; - - public XmlResourceTexture(Context context, @DrawableRes int resId) { - mContext = context; - mResId = resId; - setOpaque(false); - } - - public void setBound(int width, int height) { - mWidth = width; - mHeight = height; - invalidateContent(); - } - - @Override - protected Bitmap onGetBitmap() { - Drawable drawable = ContextCompat.getDrawable(mContext, mResId); - - int width = mWidth; - int height = mHeight; - if (width <= 0) { - width = drawable.getIntrinsicWidth(); - } - if (height <= 0) { - height = drawable.getIntrinsicHeight(); - } - if (width <= 0) { - width = 1; - } - if (height <= 0) { - height = 1; - } - - drawable.setBounds(0, 0, width, height); - - Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - drawable.draw(canvas); - - return bitmap; - } - - @Override - protected void onFreeBitmap(Bitmap bitmap) { - if (!inFinalizer()) { - bitmap.recycle(); - } - } -} diff --git a/app/src/main/java/com/hippo/glview/image/GLImageMovableTextView.java b/app/src/main/java/com/hippo/glview/image/GLImageMovableTextView.java deleted file mode 100644 index 73055d77e..000000000 --- a/app/src/main/java/com/hippo/glview/image/GLImageMovableTextView.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.image; - -import android.graphics.Rect; -import android.text.TextUtils; - -import com.hippo.glview.glrenderer.GLCanvas; -import com.hippo.glview.view.GLView; -import com.hippo.glview.view.Gravity; -import com.hippo.yorozuya.collect.CollectionUtils; - -public class GLImageMovableTextView extends GLView { - - ImageMovableTextTexture mTextTexture; - - private String mText = ""; - private int[] mIndexes = CollectionUtils.EMPTY_INT_ARRAY; - - private int mGravity = Gravity.NO_GRAVITY; - - private void generateIndexes() { - if (mTextTexture == null || TextUtils.isEmpty(mText)) { - mIndexes = CollectionUtils.EMPTY_INT_ARRAY; - } else { - mIndexes = mTextTexture.getTextIndexes(mText); - } - } - - public void setTextTexture(ImageMovableTextTexture textTexture) { - if (mTextTexture == textTexture) { - return; - } - mTextTexture = textTexture; - - generateIndexes(); - requestLayout(); - } - - public void setText(String text) { - if (text == null) { - text = ""; - } - if (text.equals(mText)) { - return; - } - mText = text; - - generateIndexes(); - requestLayout(); - } - - public void setGravity(int gravity) { - if (mGravity != gravity) { - mGravity = gravity; - invalidate(); - } - } - - @Override - protected int getSuggestedMinimumWidth() { - if (mTextTexture == null) { - return super.getSuggestedMinimumWidth(); - } else { - return Math.max((int) mTextTexture.getTextWidth(mIndexes) + mPaddings.left + mPaddings.right, - super.getSuggestedMinimumWidth()); - } - } - - @Override - protected int getSuggestedMinimumHeight() { - if (mTextTexture == null) { - return super.getSuggestedMinimumHeight(); - } else { - return Math.max((int) mTextTexture.getTextHeight() + mPaddings.top + mPaddings.bottom, - super.getSuggestedMinimumHeight()); - } - } - - @Override - public void onRender(GLCanvas canvas) { - if (mTextTexture == null || mIndexes == null) { - return; - } - - Rect paddings = getPaddings(); - int x = getDefaultBegin(getWidth(), (int) mTextTexture.getTextWidth(mIndexes), - paddings.left, paddings.right, Gravity.getPosition(mGravity, Gravity.HORIZONTAL)); - int y = getDefaultBegin(getHeight(), (int) mTextTexture.getTextHeight(), - paddings.top, paddings.bottom, Gravity.getPosition(mGravity, Gravity.VERTICAL)); - mTextTexture.drawText(canvas, mIndexes, x, y); - } -} diff --git a/app/src/main/java/com/hippo/glview/image/ImageMovableTextTexture.java b/app/src/main/java/com/hippo/glview/image/ImageMovableTextTexture.java deleted file mode 100644 index 54740d39c..000000000 --- a/app/src/main/java/com/hippo/glview/image/ImageMovableTextTexture.java +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.image; - -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Typeface; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.hippo.glview.glrenderer.GLCanvas; -import com.hippo.image.Image; - -import java.util.Arrays; - -public class ImageMovableTextTexture extends ImageSpriteTexture { - - private final char[] mCharacters; - private final float[] mWidths; - private final float mHeight; - private final float mMaxWidth; - - public ImageMovableTextTexture(@NonNull ImageWrapper image, int count, int[] rects, - char[] characters, float[] widths, float height, float maxWidth) { - super(image, count, rects); - - mCharacters = characters; - mWidths = widths; - mHeight = height; - mMaxWidth = maxWidth; - } - - /** - * Create a TextTexture to draw text - * - * @param typeface the typeface - * @param size text size - * @param characters all Characters - * @return the TextTexture - */ - @Nullable - public static ImageMovableTextTexture create(Typeface typeface, int size, int color, char[] characters) { - Paint paint = new Paint(); - paint.setAntiAlias(true); - paint.setTextSize(size); - paint.setColor(color); - paint.setTypeface(typeface); - - Paint.FontMetricsInt fmi = paint.getFontMetricsInt(); - int fixed = fmi.bottom; - int height = fmi.bottom - fmi.top; - - int length = characters.length; - float[] widths = new float[length]; - paint.getTextWidths(characters, 0, length, widths); - - // Calculate bitmap size - float maxWidth = 0.0f; - for (float f : widths) { - maxWidth = Math.max(maxWidth, f); - } - int hCount = (int) Math.ceil(Math.sqrt(height / maxWidth * length)); - int vCount = (int) Math.ceil(Math.sqrt(maxWidth / height * length)); - if (hCount * (vCount - 1) > length) { - vCount--; - } - if ((hCount - 1) * vCount > length) { - hCount--; - } - - Bitmap bitmap = Bitmap.createBitmap((int) Math.ceil(hCount * maxWidth), - (int) Math.ceil(vCount * height), Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - canvas.translate(0, height - fixed); - - // Draw - int[] rects = new int[length * 4]; - int x = 0; - int y = 0; - for (int i = 0; i < length; i++) { - int offset = i * 4; - rects[offset] = x; - rects[offset + 1] = y; - rects[offset + 2] = (int) widths[i]; - rects[offset + 3] = height; - - canvas.drawText(characters, i, 1, x, y, paint); - - if (i % hCount == hCount - 1) { - // The end of row - x = 0; - y += height; - } else { - x += maxWidth; - } - } - - Image image = Image.create(bitmap); - if (image == null) { - return null; - } - ImageWrapper imageWrapper = new ImageWrapper(image); - if (imageWrapper.obtain()) { - return new ImageMovableTextTexture(imageWrapper, length, rects, characters, widths, height, maxWidth); - } else { - return null; - } - } - - public int[] getTextIndexes(String text) { - char[] characters = mCharacters; - - int length = text.length(); - int[] indexes = new int[length]; - for (int i = 0; i < length; i++) { - char ch = text.charAt(i); - indexes[i] = Arrays.binarySearch(characters, ch); - } - - return indexes; - } - - public float getTextWidth(String text) { - char[] characters = mCharacters; - float[] widths = mWidths; - float width = 0.0f; - - for (int i = 0, n = text.length(); i < n; i++) { - char ch = text.charAt(i); - int index = Arrays.binarySearch(characters, ch); - if (index >= 0) { - width += widths[index]; - } else { - width += mMaxWidth; - } - } - - return width; - } - - public float getTextWidth(int[] indexes) { - float[] widths = mWidths; - float width = 0.0f; - int length = indexes.length; - for (int i = 0; i < length; i++) { - int index = indexes[i]; - if (index >= 0) { - width += widths[index]; - } else { - width += mMaxWidth; - } - } - - return width; - } - - public float getMaxWidth() { - return mMaxWidth; - } - - public float getTextHeight() { - return mHeight; - } - - public void drawText(GLCanvas canvas, String text, int x, int y) { - char[] characters = mCharacters; - float[] widths = mWidths; - - for (int i = 0, n = text.length(); i < n; i++) { - char ch = text.charAt(i); - int index = Arrays.binarySearch(characters, ch); - if (index >= 0) { - drawSprite(canvas, index, x, y); - x += widths[index]; - } else { - x += mMaxWidth; - } - } - } - - public void drawText(GLCanvas canvas, int[] indexes, int x, int y) { - float[] widths = mWidths; - - for (int i = 0, n = indexes.length; i < n; i++) { - int index = indexes[i]; - if (index >= 0) { - drawSprite(canvas, index, x, y); - x += widths[index]; - } else { - x += mMaxWidth; - } - } - } -} diff --git a/app/src/main/java/com/hippo/glview/image/ImageSpriteTexture.java b/app/src/main/java/com/hippo/glview/image/ImageSpriteTexture.java deleted file mode 100644 index b66c9d270..000000000 --- a/app/src/main/java/com/hippo/glview/image/ImageSpriteTexture.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.image; - -import android.graphics.RectF; - -import androidx.annotation.NonNull; - -import com.hippo.glview.glrenderer.GLCanvas; -import com.hippo.yorozuya.AssertUtils; - -public class ImageSpriteTexture extends ImageTexture { - - private final int mCount; - private final int[] mRects; - - private final RectF mTempSource = new RectF(); - private final RectF mTempTarget = new RectF(); - - public ImageSpriteTexture(@NonNull ImageWrapper image, int count, int[] rects) { - super(image); - - AssertUtils.assertEquals("rects.length must be count * 4", count * 4, rects.length); - mCount = count; - mRects = rects; - } - - public int getCount() { - return mCount; - } - - public void drawSprite(GLCanvas canvas, int index, int x, int y) { - int[] rects = mRects; - int offset = index * 4; - int sourceX = rects[offset]; - int sourceY = rects[offset + 1]; - int sourceWidth = rects[offset + 2]; - int sourceHeight = rects[offset + 3]; - mTempSource.set(sourceX, sourceY, sourceX + sourceWidth, sourceY + sourceHeight); - mTempTarget.set(x, y, x + sourceWidth, y + sourceHeight); - draw(canvas, mTempSource, mTempTarget); - } - - public void drawSprite(GLCanvas canvas, int index, int x, int y, int width, int height) { - int[] rects = mRects; - int offset = index * 4; - int sourceX = rects[offset]; - int sourceY = rects[offset + 1]; - mTempSource.set(sourceX, sourceY, sourceX + rects[offset + 2], sourceY + rects[offset + 3]); - mTempTarget.set(x, y, x + width, y + height); - draw(canvas, mTempSource, mTempTarget); - } - - public void drawSpriteMixed(GLCanvas canvas, int index, int color, float ratio, int x, int y) { - int[] rects = mRects; - int offset = index * 4; - int sourceX = rects[offset]; - int sourceY = rects[offset + 1]; - int sourceWidth = rects[offset + 2]; - int sourceHeight = rects[offset + 3]; - mTempSource.set(sourceX, sourceY, sourceX + sourceWidth, sourceY + sourceHeight); - mTempTarget.set(x, y, x + sourceWidth, y + sourceHeight); - drawMixed(canvas, color, ratio, mTempSource, mTempTarget); - } - - public void drawSpriteMixed(GLCanvas canvas, int index, int color, float ratio, - int x, int y, int width, int height) { - int[] rects = mRects; - int offset = index * 4; - int sourceX = rects[offset]; - int sourceY = rects[offset + 1]; - mTempSource.set(sourceX, sourceY, sourceX + rects[offset + 2], - sourceY + rects[offset + 3]); - mTempTarget.set(x, y, x + width, y + height); - drawMixed(canvas, color, ratio, mTempSource, mTempTarget); - } -} diff --git a/app/src/main/java/com/hippo/glview/image/ImageTexture.java b/app/src/main/java/com/hippo/glview/image/ImageTexture.java deleted file mode 100644 index ba444d252..000000000 --- a/app/src/main/java/com/hippo/glview/image/ImageTexture.java +++ /dev/null @@ -1,678 +0,0 @@ -/* - * Copyright (C) 2015 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.image; - -import android.graphics.RectF; -import android.graphics.drawable.Animatable; -import android.os.Process; -import android.os.SystemClock; - -import androidx.annotation.IntDef; -import androidx.annotation.NonNull; - -import com.hippo.glview.annotation.RenderThread; -import com.hippo.glview.glrenderer.GLCanvas; -import com.hippo.glview.glrenderer.NativeTexture; -import com.hippo.glview.glrenderer.Texture; -import com.hippo.glview.view.GLRoot; -import com.hippo.yorozuya.thread.InfiniteThreadExecutor; -import com.hippo.yorozuya.thread.PVLock; -import com.hippo.yorozuya.thread.PriorityThreadFactory; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.ref.WeakReference; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicBoolean; - -public class ImageTexture implements Texture, Animatable { - - private static final int TILE_SMALL = 0; - private static final int TILE_LARGE = 1; - private static final int SMALL_CONTENT_SIZE = 254; - private static final int SMALL_BORDER_SIZE = 1; - private static final int SMALL_TILE_SIZE = SMALL_CONTENT_SIZE + 2 * SMALL_BORDER_SIZE; - private static final int LARGE_CONTENT_SIZE = SMALL_CONTENT_SIZE * 2; - private static final int LARGE_BORDER_SIZE = SMALL_BORDER_SIZE * 2; - private static final int LARGE_TILE_SIZE = LARGE_CONTENT_SIZE + 2 * LARGE_BORDER_SIZE; - private static final int INIT_CAPACITY = 8; - // We are targeting at 60fps, so we have 16ms for each frame. - // In this 16ms, we use about 4~8 ms to upload tiles. - private static final long UPLOAD_TILE_LIMIT = 4; // ms - private static final Executor sThreadExecutor; - private static final PVLock sPVLock; - private static final Object sFreeTileLock = new Object(); - private static Tile sSmallFreeTileHead = null; - private static Tile sLargeFreeTileHead = null; - - static { - sThreadExecutor = new InfiniteThreadExecutor(10 * 1000, new LinkedList(), - new PriorityThreadFactory("ImageTexture$AnimateTask", Process.THREAD_PRIORITY_BACKGROUND)); - sPVLock = new PVLock(3); - } - - private final ImageWrapper mImage; - private final Tile[] mTiles; // Can be modified in different threads. - private final int mWidth; - // Should be protected by "synchronized." - private final int mHeight; - private final boolean mOpaque; - private final RectF mSrcRect = new RectF(); - private final RectF mDestRect = new RectF(); - private final AtomicBoolean mRunning = new AtomicBoolean(); - private final AtomicBoolean mRequestAnimation = new AtomicBoolean(); - private final AtomicBoolean mFrameDirty = new AtomicBoolean(); - private final AtomicBoolean mNeedRelease = new AtomicBoolean(); - private final AtomicBoolean mReleased = new AtomicBoolean(); - private int mUploadIndex = 0; - private boolean mImageBusy = false; - private Runnable mAnimateRunnable = null; - - private WeakReference mCallback; - - /** - * Call {@link ImageWrapper#obtain()} first - */ - public ImageTexture(@NonNull ImageWrapper image) { - mImage = image; - int width = mWidth = image.getWidth(); - int height = mHeight = image.getHeight(); - boolean opaque = mOpaque = image.isOpaque(); - ArrayList list = new ArrayList<>(); - - for (int x = 0; x < width; x += LARGE_CONTENT_SIZE) { - for (int y = 0; y < height; y += LARGE_CONTENT_SIZE) { - int w = Math.min(LARGE_CONTENT_SIZE, width - x); - int h = Math.min(LARGE_CONTENT_SIZE, height - y); - - if (w <= SMALL_CONTENT_SIZE) { - Tile tile = obtainSmallTile(); - tile.offsetX = x; - tile.offsetY = y; - tile.image = image; - tile.setSize(TILE_SMALL, w, Math.min(SMALL_CONTENT_SIZE, h)); - tile.setOpaque(opaque); - list.add(tile); - - int nextHeight = h - SMALL_CONTENT_SIZE; - if (nextHeight > 0) { - Tile nextTile = obtainSmallTile(); - nextTile.offsetX = x; - nextTile.offsetY = y + SMALL_CONTENT_SIZE; - nextTile.image = image; - nextTile.setSize(TILE_SMALL, w, nextHeight); - nextTile.setOpaque(opaque); - list.add(nextTile); - } - } else if (h <= SMALL_CONTENT_SIZE) { - Tile tile = obtainSmallTile(); - tile.offsetX = x; - tile.offsetY = y; - tile.image = image; - tile.setSize(TILE_SMALL, Math.min(SMALL_CONTENT_SIZE, w), h); - tile.setOpaque(opaque); - list.add(tile); - - int nextWidth = w - SMALL_CONTENT_SIZE; - if (nextWidth > 0) { - Tile nextTile = obtainSmallTile(); - nextTile.offsetX = x + SMALL_CONTENT_SIZE; - nextTile.offsetY = y; - nextTile.image = image; - nextTile.setSize(TILE_SMALL, nextWidth, h); - nextTile.setOpaque(opaque); - list.add(nextTile); - } - } else { - Tile tile = obtainLargeTile(); - tile.offsetX = x; - tile.offsetY = y; - tile.image = image; - tile.setSize(TILE_LARGE, w, h); - tile.setOpaque(opaque); - list.add(tile); - } - } - } - - mTiles = list.toArray(new Tile[list.size()]); - } - - private static Tile obtainSmallTile() { - synchronized (sFreeTileLock) { - Tile result = sSmallFreeTileHead; - if (result == null) { - return new Tile(); - } else { - sSmallFreeTileHead = result.nextFreeTile; - result.nextFreeTile = null; - } - return result; - } - } - - private static Tile obtainLargeTile() { - synchronized (sFreeTileLock) { - Tile result = sLargeFreeTileHead; - if (result == null) { - return new Tile(); - } else { - sLargeFreeTileHead = result.nextFreeTile; - result.nextFreeTile = null; - } - return result; - } - } - - // We want to draw the "source" on the "target". - // This method is to find the "output" rectangle which is - // the corresponding area of the "src". - // (x,y) target - // (x0,y0) source +---------------+ - // +----------+ | | - // | src | | output | - // | +--+ | linear map | +----+ | - // | +--+ | ----------> | | | | - // | | by (scaleX, scaleY) | +----+ | - // +----------+ | | - // Texture +---------------+ - // Canvas - private static void mapRect(RectF output, - RectF src, float x0, float y0, float x, float y, float scaleX, - float scaleY) { - output.set(x + (src.left - x0) * scaleX, - y + (src.top - y0) * scaleY, - x + (src.right - x0) * scaleX, - y + (src.bottom - y0) * scaleY); - } - - public Callback getCallback() { - if (mCallback != null) { - return mCallback.get(); - } - return null; - } - - public final void setCallback(Callback cb) { - mCallback = new WeakReference<>(cb); - } - - public void invalidateSelf() { - final Callback callback = getCallback(); - if (callback != null) { - callback.invalidateImageTexture(this); - } - } - - @Override - public void start() { - synchronized (mImage) { - if (!mImageBusy) { - mImageBusy = true; - } else { - mRequestAnimation.lazySet(true); - return; - } - } - - boolean end = mReleased.get() || mImage.isImageRecycled() || mNeedRelease.get() || - (!mImage.getAnimated()) || mRunning.get(); - - synchronized (mImage) { - mImageBusy = false; - } - - if (end) { - return; - } - - mRunning.lazySet(true); - - synchronized (mImage) { - if (mAnimateRunnable == null) { - Runnable runnable = new AnimateRunnable(); - mAnimateRunnable = runnable; - sThreadExecutor.execute(runnable); - } - } - } - - @Override - public void stop() { - mRunning.lazySet(false); - mRequestAnimation.lazySet(false); - } - - @Override - public boolean isRunning() { - return mRunning.get(); - } - - private boolean uploadNextTile(GLCanvas canvas) { - if (mUploadIndex == mTiles.length) return true; - - synchronized (mTiles) { - Tile next = mTiles[mUploadIndex++]; - - // Make sure tile has not already been recycled by the time - // this is called (race condition in onGLIdle) - if (next.image != null) { - boolean hasBeenLoad = next.isLoaded(); - next.updateContent(canvas); - - // It will take some time for a texture to be drawn for the first - // time. When scrolling, we need to draw several tiles on the screen - // at the same time. It may cause a UI jank even these textures has - // been uploaded. - if (!hasBeenLoad) next.draw(canvas, 0, 0); - } - } - return mUploadIndex == mTiles.length; - } - - @Override - public int getWidth() { - return mWidth; - } - - @Override - public int getHeight() { - return mHeight; - } - - @RenderThread - private void syncFrame() { - if (mFrameDirty.getAndSet(false)) { - // invalid tiles - for (Tile tile : mTiles) { - tile.invalidateContent(); - } - } - } - - @Override - public void draw(GLCanvas canvas, int x, int y) { - draw(canvas, x, y, mWidth, mHeight); - } - - // Draws the texture on to the specified rectangle. - @Override - public void draw(GLCanvas canvas, int x, int y, int w, int h) { - RectF src = mSrcRect; - RectF dest = mDestRect; - float scaleX = (float) w / mWidth; - float scaleY = (float) h / mHeight; - - syncFrame(); - for (Tile t : mTiles) { - src.set(0, 0, t.contentWidth, t.contentHeight); - src.offset(t.offsetX, t.offsetY); - mapRect(dest, src, 0, 0, x, y, scaleX, scaleY); - src.offset(t.borderSize - t.offsetX, t.borderSize - t.offsetY); - canvas.drawTexture(t, src, dest); - } - } - - // Draws a sub region of this texture on to the specified rectangle. - @Override - public void draw(GLCanvas canvas, RectF source, RectF target) { - RectF src = mSrcRect; - RectF dest = mDestRect; - float x0 = source.left; - float y0 = source.top; - float x = target.left; - float y = target.top; - float scaleX = target.width() / source.width(); - float scaleY = target.height() / source.height(); - - syncFrame(); - for (Tile t : mTiles) { - src.set(0, 0, t.contentWidth, t.contentHeight); - src.offset(t.offsetX, t.offsetY); - if (!src.intersect(source)) { - continue; - } - mapRect(dest, src, x0, y0, x, y, scaleX, scaleY); - src.offset(t.borderSize - t.offsetX, t.borderSize - t.offsetY); - canvas.drawTexture(t, src, dest); - } - } - - // Draws a mixed color of this texture and a specified color onto the - // a rectangle. The used color is: from * (1 - ratio) + to * ratio. - public void drawMixed(GLCanvas canvas, int color, float ratio, - int x, int y, int width, int height) { - RectF src = mSrcRect; - RectF dest = mDestRect; - float scaleX = (float) width / mWidth; - float scaleY = (float) height / mHeight; - - syncFrame(); - for (Tile t : mTiles) { - src.set(0, 0, t.contentWidth, t.contentHeight); - src.offset(t.offsetX, t.offsetY); - mapRect(dest, src, 0, 0, x, y, scaleX, scaleY); - src.offset(t.borderSize - t.offsetX, t.borderSize - t.offsetY); - canvas.drawMixed(t, color, ratio, src, dest); - } - } - - public void drawMixed(GLCanvas canvas, int color, float ratio, - RectF source, RectF target) { - RectF src = mSrcRect; - RectF dest = mDestRect; - float x0 = source.left; - float y0 = source.top; - float x = target.left; - float y = target.top; - float scaleX = target.width() / source.width(); - float scaleY = target.height() / source.height(); - - syncFrame(); - for (Tile t : mTiles) { - src.set(0, 0, t.contentWidth, t.contentHeight); - src.offset(t.offsetX, t.offsetY); - if (!src.intersect(source)) { - continue; - } - mapRect(dest, src, x0, y0, x, y, scaleX, scaleY); - src.offset(t.borderSize - t.offsetX, t.borderSize - t.offsetY); - canvas.drawMixed(t, color, ratio, src, dest); - } - } - - @Override - public boolean isOpaque() { - return mOpaque; - } - - public boolean isReady() { - return mUploadIndex == mTiles.length; - } - - public void recycle() { - mRunning.lazySet(false); - - for (Tile mTile : mTiles) { - mTile.free(); - } - - boolean releaseNow; - - synchronized (mImage) { - if (!mImageBusy) { - releaseNow = true; - mImageBusy = true; - } else { - releaseNow = false; - mNeedRelease.lazySet(true); - } - } - - if (releaseNow) { - if (!mReleased.get()) { - mImage.release(); - mReleased.lazySet(true); - } - synchronized (mImage) { - mImageBusy = false; - } - } - } - - @IntDef({TILE_SMALL, TILE_LARGE}) - @Retention(RetentionPolicy.SOURCE) - private @interface TileType { - } - - public interface Callback { - void invalidateImageTexture(ImageTexture who); - } - - public static class Uploader implements GLRoot.OnGLIdleListener { - private final ArrayDeque mTextures = - new ArrayDeque<>(INIT_CAPACITY); - - private final GLRoot mGlRoot; - private boolean mIsQueued = false; - - public Uploader(GLRoot glRoot) { - mGlRoot = glRoot; - } - - public synchronized void clear() { - mTextures.clear(); - } - - public synchronized void addTexture(ImageTexture t) { - if (t.isReady()) return; - mTextures.addLast(t); - - if (mIsQueued) return; - mIsQueued = true; - mGlRoot.addOnGLIdleListener(this); - } - - @Override - public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) { - ArrayDeque deque = mTextures; - synchronized (this) { - long now = SystemClock.uptimeMillis(); - long dueTime = now + UPLOAD_TILE_LIMIT; - while (now < dueTime && !deque.isEmpty()) { - ImageTexture t = deque.peekFirst(); - if (t.uploadNextTile(canvas)) { - deque.removeFirst(); - mGlRoot.requestRender(); - } - now = SystemClock.uptimeMillis(); - } - mIsQueued = !mTextures.isEmpty(); - - // return true to keep this listener in the queue - return mIsQueued; - } - } - } - - private static class Tile extends NativeTexture { - - public int offsetX; - public int offsetY; - public ImageWrapper image; - public Tile nextFreeTile; - public int contentWidth; - public int contentHeight; - public int borderSize; - @TileType - private int mTileType; - - private static void freeSmallTile(Tile tile) { - tile.invalidate(); - synchronized (sFreeTileLock) { - tile.nextFreeTile = sSmallFreeTileHead; - sSmallFreeTileHead = tile; - } - } - - private static void freeLargeTile(Tile tile) { - tile.invalidate(); - synchronized (sFreeTileLock) { - tile.nextFreeTile = sLargeFreeTileHead; - sLargeFreeTileHead = tile; - } - } - - public void setSize(@TileType int tileType, int width, int height) { - mTileType = tileType; - int tileSize; - if (tileType == TILE_SMALL) { - borderSize = SMALL_BORDER_SIZE; - tileSize = SMALL_TILE_SIZE; - } else if (tileType == TILE_LARGE) { - borderSize = LARGE_BORDER_SIZE; - tileSize = LARGE_TILE_SIZE; - } else { - throw new IllegalStateException("Not support tile type: " + tileType); - } - contentWidth = width; - contentHeight = height; - - mWidth = width + 2 * borderSize; - mHeight = height + 2 * borderSize; - mTextureWidth = tileSize; - mTextureHeight = tileSize; - } - - @Override - protected void texImage(boolean init) { - if (image != null && !image.isRecycled()) { - int w, h; - if (init) { - w = mTextureWidth; - h = mTextureHeight; - } else { - w = mWidth; - h = mHeight; - } - image.texImage(init, offsetX - borderSize, offsetY - borderSize, w, h); - } - } - - private void invalidate() { - invalidateContent(); - image = null; - } - - public void free() { - switch (mTileType) { - case TILE_SMALL: - freeSmallTile(this); - break; - case TILE_LARGE: - freeLargeTile(this); - break; - default: - throw new IllegalStateException("Not support tile type: " + mTileType); - } - } - } - - private class AnimateRunnable implements Runnable { - - public void doRun() { - long lastTime = System.nanoTime(); - long lastDelay = -1L; - - synchronized (mImage) { - // Check released, image busy, Need release - if (mReleased.get() || mImage.isImageRecycled() || mImageBusy || mNeedRelease.get()) { - mAnimateRunnable = null; - return; - } - // Obtain image - mImageBusy = true; - } - - synchronized (mImage) { - // Release image - mImageBusy = false; - // Check need release, frameCount <= 1 - if (mNeedRelease.get() || !mImage.getAnimated()) { - mAnimateRunnable = null; - return; - } - } - - if (mRequestAnimation.get()) { - mRunning.lazySet(true); - } - - for (; ; ) { - // Obtain - synchronized (mImage) { - // Check released, image busy, Need release, not running - if (mReleased.get() || mImage.isImageRecycled() || mImageBusy || mNeedRelease.get() || !mRunning.get()) { - mAnimateRunnable = null; - return; - } - // Obtain image - mImageBusy = true; - } - - mImage.start(); - long delay = mImage.getDelay(); - long time = System.nanoTime(); - if (-1L != lastDelay) { - delay -= (time - lastTime) / 1000000 - lastDelay; - } - lastTime = time; - lastDelay = delay; - mFrameDirty.lazySet(true); - invalidateSelf(); - - synchronized (mImage) { - // Release image - mImageBusy = false; - } - - if (delay > 0) { - try { - Thread.sleep(delay); - } catch (InterruptedException e) { - // Ignore - } - } - } - } - - @Override - public void run() { - doRun(); - - while (mNeedRelease.get()) { - // Obtain - synchronized (mImage) { - // Check released - if (mReleased.get() || mImage.isImageRecycled()) { - break; - } - // Check image busy - if (mImageBusy) { - // Image is busy, means it is recycling - break; - } - // Obtain image - mImageBusy = true; - } - - if (!mReleased.get()) { - mImage.release(); - mReleased.lazySet(true); - } - - synchronized (mImage) { - // Release image - mImageBusy = false; - } - } - } - } -} diff --git a/app/src/main/java/com/hippo/glview/image/ImageWrapper.java b/app/src/main/java/com/hippo/glview/image/ImageWrapper.java deleted file mode 100644 index 3b700b942..000000000 --- a/app/src/main/java/com/hippo/glview/image/ImageWrapper.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.image; - -import android.graphics.Rect; -import android.util.Log; - -import androidx.annotation.NonNull; - -import com.hippo.image.Image; - -/** - * A wrapper for {@link Image}. It is useful for multi-usage. - * It handles image recycle automatically. - */ -public class ImageWrapper { - - private static final String LOG_TAG = "ImageWrapper"; - - private final Image mImage; - private final Rect mCut; - private int mReferences; - - /** - * Create ImageWrapper - * - * @param image the image should not be obtained or recycled. - */ - public ImageWrapper(@NonNull Image image) { - mImage = image; - mCut = new Rect(0, 0, image.getWidth(), image.getHeight()); - } - - /** - * Cuts this image to a specified region. - * If the region is out of the image size, clamp the region. - */ - public void setCutRect(int left, int top, int right, int bottom) { - mCut.left = Math.max(0, left); - mCut.top = Math.max(0, top); - mCut.right = Math.min(mImage.getWidth(), right); - mCut.bottom = Math.min(mImage.getHeight(), bottom); - - if (mCut.isEmpty()) { - // Empty mCut has unspecified behavior - Log.e(LOG_TAG, "Cut rect is empty"); - mCut.set(0, 0, mImage.getWidth(), mImage.getHeight()); - } - } - - /** - * Cuts this image to a specified region. - * The region is described in percent, {@code [0.0f, 1.0f]}. - * If the region is out of the image size, clamp the region. - */ - public void setCutPercent(float left, float top, float right, float bottom) { - setCutRect((int) (getWidth() * left), (int) (getHeight() * top), - (int) (getWidth() * right), (int) (getHeight() * bottom)); - } - - /** - * Obtain the image - * - * @return false for the image is recycled and obtain failed - */ - public synchronized boolean obtain() { - if (mImage.isRecycled()) { - return false; - } else { - ++mReferences; - return true; - } - } - - /** - * Release the image - */ - public synchronized void release() { - --mReferences; - if (mReferences <= 0 && !mImage.isRecycled()) { - mImage.recycle(); - } - } - - public boolean isImageRecycled() { - return mImage.isRecycled(); - } - - /** - * @see Image#getAnimated() - */ - public Boolean getAnimated() { - return mImage.getAnimated(); - } - - /** - * @see Image#getAnimated() - */ - public int getWidth() { - return mCut.width(); - } - - /** - * @see Image#getHeight() - */ - public int getHeight() { - return mCut.height(); - } - - /** - * @see Image#texImage(boolean, int, int, int, int) - */ - public void texImage(boolean init, int offsetX, int offsetY, int width, int height) { - mImage.texImage(init, offsetX + mCut.left, offsetY + mCut.top, width, height); - } - - /** - * @see Image#start() - */ - public void start() { - mImage.start(); - } - - /** - * @see Image#getDelay() - */ - public int getDelay() { - return mImage.getDelay(); - } - - /** - * @see Image#isOpaque() - */ - public boolean isOpaque() { - return mImage.isOpaque(); - } - - /** - * @see Image#isRecycled() - */ - public boolean isRecycled() { - return mImage.isRecycled(); - } -} diff --git a/app/src/main/java/com/hippo/glview/util/ApiHelper.java b/app/src/main/java/com/hippo/glview/util/ApiHelper.java deleted file mode 100644 index 5f8737e6e..000000000 --- a/app/src/main/java/com/hippo/glview/util/ApiHelper.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.util; - -import android.app.admin.DevicePolicyManager; -import android.content.ComponentName; -import android.hardware.Camera; -import android.os.Build; -import android.provider.MediaStore.MediaColumns; -import android.view.View; -import android.view.WindowManager; - -import java.lang.reflect.Field; - -public class ApiHelper { - public static final boolean AT_LEAST_16 = Build.VERSION.SDK_INT >= 16; - public static final boolean USE_888_PIXEL_FORMAT = - Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; - public static final boolean ENABLE_PHOTO_EDITOR = - Build.VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH; - public static final boolean HAS_VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE = - hasField(View.class, "SYSTEM_UI_FLAG_LAYOUT_STABLE"); - public static final boolean HAS_VIEW_SYSTEM_UI_FLAG_HIDE_NAVIGATION = - hasField(View.class, "SYSTEM_UI_FLAG_HIDE_NAVIGATION"); - public static final boolean HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT = - hasField(MediaColumns.class, "WIDTH"); - public static final boolean HAS_REUSING_BITMAP_IN_BITMAP_REGION_DECODER = - Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; - public static final boolean HAS_REUSING_BITMAP_IN_BITMAP_FACTORY = - Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; - public static final boolean HAS_SET_BEAM_PUSH_URIS = - Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; - public static final boolean HAS_SET_DEFALT_BUFFER_SIZE = hasMethod( - "android.graphics.SurfaceTexture", "setDefaultBufferSize", - int.class, int.class); - public static final boolean HAS_RELEASE_SURFACE_TEXTURE = hasMethod( - "android.graphics.SurfaceTexture", "release"); - public static final boolean HAS_SURFACE_TEXTURE = - Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; - public static final boolean HAS_MTP = - Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1; - public static final boolean HAS_AUTO_FOCUS_MOVE_CALLBACK = - Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; - public static final boolean HAS_REMOTE_VIEWS_SERVICE = - Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; - public static final boolean HAS_INTENT_EXTRA_LOCAL_ONLY = - Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; - public static final boolean HAS_SET_SYSTEM_UI_VISIBILITY = - hasMethod(View.class, "setSystemUiVisibility", int.class); - public static final boolean HAS_FACE_DETECTION; - public static final boolean HAS_GET_CAMERA_DISABLED = - hasMethod(DevicePolicyManager.class, "getCameraDisabled", ComponentName.class); - public static final boolean HAS_MEDIA_ACTION_SOUND = - Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; - public static final boolean HAS_TIME_LAPSE_RECORDING = - Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; - public static final boolean HAS_ZOOM_WHEN_RECORDING = - Build.VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH; - public static final boolean HAS_CAMERA_FOCUS_AREA = - Build.VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH; - public static final boolean HAS_CAMERA_METERING_AREA = - Build.VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH; - public static final boolean HAS_MOTION_EVENT_TRANSFORM = - Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; - public static final boolean HAS_EFFECTS_RECORDING = false; - // "Background" filter does not have "context" input port in jelly bean. - public static final boolean HAS_EFFECTS_RECORDING_CONTEXT_INPUT = - Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1; - public static final boolean HAS_GET_SUPPORTED_VIDEO_SIZE = - Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; - public static final boolean HAS_SET_ICON_ATTRIBUTE = - Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; - public static final boolean HAS_MEDIA_PROVIDER_FILES_TABLE = - Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; - public static final boolean HAS_SURFACE_TEXTURE_RECORDING = - Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; - public static final boolean HAS_ACTION_BAR = - Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; - // Ex: View.setTranslationX. - public static final boolean HAS_VIEW_TRANSFORM_PROPERTIES = - Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; - public static final boolean HAS_CAMERA_HDR = - Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1; - public static final boolean HAS_OPTIONS_IN_MUTABLE = - Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; - public static final boolean CAN_START_PREVIEW_IN_JPEG_CALLBACK = - Build.VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH; - public static final boolean HAS_VIEW_PROPERTY_ANIMATOR = - Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1; - public static final boolean HAS_POST_ON_ANIMATION = - Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; - public static final boolean HAS_ANNOUNCE_FOR_ACCESSIBILITY = - Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; - public static final boolean HAS_OBJECT_ANIMATION = - Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; - public static final boolean HAS_GLES20_REQUIRED = - Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; - public static final boolean HAS_ROTATION_ANIMATION = - hasField(WindowManager.LayoutParams.class, "rotationAnimation"); - public static final boolean HAS_ORIENTATION_LOCK = - Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2; - public static final boolean HAS_CANCELLATION_SIGNAL = - Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; - public static final boolean HAS_MEDIA_MUXER = - Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2; - public static final boolean HAS_DISPLAY_LISTENER = - Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1; - - static { - boolean hasFaceDetection = false; - try { - Class listenerClass = Class.forName( - "android.hardware.Camera$FaceDetectionListener"); - hasFaceDetection = - hasMethod(Camera.class, "setFaceDetectionListener", listenerClass) && - hasMethod(Camera.class, "startFaceDetection") && - hasMethod(Camera.class, "stopFaceDetection") && - hasMethod(Camera.Parameters.class, "getMaxNumDetectedFaces"); - } catch (Throwable t) { - } - HAS_FACE_DETECTION = hasFaceDetection; - } - - public static int getIntFieldIfExists(Class klass, String fieldName, - Class obj, int defaultVal) { - try { - Field f = klass.getDeclaredField(fieldName); - return f.getInt(obj); - } catch (Exception e) { - return defaultVal; - } - } - - private static boolean hasField(Class klass, String fieldName) { - try { - klass.getDeclaredField(fieldName); - return true; - } catch (NoSuchFieldException e) { - return false; - } - } - - private static boolean hasMethod(String className, String methodName, - Class... parameterTypes) { - try { - Class klass = Class.forName(className); - klass.getDeclaredMethod(methodName, parameterTypes); - return true; - } catch (Throwable th) { - return false; - } - } - - private static boolean hasMethod( - Class klass, String methodName, Class... paramTypes) { - try { - klass.getDeclaredMethod(methodName, paramTypes); - return true; - } catch (NoSuchMethodException e) { - return false; - } - } - - public interface VERSION_CODES { - // These value are copied from Build.VERSION_CODES - int GINGERBREAD_MR1 = 10; - int HONEYCOMB = 11; - int HONEYCOMB_MR1 = 12; - int HONEYCOMB_MR2 = 13; - int ICE_CREAM_SANDWICH = 14; - int ICE_CREAM_SANDWICH_MR1 = 15; - int JELLY_BEAN = 16; - int JELLY_BEAN_MR1 = 17; - int JELLY_BEAN_MR2 = 18; - } -} diff --git a/app/src/main/java/com/hippo/glview/util/GalleryUtils.java b/app/src/main/java/com/hippo/glview/util/GalleryUtils.java deleted file mode 100644 index 654fcf466..000000000 --- a/app/src/main/java/com/hippo/glview/util/GalleryUtils.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.util; - -import android.graphics.Color; - -import com.hippo.yorozuya.AssertError; - -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - -public class GalleryUtils { - - private static final String TAG = "GalleryUtils"; - private static final List sRenderThreads = new CopyOnWriteArrayList<>(); - - public static float[] intColorToFloatARGBArray(int from) { - return new float[]{ - Color.alpha(from) / 255f, - Color.red(from) / 255f, - Color.green(from) / 255f, - Color.blue(from) / 255f - }; - } - - // Below are used the detect using database in the render thread. It only - // works most of the time, but that's ok because it's for debugging only. - - public static int floatARGBArrayTointColor(float[] from) { - return Color.argb((int) (from[0] * 255), (int) (from[1] * 255), (int) (from[2] * 255), (int) (from[3] * 255)); - } - - public static void setRenderThread() { - sRenderThreads.add(Thread.currentThread().hashCode()); - } - - public static boolean isRenderThread() { - return sRenderThreads.contains(Thread.currentThread().hashCode()); - } - - public static void assertInRenderThread() { - if (!isRenderThread()) { - throw new AssertError("Should not do this in non-render thread"); - } - } -} diff --git a/app/src/main/java/com/hippo/glview/util/MotionEventHelper.java b/app/src/main/java/com/hippo/glview/util/MotionEventHelper.java deleted file mode 100644 index 297f47bfe..000000000 --- a/app/src/main/java/com/hippo/glview/util/MotionEventHelper.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hippo.glview.util; - -import android.annotation.TargetApi; -import android.graphics.Matrix; -import android.view.MotionEvent; -import android.view.MotionEvent.PointerCoords; - -public final class MotionEventHelper { - private MotionEventHelper() { - } - - public static MotionEvent transformEvent(MotionEvent e, Matrix m) { - // We try to use the new transform method if possible because it uses - // less memory. - if (ApiHelper.HAS_MOTION_EVENT_TRANSFORM) { - return transformEventNew(e, m); - } else { - return transformEventOld(e, m); - } - } - - @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) - private static MotionEvent transformEventNew(MotionEvent e, Matrix m) { - MotionEvent newEvent = MotionEvent.obtain(e); - newEvent.transform(m); - return newEvent; - } - - // This is copied from Input.cpp in the android framework. - private static MotionEvent transformEventOld(MotionEvent e, Matrix m) { - long downTime = e.getDownTime(); - long eventTime = e.getEventTime(); - int action = e.getAction(); - int pointerCount = e.getPointerCount(); - int[] pointerIds = getPointerIds(e); - PointerCoords[] pointerCoords = getPointerCoords(e); - int metaState = e.getMetaState(); - float xPrecision = e.getXPrecision(); - float yPrecision = e.getYPrecision(); - int deviceId = e.getDeviceId(); - int edgeFlags = e.getEdgeFlags(); - int source = e.getSource(); - int flags = e.getFlags(); - - // Copy the x and y coordinates into an array, map them, and copy back. - float[] xy = new float[pointerCoords.length * 2]; - for (int i = 0; i < pointerCount; i++) { - xy[2 * i] = pointerCoords[i].x; - xy[2 * i + 1] = pointerCoords[i].y; - } - m.mapPoints(xy); - for (int i = 0; i < pointerCount; i++) { - pointerCoords[i].x = xy[2 * i]; - pointerCoords[i].y = xy[2 * i + 1]; - pointerCoords[i].orientation = transformAngle( - m, pointerCoords[i].orientation); - } - - MotionEvent n = MotionEvent.obtain(downTime, eventTime, action, - pointerCount, pointerIds, pointerCoords, metaState, xPrecision, - yPrecision, deviceId, edgeFlags, source, flags); - - return n; - } - - private static int[] getPointerIds(MotionEvent e) { - int n = e.getPointerCount(); - int[] r = new int[n]; - for (int i = 0; i < n; i++) { - r[i] = e.getPointerId(i); - } - return r; - } - - private static PointerCoords[] getPointerCoords(MotionEvent e) { - int n = e.getPointerCount(); - PointerCoords[] r = new PointerCoords[n]; - for (int i = 0; i < n; i++) { - r[i] = new PointerCoords(); - e.getPointerCoords(i, r[i]); - } - return r; - } - - private static float transformAngle(Matrix m, float angleRadians) { - // Construct and transform a vector oriented at the specified clockwise - // angle from vertical. Coordinate system: down is increasing Y, right is - // increasing X. - float[] v = new float[2]; - v[0] = (float) Math.sin(angleRadians); - v[1] = (float) -Math.cos(angleRadians); - m.mapVectors(v); - - // Derive the transformed vector's clockwise angle from vertical. - float result = (float) Math.atan2(v[0], -v[1]); - if (result < -Math.PI / 2) { - result += Math.PI; - } else if (result > Math.PI / 2) { - result -= Math.PI; - } - return result; - } -} diff --git a/app/src/main/java/com/hippo/glview/view/AnimationTime.java b/app/src/main/java/com/hippo/glview/view/AnimationTime.java deleted file mode 100644 index eba37c949..000000000 --- a/app/src/main/java/com/hippo/glview/view/AnimationTime.java +++ /dev/null @@ -1,45 +0,0 @@ - -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.view; - -import android.os.SystemClock; - -// -// The animation time should ideally be the vsync time the frame will be -// displayed, but that is an unknown time in the future. So we use the system -// time just after eglSwapBuffers (when GLSurfaceView.onDrawFrame is called) -// as a approximation. -// -public class AnimationTime { - private static volatile long sTime; - - // Sets current time as the animation time. - public static void update() { - sTime = SystemClock.uptimeMillis(); - } - - // Returns the animation time. - public static long get() { - return sTime; - } - - public static long startTime() { - sTime = SystemClock.uptimeMillis(); - return sTime; - } -} diff --git a/app/src/main/java/com/hippo/glview/view/GLRoot.java b/app/src/main/java/com/hippo/glview/view/GLRoot.java deleted file mode 100644 index 98766ef47..000000000 --- a/app/src/main/java/com/hippo/glview/view/GLRoot.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.view; - -import android.content.Context; -import android.graphics.Matrix; - -import com.hippo.glview.anim.CanvasAnimation; -import com.hippo.glview.glrenderer.GLCanvas; - -public interface GLRoot { - - void addOnGLIdleListener(OnGLIdleListener listener); - - void registerLaunchedAnimation(CanvasAnimation animation); - - void requestRenderForced(); - - void requestRender(); - - void requestLayoutContentPane(); - - void lockRenderThread(); - - void unlockRenderThread(); - - void setContentPane(GLView content); - - void setOrientationSource(OrientationSource source); - - int getDisplayRotation(); - - int getCompensation(); - - Matrix getCompensationMatrix(); - - void freeze(); - - void unfreeze(); - - void setLightsOutMode(boolean enabled); - - Context getContext(); - - int getWidth(); - - int getHeight(); - - // Listener will be called when GL is idle AND before each frame. - // Mainly used for uploading textures. - interface OnGLIdleListener { - boolean onGLIdle(GLCanvas canvas, boolean renderRequested); - } -} diff --git a/app/src/main/java/com/hippo/glview/view/GLRootView.java b/app/src/main/java/com/hippo/glview/view/GLRootView.java deleted file mode 100644 index 762930603..000000000 --- a/app/src/main/java/com/hippo/glview/view/GLRootView.java +++ /dev/null @@ -1,710 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.view; - -import android.content.Context; -import android.graphics.Matrix; -import android.graphics.PixelFormat; -import android.opengl.GLSurfaceView; -import android.opengl.GLUtils; -import android.os.Parcelable; -import android.os.Process; -import android.os.SystemClock; -import android.util.AttributeSet; -import android.util.Log; -import android.util.SparseArray; -import android.view.MotionEvent; -import android.view.SurfaceHolder; - -import androidx.annotation.NonNull; - -import com.hippo.glview.anim.CanvasAnimation; -import com.hippo.glview.glrenderer.BasicTexture; -import com.hippo.glview.glrenderer.GLCanvas; -import com.hippo.glview.glrenderer.GLES11Canvas; -import com.hippo.glview.glrenderer.GLES20Canvas; -import com.hippo.glview.glrenderer.UploadedTexture; -import com.hippo.glview.util.ApiHelper; -import com.hippo.glview.util.GalleryUtils; -import com.hippo.glview.util.MotionEventHelper; -import com.hippo.yorozuya.AssertUtils; - -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.ReentrantLock; - -import javax.microedition.khronos.egl.EGL10; -import javax.microedition.khronos.egl.EGLConfig; -import javax.microedition.khronos.egl.EGLContext; -import javax.microedition.khronos.egl.EGLDisplay; -import javax.microedition.khronos.opengles.GL10; -import javax.microedition.khronos.opengles.GL11; - -// TODO Call attachToRoot and detachFromRoot in render thread -// The root component of all GLViews. The rendering is done in GL -// thread while the event handling is done in the main thread. To synchronize -// the two threads, the entry points of this package need to synchronize on the -// GLRootView instance unless it can be proved that the rendering -// thread won't access the same thing as the method. The entry points include: -// (1) The public methods of HeadUpDisplay -// (2) The public methods of CameraHeadUpDisplay -// (3) The overridden methods in GLRootView. -public class GLRootView extends GLSurfaceView - implements GLRoot { - private static final String TAG = "GLRootView"; - - private static final boolean DEBUG_FPS = false; - private static final boolean DEBUG_INVALIDATE = false; - private static final boolean DEBUG_DRAWING_STAT = false; - private static final int FLAG_INITIALIZED = 1; - private static final int FLAG_NEED_LAYOUT = 2; - // mCompensationMatrix maps the coordinates of touch events. It is kept sync - // with mCompensation. - private final Matrix mCompensationMatrix = new Matrix(); - private final ArrayList mAnimations = - new ArrayList<>(); - private final ArrayDeque mIdleListeners = - new ArrayDeque<>(); - private final IdleRunner mIdleRunner = new IdleRunner(); - private final ReentrantLock mRenderLock = new ReentrantLock(); - private final Condition mFreezeCondition = - mRenderLock.newCondition(); - private final Runnable mRequestRenderOnAnimationFrame = this::superRequestRender; - private int mFrameCount = 0; - private long mFrameCountingStart = 0; - private int mInvalidateColor = 0; - private GL11 mGL; - private GLCanvas mCanvas; - private GLView mContentView; - private OrientationSource mOrientationSource; - // mCompensation is the difference between the UI orientation on GLCanvas - // and the framework orientation. See OrientationManager for details. - private int mCompensation; - private int mDisplayRotation; - private int mFlags = FLAG_NEED_LAYOUT; - private volatile boolean mRenderRequested = false; - private boolean mFreeze; - private boolean mInDownState = false; - private int mEGLContextClientVersion; - - public GLRootView(Context context) { - this(context, null); - } - - @SuppressWarnings("deprecation") - public GLRootView(Context context, AttributeSet attrs) { - super(context, attrs); - mFlags |= FLAG_INITIALIZED; - setBackgroundDrawable(null); - - setEGLConfigChooser(new ConfigChooser()); - setEGLContextFactory(new ContextFactory()); - setRenderer(new GLRootRenderer()); - getHolder().setFormat(PixelFormat.RGB_888); - - // Uncomment this to enable gl error check. - // setDebugFlags(DEBUG_CHECK_GL_ERROR); - } - - @Override - public void registerLaunchedAnimation(CanvasAnimation animation) { - // Register the newly launched animation so that we can set the start - // time more precisely. (Usually, it takes much longer for first - // rendering, so we set the animation start time as the time we - // complete rendering) - mAnimations.add(animation); - } - - @Override - public void addOnGLIdleListener(OnGLIdleListener listener) { - synchronized (mIdleListeners) { - mIdleListeners.addLast(listener); - if (mCanvas != null) { - // Wait for onSurfaceCreated - mIdleRunner.enable(); - } - } - } - - @Override - public void setContentPane(GLView content) { - if (mContentView == content) return; - if (mContentView != null) { - if (mInDownState) { - long now = SystemClock.uptimeMillis(); - MotionEvent cancelEvent = MotionEvent.obtain( - now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0); - mContentView.dispatchTouchEvent(cancelEvent); - cancelEvent.recycle(); - mInDownState = false; - } - mContentView.detachFromRoot(); - BasicTexture.yieldAllTextures(); - } - mContentView = content; - if (content != null) { - content.attachToRoot(this); - requestLayoutContentPane(); - } - } - - @Override - public void requestRenderForced() { - superRequestRender(); - } - - @Override - public void requestRender() { - if (DEBUG_INVALIDATE) { - StackTraceElement e = Thread.currentThread().getStackTrace()[4]; - String caller = e.getFileName() + ":" + e.getLineNumber() + " "; - Log.d(TAG, "invalidate: " + caller); - } - if (mRenderRequested) return; - mRenderRequested = true; - postOnAnimation(mRequestRenderOnAnimationFrame); - } - - private void superRequestRender() { - super.requestRender(); - } - - @Override - public void requestLayoutContentPane() { - mRenderLock.lock(); - try { - if (mContentView == null || (mFlags & FLAG_NEED_LAYOUT) != 0) return; - - // "View" system will invoke onLayout() for initialization(bug ?), we - // have to ignore it since the GLThread is not ready yet. - if ((mFlags & FLAG_INITIALIZED) == 0) return; - - mFlags |= FLAG_NEED_LAYOUT; - requestRender(); - } finally { - mRenderLock.unlock(); - } - } - - private void layoutContentPane() { - mFlags &= ~FLAG_NEED_LAYOUT; - - int w = getWidth(); - int h = getHeight(); - int displayRotation; - int compensation; - - // Get the new orientation values - if (mOrientationSource != null) { - displayRotation = mOrientationSource.getDisplayRotation(); - compensation = mOrientationSource.getCompensation(); - } else { - displayRotation = 0; - compensation = 0; - } - - if (mCompensation != compensation) { - mCompensation = compensation; - if (mCompensation % 180 != 0) { - mCompensationMatrix.setRotate(mCompensation); - // move center to origin before rotation - mCompensationMatrix.preTranslate(-w / 2, -h / 2); - // align with the new origin after rotation - mCompensationMatrix.postTranslate(h / 2, w / 2); - } else { - mCompensationMatrix.setRotate(mCompensation, w / 2, h / 2); - } - } - mDisplayRotation = displayRotation; - - // Do the actual layout. - if (mCompensation % 180 != 0) { - int tmp = w; - w = h; - h = tmp; - } - Log.i(TAG, "layout content pane " + w + "x" + h + " (compensation " + mCompensation + ")"); - if (mContentView != null && w != 0 && h != 0) { - mContentView.measure(GLView.MeasureSpec.makeMeasureSpec(w, GLView.MeasureSpec.EXACTLY), - GLView.MeasureSpec.makeMeasureSpec(h, GLView.MeasureSpec.EXACTLY)); - mContentView.layout(0, 0, w, h); - } - // Uncomment this to dump the view hierarchy. - //mContentView.dumpTree(""); - } - - @Override - protected void onLayout( - boolean changed, int left, int top, int right, int bottom) { - if (changed) requestLayoutContentPane(); - } - - private void outputFps() { - long now = System.nanoTime(); - if (mFrameCountingStart == 0) { - mFrameCountingStart = now; - } else if ((now - mFrameCountingStart) > 1000000000) { - Log.d(TAG, "fps: " + (double) mFrameCount - * 1000000000 / (now - mFrameCountingStart)); - mFrameCountingStart = now; - mFrameCount = 0; - } - ++mFrameCount; - } - - private void onDrawFrameLocked() { - if (DEBUG_FPS) outputFps(); - // release the unbound textures and deleted buffers. - mCanvas.deleteRecycledResources(); - - // reset texture upload limit - UploadedTexture.resetUploadLimit(); - - mRenderRequested = false; - - if ((mOrientationSource != null - && mDisplayRotation != mOrientationSource.getDisplayRotation()) - || (mFlags & FLAG_NEED_LAYOUT) != 0) { - layoutContentPane(); - } - - mCanvas.save(GLCanvas.SAVE_FLAG_ALL); - rotateCanvas(-mCompensation); - if (mContentView != null) { - mContentView.render(mCanvas); - } else { - // Make sure we always draw something to prevent displaying garbage - mCanvas.clearBuffer(); - } - mCanvas.restore(); - - if (!mAnimations.isEmpty()) { - long now = AnimationTime.get(); - for (int i = 0, n = mAnimations.size(); i < n; i++) { - mAnimations.get(i).setStartTime(now); - } - mAnimations.clear(); - } - - if (UploadedTexture.uploadLimitReached()) { - requestRender(); - } - - synchronized (mIdleListeners) { - if (!mIdleListeners.isEmpty()) mIdleRunner.enable(); - } - - if (DEBUG_INVALIDATE) { - mCanvas.fillRect(10, 10, 5, 5, mInvalidateColor); - mInvalidateColor = ~mInvalidateColor; - } - - if (DEBUG_DRAWING_STAT) { - mCanvas.dumpStatisticsAndClear(); - } - } - - private void rotateCanvas(int degrees) { - if (degrees == 0) return; - int w = getWidth(); - int h = getHeight(); - int cx = w / 2; - int cy = h / 2; - mCanvas.translate(cx, cy); - mCanvas.rotate(degrees, 0, 0, 1); - if (degrees % 180 != 0) { - mCanvas.translate(-cy, -cx); - } else { - mCanvas.translate(-cx, -cy); - } - } - - @Override - public boolean dispatchTouchEvent(MotionEvent event) { - if (!isEnabled()) return false; - - int action = event.getAction(); - if (action == MotionEvent.ACTION_CANCEL - || action == MotionEvent.ACTION_UP) { - mInDownState = false; - } else if (!mInDownState && action != MotionEvent.ACTION_DOWN) { - return false; - } - - if (mCompensation != 0) { - event = MotionEventHelper.transformEvent(event, mCompensationMatrix); - } - - mRenderLock.lock(); - try { - // If this has been detached from root, we don't need to handle event - boolean handled = mContentView != null - && mContentView.dispatchTouchEvent(event); - if (action == MotionEvent.ACTION_DOWN && handled) { - mInDownState = true; - } - return handled; - } finally { - mRenderLock.unlock(); - } - } - - @Override - public void lockRenderThread() { - mRenderLock.lock(); - } - - @Override - public void unlockRenderThread() { - mRenderLock.unlock(); - } - - @Override - public void onPause() { - unfreeze(); - if (mContentView != null) { - mContentView.pause(); - } - super.onPause(); - } - - @Override - public void onResume() { - if (mContentView != null) { - mContentView.resume(); - } - super.onResume(); - } - - @Override - public void setOrientationSource(OrientationSource source) { - mOrientationSource = source; - } - - @Override - public int getDisplayRotation() { - return mDisplayRotation; - } - - @Override - public int getCompensation() { - return mCompensation; - } - - @Override - public Matrix getCompensationMatrix() { - return mCompensationMatrix; - } - - @Override - public void freeze() { - mRenderLock.lock(); - mFreeze = true; - mRenderLock.unlock(); - } - - @Override - public void unfreeze() { - mRenderLock.lock(); - mFreeze = false; - mFreezeCondition.signalAll(); - mRenderLock.unlock(); - } - - @Override - public void setLightsOutMode(boolean enabled) { - if (!ApiHelper.HAS_SET_SYSTEM_UI_VISIBILITY) return; - - int flags = 0; - if (enabled) { - flags = STATUS_BAR_HIDDEN; - if (ApiHelper.HAS_VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE) { - flags |= (SYSTEM_UI_FLAG_FULLSCREEN | SYSTEM_UI_FLAG_LAYOUT_STABLE); - } - } - setSystemUiVisibility(flags); - } - - // We need to unfreeze in the following methods and in onPause(). - // These methods will wait on GLThread. If we have freezed the GLRootView, - // the GLThread will wait on main thread to call unfreeze and cause dead - // lock. - @Override - public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { - unfreeze(); - super.surfaceChanged(holder, format, w, h); - } - - @Override - public void surfaceCreated(SurfaceHolder holder) { - unfreeze(); - super.surfaceCreated(holder); - } - - @Override - public void surfaceDestroyed(SurfaceHolder holder) { - unfreeze(); - super.surfaceDestroyed(holder); - } - - @Override - protected void onDetachedFromWindow() { - unfreeze(); - super.onDetachedFromWindow(); - } - - /** - * {@inheritDoc} - */ - @Override - protected void dispatchSaveInstanceState(@NonNull SparseArray container) { - super.dispatchSaveInstanceState(container); - if (mContentView != null) { - mContentView.saveHierarchyState(container); - } - } - - /** - * {@inheritDoc} - */ - @Override - protected void dispatchRestoreInstanceState(@NonNull SparseArray container) { - super.dispatchRestoreInstanceState(container); - if (mContentView != null) { - mContentView.restoreHierarchyState(container); - } - } - - @Override - protected void finalize() throws Throwable { - try { - unfreeze(); - } finally { - super.finalize(); - } - } - - private class GLRootRenderer implements GLSurfaceView.Renderer { - - /** - * Called when the context is created, possibly after automatic destruction. - */ - @Override - public void onSurfaceCreated(GL10 gl1, EGLConfig config) { - GL11 gl = (GL11) gl1; - if (mGL != null) { - // The GL Object has changed - Log.i(TAG, "GLObject has changed from " + mGL + " to " + gl); - } - mRenderLock.lock(); - try { - mGL = gl; - mCanvas = mEGLContextClientVersion == 2 ? new GLES20Canvas() : new GLES11Canvas(gl); - BasicTexture.invalidateAllTextures(); - } finally { - mRenderLock.unlock(); - } - - if (DEBUG_FPS) { - setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); - } else { - setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); - } - } - - /** - * Called when the OpenGL surface is recreated without destroying the - * context. - */ - @Override - public void onSurfaceChanged(GL10 gl1, int width, int height) { - Log.i(TAG, "onSurfaceChanged: " + width + "x" + height + ", gl10: " + gl1.toString()); - Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY); - GalleryUtils.setRenderThread(); - GL11 gl = (GL11) gl1; - AssertUtils.assertTrue(mGL == gl); - - mCanvas.setSize(width, height); - } - - @Override - public void onDrawFrame(GL10 gl) { - AnimationTime.update(); - - long t0 = System.nanoTime(); - - mRenderLock.lock(); - - while (mFreeze) { - mFreezeCondition.awaitUninterruptibly(); - } - - try { - onDrawFrameLocked(); - } finally { - mRenderLock.unlock(); - } - - long t = System.nanoTime(); - long duration = (t - t0) / 1000000; - - if (duration > 8) { - Log.v(TAG, "--- " + duration + " ---"); - } - } - } - - private class IdleRunner implements Runnable { - // true if the idle runner is in the queue - private boolean mActive = false; - - @Override - public void run() { - OnGLIdleListener listener; - synchronized (mIdleListeners) { - mActive = false; - if (mIdleListeners.isEmpty()) return; - listener = mIdleListeners.removeFirst(); - } - mRenderLock.lock(); - boolean keepInQueue; - try { - keepInQueue = listener.onGLIdle(mCanvas, mRenderRequested); - } finally { - mRenderLock.unlock(); - } - synchronized (mIdleListeners) { - if (keepInQueue) mIdleListeners.addLast(listener); - if (!mRenderRequested && !mIdleListeners.isEmpty()) enable(); - } - } - - public void enable() { - // Who gets the flag can add it to the queue - if (mActive) return; - mActive = true; - queueEvent(this); - } - } - - // Always chose a config - private class ConfigChooser implements EGLConfigChooser { - - private final int[] mValue = new int[1]; - - @Override - public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { - int[] num_config = new int[1]; - int[] configSpec = new int[]{EGL10.EGL_NONE}; - if (!egl.eglChooseConfig(display, configSpec, null, 0, num_config)) { - throw new IllegalArgumentException("eglChooseConfig failed"); - } - - int numConfigs = num_config[0]; - - if (numConfigs <= 0) { - throw new IllegalArgumentException( - "No configs match configSpec"); - } - - EGLConfig[] configs = new EGLConfig[numConfigs]; - if (!egl.eglChooseConfig(display, configSpec, configs, numConfigs, num_config)) { - throw new IllegalArgumentException("eglChooseConfig#2 failed"); - } - EGLConfig config = chooseConfig(egl, display, configs); - if (config == null) { - throw new IllegalArgumentException("No config chosen"); - } - - int renderableType = findConfigAttrib(egl, display, config, EGL10.EGL_RENDERABLE_TYPE); - if ((renderableType & 0x0004) == 0x0004 /* EGL_OPENGL_ES2_BIT */) { - mEGLContextClientVersion = 2; - } else { - mEGLContextClientVersion = 1; - } - - return config; - } - - private EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs) { - // Use score to avoid "No config chosen" - int configIndex = 0; - int maxScore = 0; - - for (int i = 0, n = configs.length; i < n; i++) { - final EGLConfig config = configs[i]; - final int redSize = findConfigAttrib(egl, display, config, - EGL10.EGL_RED_SIZE); - final int greenSize = findConfigAttrib(egl, display, config, - EGL10.EGL_GREEN_SIZE); - final int blueSize = findConfigAttrib(egl, display, config, - EGL10.EGL_BLUE_SIZE); - final int alphaSize = findConfigAttrib(egl, display, config, - EGL10.EGL_ALPHA_SIZE); - final int sampleBuffers = findConfigAttrib(egl, display, config, - EGL10.EGL_SAMPLE_BUFFERS); - final int samples = findConfigAttrib(egl, display, config, - EGL10.EGL_SAMPLES); - final int depth = findConfigAttrib(egl, display, config, - EGL10.EGL_DEPTH_SIZE); - final int stencil = findConfigAttrib(egl, display, config, - EGL10.EGL_STENCIL_SIZE); - - final int score = redSize + greenSize + blueSize + alphaSize + - sampleBuffers + samples - depth - stencil; - - if (score > maxScore) { - maxScore = score; - configIndex = i; - } - } - - return configs[configIndex]; - } - - private int findConfigAttrib(EGL10 egl, EGLDisplay display, - EGLConfig config, int attribute) { - if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) { - return mValue[0]; - } - return 0; - } - } - - private class ContextFactory implements EGLContextFactory { - - private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; - - @Override - public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig config) { - int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, mEGLContextClientVersion, - EGL10.EGL_NONE}; - - return egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT, - mEGLContextClientVersion != 0 ? attrib_list : null); - } - - @Override - public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) { - if (!egl.eglDestroyContext(display, context)) { - Log.e("DefaultContextFactory", "display:" + display + " context: " + context); - throw new RuntimeException("eglDestroyContex failed: " + GLUtils.getEGLErrorString(egl.eglGetError())); - } - } - } -} diff --git a/app/src/main/java/com/hippo/glview/view/GLView.java b/app/src/main/java/com/hippo/glview/view/GLView.java deleted file mode 100644 index 856471579..000000000 --- a/app/src/main/java/com/hippo/glview/view/GLView.java +++ /dev/null @@ -1,1341 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.view; - -import android.graphics.Color; -import android.graphics.Rect; -import android.os.Parcelable; -import android.os.SystemClock; -import android.util.Log; -import android.util.SparseArray; -import android.view.MotionEvent; - -import androidx.annotation.IntDef; - -import com.hippo.glview.anim.CanvasAnimation; -import com.hippo.glview.glrenderer.GLCanvas; -import com.hippo.glview.glrenderer.GLPaint; -import com.hippo.yorozuya.AssertUtils; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; - -// GLView is a UI component. It can render to a GLCanvas and accept touch -// events. A GLView may have zero or more child GLView and they form a tree -// structure. The rendering and event handling will pass through the tree -// structure. -// -// A GLView tree should be attached to a GLRoot before event dispatching and -// rendering happens. GLView asks GLRoot to re-render or re-layout the -// GLView hierarchy using requestRender() and requestLayoutContentPane(). -// -// The render() method is called in a separate thread. Before calling -// dispatchTouchEvent() and layout(), GLRoot acquires a lock to avoid the -// rendering thread running at the same time. If there are other entry points -// from main thread (like a Handler) in your GLView, you need to call -// lockRendering() if the rendering thread should not run at the same time. -// -public class GLView implements TouchOwner { - public static final int VISIBLE = 0b00000000; - public static final int INVISIBLE = 0b00000001; - public static final int GONE = 0b00000010; - public static final int VISIBILITY_INVALID = 0b00000011; - /** - * Used to mark a GlView that has no ID. - */ - public static final int NO_ID = -1; - protected final static GLPaint mDrawBoundsPaint; - private static final String TAG = "GLView"; - private static final boolean DEBUG_DRAW_BOUNDS = false; - private static final int FLAG_INVISIBLE = 0b00000011; - private static final int FLAG_SET_MEASURED_SIZE = 0b00000100; - private static final int FLAG_LAYOUT_REQUESTED = 0b00001000; - - static { - mDrawBoundsPaint = new GLPaint(); - mDrawBoundsPaint.setColor(Color.RED); - mDrawBoundsPaint.setLineWidth(2); - } - - /** - * Position in parent, just like left, top, right, bottom in {@link android.view.View} - */ - protected final Rect mBounds = new Rect(); - protected final Rect mPaddings = new Rect(); - /** - * Position in root - */ - private final int[] mPositionInRoot = new int[2]; - private final Rect mTempRect = new Rect(); - protected GLView mParent; - protected int mMeasuredWidth = 0; - protected int mMeasuredHeight = 0; - protected int mScrollY = 0; - protected int mScrollX = 0; - protected int mScrollHeight = 0; - protected int mScrollWidth = 0; - private GLRoot mRoot; - private ArrayList mComponents; - private GLView mMotionTarget; - private CanvasAnimation mAnimation; - private int mViewFlags = 0; - private int mLastWidthSpec = -1; - private int mLastHeightSpec = -1; - /** - * The GlView's identifier. - * - * @see #setId(int) - * @see #getId() - */ - private int mID = NO_ID; - /** - * The minimum height of the view. We'll try our best to have the height - * of this view to at least this amount. - */ - private int mMinHeight; - /** - * The minimum width of the view. We'll try our best to have the width - * of this view to at least this amount. - */ - private int mMinWidth; - private LayoutParams mLayoutParams; - private int mBackgroundColor = Color.TRANSPARENT; - private TouchHelper mTouchHelper; - private float mHotspotX; - private float mHotspotY; - private boolean mPressed; - private OnClickListener mOnClickListener; - private OnLongClickListener mOnLongClickListener; - - /** - * Utility to return a default begin. Uses the supplied number as left or top. - * - * @param size the parent size - * @param specSize the child size - * @param paddingBeign the padding begin - * @param paddingFinish the padding finish - * @param position the {@link Gravity#POSITION_BEGIN} or {@link Gravity#POSITION_CENTER} or - * {@link Gravity#POSITION_FINISH} - * @return the begin size - */ - public static int getDefaultBegin(int size, int specSize, int paddingBeign, int paddingFinish, - @Gravity.PositionMode int position) { - if (position == Gravity.POSITION_FINISH) { - return size - paddingFinish - specSize; - } else if (position == Gravity.POSITION_CENTER) { - return ((size - paddingBeign - paddingFinish) / 2) - (specSize / 2) + paddingBeign; - } else { - return paddingBeign; - } - } - - /** - * Utility to return a default size. Uses the supplied size if the - * MeasureSpec imposed no constraints. Will get larger if allowed - * by the MeasureSpec. - *

- * It works differently from {@link android.view.View#getDefaultSize(int, int)}. - * For {@link MeasureSpec#AT_MOST}, size is suggestion size - * if it is small then spec size or it is not zero, - * otherwise spec size. - * - * @param size Default size for this view - * @param measureSpec Constraints imposed by the parent - * @return The size this view should be. - */ - public static int getDefaultSize(int size, int measureSpec) { - int result = size; - int specMode = MeasureSpec.getMode(measureSpec); - int specSize = MeasureSpec.getSize(measureSpec); - - switch (specMode) { - case MeasureSpec.UNSPECIFIED: - result = size; - break; - case MeasureSpec.EXACTLY: - result = specSize; - break; - case MeasureSpec.AT_MOST: - return size == 0 ? specSize : Math.min(size, specSize); - } - return result; - } - - /** - * Utility to return a max size. Uses the supplied size if the - * MeasureSpec imposed no constraints. Will get larger if allowed - * by the MeasureSpec. - *

- * It works the same as {@link android.view.View#getDefaultSize(int, int)}. - * - * @param size Default size for this view - * @param measureSpec Constraints imposed by the parent - * @return The size this view should be. - */ - public static int getMaxSize(int size, int measureSpec) { - int result = size; - int specMode = MeasureSpec.getMode(measureSpec); - int specSize = MeasureSpec.getSize(measureSpec); - - switch (specMode) { - case MeasureSpec.UNSPECIFIED: - result = size; - break; - case MeasureSpec.EXACTLY: - case MeasureSpec.AT_MOST: - result = specSize; - break; - } - return result; - } - - public static int getComponentSpec(int spec, int childSize) { - int specMode = MeasureSpec.getMode(spec); - int specSize = MeasureSpec.getSize(spec); - - int size = Math.max(0, specSize); - - int resultSize = 0; - int resultMode = 0; - - switch (specMode) { - // Parent has imposed an exact size on us - case MeasureSpec.EXACTLY: - if (childSize >= 0) { - resultSize = childSize; - resultMode = MeasureSpec.EXACTLY; - } else if (childSize == LayoutParams.MATCH_PARENT) { - // Child wants to be our size. So be it. - resultSize = size; - resultMode = MeasureSpec.EXACTLY; - } else if (childSize == LayoutParams.WRAP_CONTENT) { - // Child wants to determine its own size. It can't be - // bigger than us. - resultSize = size; - resultMode = MeasureSpec.AT_MOST; - } - break; - - // Parent has imposed a maximum size on us - case MeasureSpec.AT_MOST: - if (childSize >= 0) { - // Child wants a specific size... so be it - resultSize = childSize; - resultMode = MeasureSpec.EXACTLY; - } else if (childSize == LayoutParams.MATCH_PARENT) { - // Child wants to be our size, but our size is not fixed. - // Constrain child to not be bigger than us. - resultSize = size; - resultMode = MeasureSpec.AT_MOST; - } else if (childSize == LayoutParams.WRAP_CONTENT) { - // Child wants to determine its own size. It can't be - // bigger than us. - resultSize = size; - resultMode = MeasureSpec.AT_MOST; - } - break; - - // Parent asked to see how big we want to be - case MeasureSpec.UNSPECIFIED: - if (childSize >= 0) { - // Child wants a specific size... let him have it - resultSize = childSize; - resultMode = MeasureSpec.EXACTLY; - } else if (childSize == LayoutParams.MATCH_PARENT) { - // Child wants to be our size... find out how big it should - // be - resultSize = 0; - resultMode = MeasureSpec.UNSPECIFIED; - } else if (childSize == LayoutParams.WRAP_CONTENT) { - // Child wants to determine its own size.... find out how - // big it should be - resultSize = 0; - resultMode = MeasureSpec.UNSPECIFIED; - } - break; - } - return MeasureSpec.makeMeasureSpec(resultSize, resultMode); - } - - public static void measureComponent(GLView component, int widthSpec, int heightSpec) { - final LayoutParams lp = component.getLayoutParams(); - final int componentWidthSpec = getComponentSpec(widthSpec, lp.width); - final int componentHeightSpec = getComponentSpec(heightSpec, lp.height); - component.measure(componentWidthSpec, componentHeightSpec); - } - - public static void measureAllComponents(GLView parent, int widthSpec, int heightSpec) { - for (int i = 0, n = parent.getComponentCount(); i < n; i++) { - GLView component = parent.getComponent(i); - if (component.getVisibility() == GONE) { - continue; - } - measureComponent(component, widthSpec, heightSpec); - } - } - - public void startAnimation(CanvasAnimation animation, boolean atOnce) { - GLRoot root = getGLRoot(); - if (root == null) throw new IllegalStateException(); - mAnimation = animation; - if (mAnimation != null) { - mAnimation.start(); - if (atOnce) { - mAnimation.setStartTime(AnimationTime.get()); - } else { - root.registerLaunchedAnimation(mAnimation); - } - } - invalidate(); - } - - /** - * Returns the visibility status for this view. - * - * @return One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}. - */ - @Visibility - @SuppressWarnings("WrongConstant") - public int getVisibility() { - int visibility = mViewFlags & FLAG_INVISIBLE; - - if (visibility == VISIBILITY_INVALID) { - visibility = VISIBLE; - mViewFlags &= ~FLAG_INVISIBLE; - mViewFlags |= visibility; - } - - return visibility; - } - - /** - * Set the enabled state of this view. - * - * @param visibility One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}. - */ - public void setVisibility(@Visibility int visibility) { - @Visibility - int oldVisibility = getVisibility(); - if (visibility == oldVisibility) { - return; - } - - mViewFlags &= ~FLAG_INVISIBLE; - mViewFlags |= visibility; - - onVisibilityChanged(this, visibility); - - if (oldVisibility == GLView.GONE || visibility == GLView.GONE) { - requestLayout(); - } else { - invalidate(); - } - } - - // This should only be called on the content pane (the topmost GLView). - void attachToRoot(GLRoot root) { - AssertUtils.assertTrue(mParent == null && mRoot == null); - onAttachToRoot(root); - } - - // This should only be called on the content pane (the topmost GLView). - void detachFromRoot() { - AssertUtils.assertTrue(mParent == null && mRoot != null); - onDetachFromRoot(); - } - - // This should only be called on the content pane (the topmost GLView). - void pause() { - onPause(); - } - - // This should only be called on the content pane (the topmost GLView). - void resume() { - onResume(); - } - - public boolean isAttachedToRoot() { - return mRoot != null; - } - - // Returns the number of children of the GLView. - public int getComponentCount() { - return mComponents == null ? 0 : mComponents.size(); - } - - // Returns the children for the given index. - public GLView getComponent(int index) { - if (mComponents == null) { - throw new ArrayIndexOutOfBoundsException(index); - } - return mComponents.get(index); - } - - // Adds a child to this GLView. - public void addComponent(GLView component) { - addComponent(component, -1, null); - } - - public void addComponent(GLView component, int index) { - addComponent(component, index, null); - } - - public void addComponent(GLView component, LayoutParams params) { - addComponent(component, -1, params); - } - - public void addComponent(GLView component, int index, LayoutParams params) { - // Make component is not null - if (component == null) { - throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup"); - } - // Make sure the component doesn't have a parent currently. - if (component.mParent != null) { - throw new IllegalStateException( - "Component " + this + " being added, but it already has a parent"); - } - - // Ensure index is valid - if (index < 0) { - index = getComponentCount(); - } - - // Ensure params is valid - if (params == null) { - params = generateDefaultLayoutParams(); - } else if (!checkLayoutParams(params)) { - params = generateLayoutParams(params); - } - - // Build parent-child links - if (mComponents == null) { - mComponents = new ArrayList<>(); - } - - mComponents.add(index, component); - component.mParent = this; - component.setLayoutParams(params); - - // If this is added after we have a root, tell the component. - if (mRoot != null) { - component.onAttachToRoot(mRoot); - } - } - - // Removes a child from this GLView. - public boolean removeComponent(GLView component) { - if (mComponents == null) return false; - if (mComponents.remove(component)) { - removeOneComponent(component); - return true; - } - return false; - } - - public boolean removeComponentAt(int index) { - if (mComponents == null) { - return false; - } - if (index >= 0 && index < getComponentCount()) { - GLView component = mComponents.remove(index); - removeOneComponent(component); - return true; - } else { - return false; - } - } - - // Removes all children of this GLView. - public void removeAllComponents() { - for (int i = 0, n = mComponents.size(); i < n; ++i) { - removeOneComponent(mComponents.get(i)); - } - mComponents.clear(); - } - - private void removeOneComponent(GLView component) { - if (mMotionTarget == component) { - long now = SystemClock.uptimeMillis(); - MotionEvent cancelEvent = MotionEvent.obtain( - now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0); - dispatchTouchEvent(cancelEvent); - cancelEvent.recycle(); - } - component.onDetachFromRoot(); - component.mParent = null; - } - - /** - * Returns this GlView's identifier. - * - * @return a positive integer used to identify the view or {@link #NO_ID} - * if the view has no ID - */ - public int getId() { - return mID; - } - - /** - * Sets the identifier for this GlView. The identifier does not have to be - * unique in this GlView's hierarchy. The identifier should be a positive - * number. - * - * @param id a number used to identify the view - */ - public void setId(int id) { - mID = id; - } - - public Rect bounds() { - return mBounds; - } - - @Override - public void setHotspot(float x, float y) { - mHotspotX = x; - mHotspotY = y; - } - - public float getHotspotX() { - return mHotspotX; - } - - public float getHotspotY() { - return mHotspotY; - } - - @Override - public boolean isEnabled() { - return true; // TODO - } - - @Override - public boolean isPressed() { - return mPressed; - } - - @Override - public void setPressed(boolean pressed) { - mPressed = pressed; - } - - @Override - public boolean isClickable() { - return mOnClickListener != null; - } - - @Override - public boolean isLongClickable() { - return mOnLongClickListener != null; - } - - @Override - public boolean performClick() { - if (mOnClickListener != null) { - return mOnClickListener.onClick(this); - } else { - return false; - } - } - - @Override - public boolean performLongClick() { - if (mOnLongClickListener != null) { - return mOnLongClickListener.onLongClick(this); - } else { - return false; - } - } - - public void setOnClickListener(OnClickListener listener) { - mOnClickListener = listener; - } - - public void setOnLongClickListener(OnLongClickListener listener) { - mOnLongClickListener = listener; - } - - @Override - public int getWidth() { - return mBounds.right - mBounds.left; - } - - @Override - public int getHeight() { - return mBounds.bottom - mBounds.top; - } - - /** - * Offset this GLView's vertical location by the specified number of pixels. - * - * @param offset the number of pixels to offset the view by - */ - public void offsetTopAndBottom(int offset) { - if (offset != 0) { - mBounds.offset(0, offset); - dispatchNotifyPositionInRoot(); - invalidate(); - } - } - - /** - * Offset this GLView's horizontal location by the specified amount of pixels. - * - * @param offset the number of pixels to offset the view by - */ - public void offsetLeftAndRight(int offset) { - if (offset != 0) { - mBounds.offset(offset, 0); - dispatchNotifyPositionInRoot(); - invalidate(); - } - } - - public GLRoot getGLRoot() { - return mRoot; - } - - // Request re-rendering of the view hierarchy. - // This is used for animation or when the contents changed. - public void invalidate() { - GLRoot root = getGLRoot(); - if (root != null) root.requestRender(); - } - - // Request re-layout of the view hierarchy. - public void requestLayout() { - mViewFlags |= FLAG_LAYOUT_REQUESTED; - mLastWidthSpec = -1; - mLastHeightSpec = -1; - if (mParent != null) { - mParent.requestLayout(); - } else { - // Is this a content pane ? - GLRoot root = getGLRoot(); - if (root != null) root.requestLayoutContentPane(); - } - } - - public void render(GLCanvas canvas) { - // render background color - if (Color.alpha(mBackgroundColor) != 0) { - canvas.fillRect(0, 0, getWidth(), getHeight(), mBackgroundColor); - } - - // render content - onRender(canvas); - - // render child - canvas.save(); - for (int i = 0, n = getComponentCount(); i < n; ++i) { - renderChild(canvas, getComponent(i)); - } - canvas.restore(); - - // render bounds - if (DEBUG_DRAW_BOUNDS) { - canvas.drawRect(0, 0, getWidth(), getHeight(), mDrawBoundsPaint); - } - } - - public void onRender(GLCanvas canvas) { - } - - public void setBackgroundColor(int color) { - mBackgroundColor = color; - } - - protected void renderChild(GLCanvas canvas, GLView component) { - if (component.getVisibility() != GLView.VISIBLE) { - return; - } - - getValidRect(mTempRect); - if (mTempRect.isEmpty()) { - return; - } - - int xOffset = component.mBounds.left - mScrollX; - int yOffset = component.mBounds.top - mScrollY; - - canvas.translate(xOffset, yOffset); - - CanvasAnimation anim = component.mAnimation; - if (anim != null) { - canvas.save(anim.getCanvasSaveFlags()); - if (anim.calculate(AnimationTime.get())) { - invalidate(); - } else { - component.mAnimation = null; - } - anim.apply(canvas); - } - component.render(canvas); - if (anim != null) canvas.restore(); - canvas.translate(-xOffset, -yOffset); - } - - protected boolean onTouch(MotionEvent event) { - if (mTouchHelper == null) { - mTouchHelper = new TouchHelper(this); - } - return mTouchHelper.onTouch(event); - } - - protected boolean dispatchTouchEvent(MotionEvent event, - int x, int y, GLView component, boolean checkBounds) { - Rect rect = component.mBounds; - int left = rect.left; - int top = rect.top; - if (!checkBounds || rect.contains(x, y)) { - event.offsetLocation(-left, -top); - if (component.dispatchTouchEvent(event)) { - event.offsetLocation(left, top); - return true; - } - event.offsetLocation(left, top); - } - return false; - } - - protected boolean dispatchTouchEvent(MotionEvent event) { - int x = (int) event.getX(); - int y = (int) event.getY(); - int action = event.getAction(); - if (mMotionTarget != null) { - if (action == MotionEvent.ACTION_DOWN) { - MotionEvent cancel = MotionEvent.obtain(event); - cancel.setAction(MotionEvent.ACTION_CANCEL); - dispatchTouchEvent(cancel, x, y, mMotionTarget, false); - mMotionTarget = null; - } else { - dispatchTouchEvent(event, x, y, mMotionTarget, false); - if (action == MotionEvent.ACTION_CANCEL - || action == MotionEvent.ACTION_UP) { - mMotionTarget = null; - } - return true; - } - } - if (action == MotionEvent.ACTION_DOWN) { - // in the reverse rendering order - for (int i = getComponentCount() - 1; i >= 0; --i) { - GLView component = getComponent(i); - if (component.getVisibility() != GLView.VISIBLE) continue; - if (dispatchTouchEvent(event, x, y, component, true)) { - mMotionTarget = component; - return true; - } - } - } - return onTouch(event); - } - - /** - * Called by {@link GLRootView#dispatchSaveInstanceState(SparseArray)} to - * store this GlView hierarchy's frozen state into the given container. - * - * @param container The SparseArray in which to save the view's state. - */ - public void saveHierarchyState(SparseArray container) { - dispatchSaveInstanceState(container); - } - - protected void dispatchSaveInstanceState(SparseArray container) { - if (mID != NO_ID) { - Parcelable state = onSaveInstanceState(); - if (state != null) { - container.put(mID, state); - } - } - for (int i = 0, n = getComponentCount(); i < n; ++i) { - getComponent(i).dispatchSaveInstanceState(container); - } - } - - protected Parcelable onSaveInstanceState() { - return null; - } - - /** - * Called by {@link GLRootView#dispatchRestoreInstanceState(SparseArray)} to - * restore this view hierarchy's frozen state from the given container. - * - * @param container The SparseArray which holds previously frozen states. - */ - public void restoreHierarchyState(SparseArray container) { - dispatchRestoreInstanceState(container); - } - - protected void dispatchRestoreInstanceState(SparseArray container) { - if (mID != NO_ID) { - Parcelable state = container.get(mID); - if (state != null) { - onRestoreInstanceState(state); - } - } - for (int i = 0, n = getComponentCount(); i < n; ++i) { - getComponent(i).dispatchRestoreInstanceState(container); - } - } - - protected void onRestoreInstanceState(Parcelable state) { - if (state != null) { - throw new IllegalArgumentException("Please override onRestoreInstanceState"); - } - } - - public void setPaddings(int paddingLeft, int paddingTop, int paddingRight, int paddingBottom) { - mPaddings.set(paddingLeft, paddingTop, paddingRight, paddingBottom); - } - - public void setPaddingLeft(int paddingLeft) { - mPaddings.left = paddingLeft; - } - - public void setPaddingTop(int paddingTop) { - mPaddings.top = paddingTop; - } - - public void setPaddingRight(int paddingRight) { - mPaddings.right = paddingRight; - } - - public void setPaddingBottom(int paddingBottom) { - mPaddings.bottom = paddingBottom; - } - - public Rect getPaddings() { - return mPaddings; - } - - // There is a little different between GLView.layout() and View.layout(). - // onMeasure() is not called in GLView.layout(). - // For the content view of GLRootView, GLView.measure() is called before GLView.layout(). - // For component, GLView.measure() in called in parent's GLView.onMeasure(). - // So it is OK. - public void layout(int left, int top, int right, int bottom) { - boolean sizeChanged = setBounds(left, top, right, bottom); - final boolean forceLayout = (mViewFlags & FLAG_LAYOUT_REQUESTED) == FLAG_LAYOUT_REQUESTED; - if (sizeChanged || forceLayout) { - notifyPositionInRoot(); - onLayout(sizeChanged, left, top, right, bottom); - } else { - dispatchNotifyPositionInRoot(); - } - mViewFlags &= ~FLAG_LAYOUT_REQUESTED; - } - - public void dispatchNotifyPositionInRoot() { - if (notifyPositionInRoot()) { - for (int i = 0, size = getComponentCount(); i < size; i++) { - getComponent(i).dispatchNotifyPositionInRoot(); - } - } - } - - public boolean notifyPositionInRoot() { - // Update position in root - int[] position = mPositionInRoot; - int oldX = position[0]; - int oldY = position[1]; - if (mParent == null) { - position[0] = 0; - position[1] = 0; - } else { - mParent.getPositionInRoot(position); - // Apply offset in parent - position[0] += mBounds.left; - position[1] += mBounds.top; - } - int x = position[0]; - int y = position[1]; - if (x != oldX || y != oldY) { - onPositionInRootChanged(x, y, oldX, oldY); - return true; - } else { - return false; - } - } - - private boolean setBounds(int left, int top, int right, int bottom) { - Rect bounds = mBounds; - int oldW = bounds.right - bounds.left; - int oldH = bounds.bottom - bounds.top; - int newW = right - left; - int newH = bottom - top; - - boolean sizeChanged = oldW != newW || oldH != newH; - bounds.set(left, top, right, bottom); - - if (sizeChanged) { - onSizeChanged(newW, newH, oldW, oldH); - } - - return sizeChanged; - } - - protected void onSizeChanged(int newW, int newH, int oldW, int oldH) { - } - - protected void onPositionInRootChanged(int x, int y, int oldX, int oldY) { - } - - public void getPositionInRoot(int[] position) { - AssertUtils.assertEquals("position should be 2 length int array", position.length, 2); - position[0] = mPositionInRoot[0]; - position[1] = mPositionInRoot[1]; - } - - /** - * Get the rect of the view clipped by root rect - * - * @param rect the result - */ - public void getValidRect(Rect rect) { - GLRoot root = mRoot; - if (root == null) { - rect.setEmpty(); - return; - } - - int x = mPositionInRoot[0]; - int y = mPositionInRoot[1]; - rect.set(x, y, x + getWidth(), y + getHeight()); - if (!rect.intersect(0, 0, root.getWidth(), root.getHeight())) { - rect.setEmpty(); - return; - } - - rect.offset(-x, -y); - } - - public void measure(int widthSpec, int heightSpec) { - final boolean forceLayout = (mViewFlags & FLAG_LAYOUT_REQUESTED) == FLAG_LAYOUT_REQUESTED; - final boolean isExactly = MeasureSpec.getMode(widthSpec) == MeasureSpec.EXACTLY && - MeasureSpec.getMode(heightSpec) == MeasureSpec.EXACTLY; - final boolean matchingSize = isExactly && - getMeasuredWidth() == MeasureSpec.getSize(widthSpec) && - getMeasuredHeight() == MeasureSpec.getSize(heightSpec); - if (forceLayout || !matchingSize && - (widthSpec != mLastWidthSpec || - heightSpec != mLastHeightSpec)) { - mLastWidthSpec = widthSpec; - mLastHeightSpec = heightSpec; - - mViewFlags &= ~FLAG_SET_MEASURED_SIZE; - onMeasure(widthSpec, heightSpec); - if ((mViewFlags & FLAG_SET_MEASURED_SIZE) == 0) { - throw new IllegalStateException(getClass().getName() - + " should call setMeasuredSize() in onMeasure()"); - } - } - } - - protected void onMeasure(int widthSpec, int heightSpec) { - setMeasuredSize(getDefaultSize(getSuggestedMinimumWidth(), widthSpec), - getDefaultSize(getSuggestedMinimumHeight(), heightSpec)); - } - - protected int getSuggestedMinimumHeight() { - return getMinimumHeight() + mPaddings.top + mPaddings.bottom; - } - - protected int getSuggestedMinimumWidth() { - return getMinimumWidth() + mPaddings.left + mPaddings.right; - } - - /** - * Returns the minimum height of the view. - * - * @return the minimum height the view will try to be. - * @see #setMinimumHeight(int) - */ - public int getMinimumHeight() { - return mMinHeight; - } - - /** - * Sets the minimum height of the view. It is not guaranteed the view will - * be able to achieve this minimum height (for example, if its parent layout - * constrains it with less available height). - * - * @param minHeight The minimum height the view will try to be. - * @see #getMinimumHeight() - */ - public void setMinimumHeight(int minHeight) { - mMinHeight = minHeight; - requestLayout(); - } - - /** - * Returns the minimum width of the view. - * - * @return the minimum width the view will try to be. - * @see #setMinimumWidth(int) - */ - public int getMinimumWidth() { - return mMinWidth; - } - - /** - * Sets the minimum width of the view. It is not guaranteed the view will - * be able to achieve this minimum width (for example, if its parent layout - * constrains it with less available width). - * - * @param minWidth The minimum width the view will try to be. - * @see #getMinimumWidth() - */ - public void setMinimumWidth(int minWidth) { - mMinWidth = minWidth; - requestLayout(); - } - - protected void setMeasuredSize(int width, int height) { - mViewFlags |= FLAG_SET_MEASURED_SIZE; - mMeasuredWidth = width; - mMeasuredHeight = height; - } - - public int getMeasuredWidth() { - return mMeasuredWidth; - } - - public int getMeasuredHeight() { - return mMeasuredHeight; - } - - protected void onLayout( - boolean changeSize, int left, int top, int right, int bottom) { - } - - public LayoutParams getLayoutParams() { - return mLayoutParams; - } - - public void setLayoutParams(LayoutParams params) { - if (params == null) { - if (mParent == null) { - throw new NullPointerException("Layout parameters cannot be null"); - } else { - params = mParent.generateDefaultLayoutParams(); - } - } - mLayoutParams = params; - requestLayout(); - } - - protected boolean checkLayoutParams(LayoutParams p) { - return p != null; - } - - protected LayoutParams generateDefaultLayoutParams() { - return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); - } - - protected LayoutParams generateLayoutParams(LayoutParams p) { - return new LayoutParams(p); - } - - /** - * Gets the bounds of the given descendant that relative to this view. - */ - public boolean getBoundsOf(GLView descendant, Rect out) { - int xOffset = 0; - int yOffset = 0; - GLView view = descendant; - while (view != this) { - if (view == null) return false; - Rect bounds = view.mBounds; - xOffset += bounds.left; - yOffset += bounds.top; - view = view.mParent; - } - out.set(xOffset, yOffset, xOffset + descendant.getWidth(), - yOffset + descendant.getHeight()); - return true; - } - - protected void onVisibilityChanged(GLView changedView, @Visibility int visibility) { - for (int i = 0, n = getComponentCount(); i < n; ++i) { - getComponent(i).onVisibilityChanged(changedView, visibility); - } - } - - public void onAttachToRoot(GLRoot root) { - mRoot = root; - for (int i = 0, n = getComponentCount(); i < n; ++i) { - getComponent(i).onAttachToRoot(root); - } - } - - public void onDetachFromRoot() { - for (int i = 0, n = getComponentCount(); i < n; ++i) { - getComponent(i).onDetachFromRoot(); - } - mRoot = null; - } - - public void onPause() { - for (int i = 0, n = getComponentCount(); i < n; ++i) { - getComponent(i).onPause(); - } - } - - public void onResume() { - for (int i = 0, n = getComponentCount(); i < n; ++i) { - getComponent(i).onResume(); - } - } - - public void lockRendering() { - if (mRoot != null) { - mRoot.lockRenderThread(); - } - } - - public void unlockRendering() { - if (mRoot != null) { - mRoot.unlockRenderThread(); - } - } - - // This is for debugging only. - // Dump the view hierarchy into log. - void dumpTree(String prefix) { - Log.d(TAG, prefix + getClass().getSimpleName()); - for (int i = 0, n = getComponentCount(); i < n; ++i) { - getComponent(i).dumpTree(prefix + "...."); - } - } - - @IntDef({VISIBLE, INVISIBLE, GONE}) - @Retention(RetentionPolicy.SOURCE) - public @interface Visibility { - } - - public interface OnClickListener { - boolean onClick(GLView v); - } - - public interface OnLongClickListener { - boolean onLongClick(GLView v); - } - - /** - * A MeasureSpec encapsulates the layout requirements passed from parent to child. - * Each MeasureSpec represents a requirement for either the width or the height. - * A MeasureSpec is comprised of a size and a mode. There are three possible - * modes: - *

- *
UNSPECIFIED
- *
- * The parent has not imposed any constraint on the child. It can be whatever size - * it wants. - *
- * - *
EXACTLY
- *
- * The parent has determined an exact size for the child. The child is going to be - * given those bounds regardless of how big it wants to be. - *
- * - *
AT_MOST
- *
- * The child can be as large as it wants up to the specified size. - *
- *
- *

- * MeasureSpecs are implemented as ints to reduce object allocation. This class - * is provided to pack and unpack the <size, mode> tuple into the int. - */ - public static class MeasureSpec { - private static final int MODE_SHIFT = 30; - /** - * Measure specification mode: The parent has not imposed any constraint - * on the child. It can be whatever size it wants. - */ - public static final int UNSPECIFIED = 0 << MODE_SHIFT; - /** - * Measure specification mode: The parent has determined an exact size - * for the child. The child is going to be given those bounds regardless - * of how big it wants to be. - */ - public static final int EXACTLY = 1 << MODE_SHIFT; - /** - * Measure specification mode: The child can be as large as it wants up - * to the specified size. - */ - public static final int AT_MOST = 2 << MODE_SHIFT; - private static final int MODE_MASK = 0x3 << MODE_SHIFT; - /** - * The max size allowed - */ - public static final int MAX_SIZE = ~MODE_MASK; - - /** - * Creates a measure specification based on the supplied size and mode. - *

- * The mode must always be one of the following: - *

    - *
  • {@link #UNSPECIFIED}
  • - *
  • {@link #EXACTLY}
  • - *
  • {@link #AT_MOST}
  • - *
- * - *

Note: On API level 17 and lower, makeMeasureSpec's - * implementation was such that the order of arguments did not matter - * and overflow in either value could impact the resulting MeasureSpec. - * {@link android.widget.RelativeLayout} was affected by this bug. - * Apps targeting API levels greater than 17 will get the fixed, more strict - * behavior.

- * - * @param size the size of the measure specification - * @param mode the mode of the measure specification - * @return the measure specification based on size and mode - */ - public static int makeMeasureSpec(int size, int mode) { - return (size & ~MODE_MASK) | (mode & MODE_MASK); - } - - /** - * Extracts the mode from the supplied measure specification. - * - * @param measureSpec the measure specification to extract the mode from - * @return {@link #UNSPECIFIED}, - * {@link #AT_MOST} or - * {@link #EXACTLY} - */ - public static int getMode(int measureSpec) { - return (measureSpec & MODE_MASK); - } - - /** - * Extracts the size from the supplied measure specification. - * - * @param measureSpec the measure specification to extract the size from - * @return the size in pixels defined in the supplied measure specification - */ - public static int getSize(int measureSpec) { - return (measureSpec & ~MODE_MASK); - } - - /** - * Returns a String representation of the specified measure - * specification. - * - * @param measureSpec the measure specification to convert to a String - * @return a String with the following format: "MeasureSpec: MODE SIZE" - */ - public static String toString(int measureSpec) { - int mode = getMode(measureSpec); - int size = getSize(measureSpec); - - StringBuilder sb = new StringBuilder("MeasureSpec: "); - - if (mode == UNSPECIFIED) - sb.append("UNSPECIFIED "); - else if (mode == EXACTLY) - sb.append("EXACTLY "); - else if (mode == AT_MOST) - sb.append("AT_MOST "); - else - sb.append(mode).append(" "); - - sb.append(size); - return sb.toString(); - } - } - - /** - * The LayoutParams look like {@link android.view.ViewGroup.LayoutParams}, - * and work like it. - */ - public static class LayoutParams { - - /** - * Special value for the height or width requested by a View. - * MATCH_PARENT means that the view wants to be as big as its parent, - * minus the parent's padding, if any. Introduced in API Level 8. - */ - public static final int MATCH_PARENT = -1; - - /** - * Special value for the height or width requested by a View. - * WRAP_CONTENT means that the view wants to be just large enough to fit - * its own internal content, taking its own padding into account. - */ - public static final int WRAP_CONTENT = -2; - - /** - * Information about how wide the view wants to be. Can be one of the - * constants {@link #MATCH_PARENT} or {@link #WRAP_CONTENT} or an exact size. - */ - public int width; - - /** - * Information about how tall the view wants to be. Can be one of the - * constants {@link #MATCH_PARENT} or {@link #WRAP_CONTENT} or an exact size. - */ - public int height; - - /** - * Creates a new set of layout parameters with the specified width - * and height. - * - * @param width the width, either {@link #WRAP_CONTENT}, - * {@link #MATCH_PARENT}, or a fixed size in pixels - * @param height the height, either {@link #WRAP_CONTENT}, - * {@link #MATCH_PARENT}, or a fixed size in pixels - */ - public LayoutParams(int width, int height) { - this.width = width; - this.height = height; - } - - /** - * Copy constructor. Clones the width and height values of the source. - * - * @param source The layout params to copy from. - */ - public LayoutParams(LayoutParams source) { - this.width = source.width; - this.height = source.height; - } - } - - /** - * LayoutParams with gravity inside - */ - public static class GravityLayoutParams extends LayoutParams { - - public int gravity = Gravity.NO_GRAVITY; - - public GravityLayoutParams(GLView.LayoutParams source) { - super(source); - - if (source instanceof GravityLayoutParams) { - gravity = ((GravityLayoutParams) source).gravity; - } - } - - public GravityLayoutParams(int width, int height) { - super(width, height); - } - } -} diff --git a/app/src/main/java/com/hippo/glview/view/Gravity.java b/app/src/main/java/com/hippo/glview/view/Gravity.java deleted file mode 100644 index 2b070209f..000000000 --- a/app/src/main/java/com/hippo/glview/view/Gravity.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2022 Tarsin Norbin - * - * This file is part of EhViewer - * - * EhViewer is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * EhViewer is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with EhViewer. - * If not, see . - */ - -package com.hippo.glview.view; - -import androidx.annotation.IntDef; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -public class Gravity { - - public static final int NO_GRAVITY = 0x0000; - public static final int LEFT = 0x0001; - public static final int TOP = 0x0002; - public static final int RIGHT = 0x0004; - public static final int BOTTOM = 0x0008; - public static final int CENTER_HORIZONTAL = LEFT | RIGHT; - public static final int HORIZONTAL_MASK = CENTER_HORIZONTAL; - public static final int CENTER_VERTICAL = TOP | BOTTOM; - public static final int CENTER = CENTER_VERTICAL | CENTER_HORIZONTAL; - public static final int VERTICAL_MASK = CENTER_VERTICAL; - public static final int POSITION_BEGIN = 0; - public static final int POSITION_CENTER = 1; - public static final int POSITION_FINISH = 2; - public static final int HORIZONTAL = 0; - public static final int VERTICAL = 1; - - public static boolean centerHorizontal(int gravity) { - return (gravity & HORIZONTAL_MASK) == CENTER_HORIZONTAL; - } - - public static boolean right(int gravity) { - return (gravity & HORIZONTAL_MASK) == RIGHT; - } - - public static boolean left(int gravity) { - return (gravity & HORIZONTAL_MASK) == LEFT; - } - - public static boolean centerVertical(int gravity) { - return (gravity & VERTICAL_MASK) == CENTER_VERTICAL; - } - - public static boolean top(int gravity) { - return (gravity & VERTICAL_MASK) == TOP; - } - - public static boolean bottom(int gravity) { - return (gravity & VERTICAL_MASK) == BOTTOM; - } - - public static @PositionMode - int getPosition(int gravity, @DirectionMode int direction) { - if (direction == HORIZONTAL) { - if (right(gravity)) { - return POSITION_FINISH; - } else if (centerHorizontal(gravity)) { - return POSITION_CENTER; - } else { - return POSITION_BEGIN; - } - } else { - if (bottom(gravity)) { - return POSITION_FINISH; - } else if (centerVertical(gravity)) { - return POSITION_CENTER; - } else { - return POSITION_BEGIN; - } - } - } - - @IntDef({POSITION_BEGIN, POSITION_CENTER, POSITION_FINISH}) - @Retention(RetentionPolicy.SOURCE) - public @interface PositionMode { - } - - @IntDef({HORIZONTAL, VERTICAL}) - @Retention(RetentionPolicy.SOURCE) - public @interface DirectionMode { - } -} diff --git a/app/src/main/java/com/hippo/glview/view/OrientationSource.java b/app/src/main/java/com/hippo/glview/view/OrientationSource.java deleted file mode 100644 index 6374e3120..000000000 --- a/app/src/main/java/com/hippo/glview/view/OrientationSource.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.view; - -public interface OrientationSource { - int getDisplayRotation(); - - int getCompensation(); -} diff --git a/app/src/main/java/com/hippo/glview/view/TouchHelper.java b/app/src/main/java/com/hippo/glview/view/TouchHelper.java deleted file mode 100644 index f60997951..000000000 --- a/app/src/main/java/com/hippo/glview/view/TouchHelper.java +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Copyright 2015 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.view; - -import android.content.Context; -import android.view.MotionEvent; -import android.view.ViewConfiguration; - -import androidx.annotation.NonNull; - -import com.hippo.yorozuya.LayoutUtils; -import com.hippo.yorozuya.SimpleHandler; - -public class TouchHelper { - - /** - * Cache the touch slop from the context that created the view. - */ - private static int sTouchSlop = 8; - private final TouchOwner mOwner; - private boolean mPrePressed = false; - private boolean mHasPerformedLongPress = false; - private CheckForLongPress mPendingCheckForLongPress = null; - private CheckForTap mPendingCheckForTap = null; - private PerformClick mPerformClick = null; - private UnsetPressedState mUnsetPressedState = null; - - public TouchHelper(@NonNull TouchOwner owner) { - mOwner = owner; - } - - public static void initialize(Context context) { - sTouchSlop = LayoutUtils.dp2pix(context, 8); // 8dp - } - - public boolean onTouch(MotionEvent event) { - TouchOwner owner = mOwner; - final float x = event.getX(); - final float y = event.getY(); - - if (event.getAction() == MotionEvent.ACTION_DOWN || - event.getAction() == MotionEvent.ACTION_MOVE) - owner.setHotspot(x, y); - - if (!owner.isEnabled()) { - if (event.getAction() == MotionEvent.ACTION_UP && owner.isPressed()) { - owner.setPressed(false); - } - // A disabled view that is clickable still consumes the touch - // events, it just doesn't respond to them. - return (owner.isClickable() || owner.isLongClickable()); - } - - if (owner.isClickable() || owner.isLongClickable()) { - switch (event.getAction()) { - case MotionEvent.ACTION_UP: - if (owner.isPressed() || mPrePressed) { - if (mPrePressed) { - // The button is being released before we actually - // showed it as pressed. Make it show the pressed - // state now (before scheduling the click) to ensure - // the user sees it. - setPressed(true, x, y); - } - - if (!mHasPerformedLongPress) { - // This is a tap, so remove the longpress check - removeLongPressCallback(); - - // Use a Runnable and post this rather than calling - // performClick directly. This lets other visual state - // of the view update before click actions start. - if (mPerformClick == null) { - mPerformClick = new PerformClick(); - } - if (!SimpleHandler.getInstance().post(mPerformClick)) { - owner.performClick(); - } - } - - if (mUnsetPressedState == null) { - mUnsetPressedState = new UnsetPressedState(); - } - - if (mPrePressed) { - SimpleHandler.getInstance().postDelayed(mUnsetPressedState, - ViewConfiguration.getPressedStateDuration()); - } else if (!SimpleHandler.getInstance().post(mUnsetPressedState)) { - // If the post failed, unpress right now - mUnsetPressedState.run(); - } - - removeTapCallback(); - } - break; - - case MotionEvent.ACTION_DOWN: - mHasPerformedLongPress = false; - - mPrePressed = true; - if (mPendingCheckForTap == null) { - mPendingCheckForTap = new CheckForTap(); - } - mPendingCheckForTap.x = event.getX(); - mPendingCheckForTap.y = event.getY(); - SimpleHandler.getInstance().postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); - break; - - case MotionEvent.ACTION_CANCEL: - owner.setPressed(false); - removeTapCallback(); - removeLongPressCallback(); - break; - - case MotionEvent.ACTION_MOVE: - owner.setHotspot(x, y); - - // Be lenient about moving outside of buttons - if (!pointInView(x, y, sTouchSlop)) { - // Outside button - removeTapCallback(); - if (owner.isPressed()) { - // Remove any future long press/tap checks - removeLongPressCallback(); - - owner.setPressed(false); - } - } - break; - } - - return true; - } - - return false; - } - - private void setPressed(boolean pressed, float x, float y) { - mOwner.setPressed(pressed); - if (pressed) { - mOwner.setHotspot(x, y); - } - } - - /** - * Utility method to determine whether the given point, in local coordinates, - * is inside the view, where the area of the view is expanded by the slop factor. - * This method is called while processing touch-move events to determine if the event - * is still within the view. - */ - public boolean pointInView(float localX, float localY, float slop) { - return localX >= -slop && localY >= -slop && localX < (mOwner.getWidth() + slop) && - localY < (mOwner.getHeight() + slop); - } - - /** - * Remove the longpress detection timer. - */ - private void removeLongPressCallback() { - if (mPendingCheckForLongPress != null) { - SimpleHandler.getInstance().removeCallbacks(mPendingCheckForLongPress); - } - } - - /** - * Remove the tap detection timer. - */ - private void removeTapCallback() { - if (mPendingCheckForTap != null) { - mPrePressed = false; - SimpleHandler.getInstance().removeCallbacks(mPendingCheckForTap); - } - } - - private void checkForLongClick(int delayOffset) { - if (mOwner.isLongClickable()) { - mHasPerformedLongPress = false; - - if (mPendingCheckForLongPress == null) { - mPendingCheckForLongPress = new CheckForLongPress(); - } - SimpleHandler.getInstance().postDelayed(mPendingCheckForLongPress, - ViewConfiguration.getLongPressTimeout() - delayOffset); - } - } - - private final class CheckForLongPress implements Runnable { - - @Override - public void run() { - if (mOwner.isPressed()) { - if (mOwner.performLongClick()) { - mHasPerformedLongPress = true; - } - } - } - } - - private final class CheckForTap implements Runnable { - public float x; - public float y; - - @Override - public void run() { - mPrePressed = false; - setPressed(true, x, y); - checkForLongClick(ViewConfiguration.getTapTimeout()); - } - } - - private final class PerformClick implements Runnable { - - @Override - public void run() { - mOwner.performClick(); - } - } - - private final class UnsetPressedState implements Runnable { - - @Override - public void run() { - mOwner.setPressed(false); - } - } -} diff --git a/app/src/main/java/com/hippo/glview/view/TouchOwner.java b/app/src/main/java/com/hippo/glview/view/TouchOwner.java deleted file mode 100644 index b9b35a48b..000000000 --- a/app/src/main/java/com/hippo/glview/view/TouchOwner.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2015 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.view; - -public interface TouchOwner { - - void setHotspot(float x, float y); - - boolean isEnabled(); - - boolean isPressed(); - - void setPressed(boolean pressed); - - boolean isClickable(); - - boolean isLongClickable(); - - boolean performClick(); - - boolean performLongClick(); - - int getWidth(); - - int getHeight(); -} diff --git a/app/src/main/java/com/hippo/glview/widget/GLFrameLayout.java b/app/src/main/java/com/hippo/glview/widget/GLFrameLayout.java deleted file mode 100644 index 23e0b9ae7..000000000 --- a/app/src/main/java/com/hippo/glview/widget/GLFrameLayout.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.widget; - -import com.hippo.glview.view.GLView; -import com.hippo.glview.view.Gravity; - -public class GLFrameLayout extends GLView { - - @Override - protected void onMeasure(int widthSpec, int heightSpec) { - int maxWidth = 0; - int maxHeight = 0; - for (int i = 0, n = getComponentCount(); i < n; i++) { - GLView component = getComponent(i); - if (component.getVisibility() == GONE) { - continue; - } - measureComponent(component, widthSpec, heightSpec); - maxWidth = Math.max(maxWidth, component.getMeasuredWidth()); - maxHeight = Math.max(maxHeight, component.getMeasuredHeight()); - } - - // Consider min - maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); - maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); - - // The final - maxWidth = getDefaultSize(maxWidth, widthSpec); - maxHeight = getDefaultSize(maxHeight, heightSpec); - - setMeasuredSize(maxWidth, maxHeight); - - if (MeasureSpec.getSize(widthSpec) != MeasureSpec.EXACTLY || - MeasureSpec.getSize(heightSpec) != MeasureSpec.EXACTLY) { - // Measure again - measureAllComponents(this, MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY)); - } - } - - @Override - protected void onLayout(boolean changeSize, int left, int top, int right, int bottom) { - int width = getWidth(); - int height = getHeight(); - - for (int i = 0, n = getComponentCount(); i < n; i++) { - GLView component = getComponent(i); - if (component.getVisibility() == GONE) { - continue; - } - int measureWidth = component.getMeasuredWidth(); - int measureHeight = component.getMeasuredHeight(); - int componentLeft; - int componentTop; - - GravityLayoutParams lp = (GravityLayoutParams) component.getLayoutParams(); - int gravity = lp.gravity; - if (Gravity.centerHorizontal(gravity)) { - componentLeft = (width / 2) - (measureWidth / 2); - } else if (Gravity.right(gravity)) { - componentLeft = width - measureWidth; - } else { - componentLeft = 0; - } - if (Gravity.centerVertical(gravity)) { - componentTop = (height / 2) - (measureHeight / 2); - } else if (Gravity.bottom(gravity)) { - componentTop = height - measureHeight; - } else { - componentTop = 0; - } - - component.layout(componentLeft, componentTop, - componentLeft + measureWidth, componentTop + measureHeight); - } - } - - @Override - protected boolean checkLayoutParams(GLView.LayoutParams p) { - return p instanceof GravityLayoutParams; - } - - @Override - protected LayoutParams generateDefaultLayoutParams() { - return new GravityLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); - } - - @Override - protected LayoutParams generateLayoutParams(GLView.LayoutParams p) { - return p == null ? generateDefaultLayoutParams() : new GravityLayoutParams(p); - } -} diff --git a/app/src/main/java/com/hippo/glview/widget/GLLinearLayout.java b/app/src/main/java/com/hippo/glview/widget/GLLinearLayout.java deleted file mode 100644 index 1cd4ff64e..000000000 --- a/app/src/main/java/com/hippo/glview/widget/GLLinearLayout.java +++ /dev/null @@ -1,243 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.widget; - -import android.graphics.Rect; - -import androidx.annotation.IntDef; - -import com.hippo.glview.view.GLView; -import com.hippo.glview.view.Gravity; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; -import java.util.List; - -public class GLLinearLayout extends GLView { - - public static final int HORIZONTAL = 0; - public static final int VERTICAL = 1; - private final List mTempList = new ArrayList<>(); - private int mInterval = 0; - private int mOrientation = VERTICAL; - - public void setInterval(int interval) { - if (mInterval != interval) { - mInterval = interval; - requestLayout(); - } - } - - /** - * Should the layout be a column or a row. - * - * @param orientation Pass {@link #HORIZONTAL} or {@link #VERTICAL}. Default - * value is {@link #HORIZONTAL}. - */ - public void setOrientation(@OrientationMode int orientation) { - if (mOrientation != orientation) { - mOrientation = orientation; - requestLayout(); - } - } - - @Override - protected void onMeasure(int widthSpec, int heightSpec) { - int layoutWidthSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthSpec) - mPaddings.left - mPaddings.right, - MeasureSpec.getMode(widthSpec)); - int layoutHeightSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightSpec) - mPaddings.top - mPaddings.bottom, - MeasureSpec.getMode(heightSpec)); - - if (mOrientation == HORIZONTAL && MeasureSpec.getMode(widthSpec) == MeasureSpec.EXACTLY) { - int width = MeasureSpec.getSize(layoutWidthSpec); - float sumWeight = 0.0f; - - for (int i = 0, n = getComponentCount(); i < n; i++) { - final GLView component = getComponent(i); - if (component.getVisibility() == GONE) { - continue; - } - final LayoutParams lp = (LayoutParams) component.getLayoutParams(); - - if (lp.weight > 0.0f) { - sumWeight += lp.weight; - mTempList.add(component); - } else { - measureComponent(component, layoutWidthSpec, layoutHeightSpec); - width -= component.getMeasuredWidth(); - } - } - - for (GLView component : mTempList) { - final LayoutParams lp = (LayoutParams) component.getLayoutParams(); - final int componentWidthSpec = MeasureSpec.makeMeasureSpec( - (int) (width * lp.weight / sumWeight), MeasureSpec.EXACTLY); - final int componentHeightSpec = getComponentSpec(layoutHeightSpec, lp.height); - component.measure(componentWidthSpec, componentHeightSpec); - } - - mTempList.clear(); - } else if (mOrientation == VERTICAL && MeasureSpec.getMode(heightSpec) == MeasureSpec.EXACTLY) { - int height = MeasureSpec.getSize(layoutHeightSpec); - float sumWeight = 0.0f; - - for (int i = 0, n = getComponentCount(); i < n; i++) { - final GLView component = getComponent(i); - if (component.getVisibility() == GONE) { - continue; - } - final LayoutParams lp = (LayoutParams) component.getLayoutParams(); - - if (lp.weight > 0.0f) { - sumWeight += lp.weight; - mTempList.add(component); - } else { - measureComponent(component, layoutWidthSpec, layoutHeightSpec); - height -= component.getMeasuredHeight(); - } - } - - for (GLView component : mTempList) { - final LayoutParams lp = (LayoutParams) component.getLayoutParams(); - final int componentWidthSpec = getComponentSpec(layoutWidthSpec, lp.width); - final int componentHeightSpec = MeasureSpec.makeMeasureSpec( - (int) (height * lp.weight / sumWeight), MeasureSpec.EXACTLY); - component.measure(componentWidthSpec, componentHeightSpec); - } - - mTempList.clear(); - } else { - measureAllComponents(this, layoutWidthSpec, layoutHeightSpec); - } - - int maxWidth = 0; - int maxHeight = 0; - for (int i = 0, n = getComponentCount(); i < n; i++) { - GLView component = getComponent(i); - if (component.getVisibility() == GONE) { - continue; - } - - if (mOrientation == VERTICAL) { - maxWidth = Math.max(maxWidth, component.getMeasuredWidth()); - if (i != 0) { - maxHeight += mInterval; - } - maxHeight += component.getMeasuredHeight(); - } else if (mOrientation == HORIZONTAL) { - maxHeight = Math.max(maxHeight, component.getMeasuredHeight()); - if (i != 0) { - maxWidth += mInterval; - } - maxWidth += component.getMeasuredWidth(); - } - } - - Rect paddings = getPaddings(); - maxWidth = maxWidth + paddings.left + paddings.right; - maxHeight = maxHeight + paddings.top + paddings.bottom; - - setMeasuredSize(getDefaultSize(maxWidth, widthSpec), getDefaultSize(maxHeight, heightSpec)); - } - - @Override - protected void onLayout(boolean changeSize, int left, int top, int right, int bottom) { - int width = getWidth(); - int height = getHeight(); - Rect paddings = getPaddings(); - int componentLeft; - int componentTop; - - if (mOrientation == VERTICAL) { - componentTop = paddings.top; - - for (int i = 0, n = getComponentCount(); i < n; i++) { - GLView component = getComponent(i); - if (component.getVisibility() == GONE) { - continue; - } - int measureWidth = component.getMeasuredWidth(); - int measureHeight = component.getMeasuredHeight(); - - LayoutParams lp = (LayoutParams) component.getLayoutParams(); - componentLeft = getDefaultBegin(width, measureWidth, paddings.left, paddings.right, - Gravity.getPosition(lp.gravity, Gravity.HORIZONTAL)); - component.layout(componentLeft, componentTop, - componentLeft + measureWidth, componentTop + measureHeight); - - componentTop += measureHeight + mInterval; - } - } else if (mOrientation == HORIZONTAL) { - componentLeft = paddings.left; - - for (int i = 0, n = getComponentCount(); i < n; i++) { - GLView component = getComponent(i); - if (component.getVisibility() == GONE) { - continue; - } - int measureWidth = component.getMeasuredWidth(); - int measureHeight = component.getMeasuredHeight(); - - LayoutParams lp = (LayoutParams) component.getLayoutParams(); - componentTop = getDefaultBegin(height, measureHeight, paddings.top, paddings.bottom, - Gravity.getPosition(lp.gravity, Gravity.VERTICAL)); - component.layout(componentLeft, componentTop, - componentLeft + measureWidth, componentTop + measureHeight); - - componentLeft += measureWidth + mInterval; - } - } - } - - @Override - protected boolean checkLayoutParams(GLView.LayoutParams p) { - return p instanceof LayoutParams; - } - - @Override - protected GLView.LayoutParams generateDefaultLayoutParams() { - return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); - } - - @Override - protected GLView.LayoutParams generateLayoutParams(GLView.LayoutParams p) { - return p == null ? generateDefaultLayoutParams() : new LayoutParams(p); - } - - @IntDef({HORIZONTAL, VERTICAL}) - @Retention(RetentionPolicy.SOURCE) - public @interface OrientationMode { - } - - public static class LayoutParams extends GravityLayoutParams { - - public float weight = 0.0f; - - public LayoutParams(GLView.LayoutParams source) { - super(source); - - if (source instanceof LayoutParams) { - weight = ((LayoutParams) source).weight; - } - } - - public LayoutParams(int width, int height) { - super(width, height); - } - } -} diff --git a/app/src/main/java/com/hippo/glview/widget/GLMovableTextView.java b/app/src/main/java/com/hippo/glview/widget/GLMovableTextView.java deleted file mode 100644 index 9a8a99c48..000000000 --- a/app/src/main/java/com/hippo/glview/widget/GLMovableTextView.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.widget; - -import android.graphics.Rect; -import android.text.TextUtils; - -import com.hippo.glview.glrenderer.GLCanvas; -import com.hippo.glview.glrenderer.MovableTextTexture; -import com.hippo.glview.view.GLView; -import com.hippo.glview.view.Gravity; -import com.hippo.yorozuya.collect.CollectionUtils; - -public class GLMovableTextView extends GLView { - - MovableTextTexture mTextTexture; - - private String mText = ""; - private int[] mIndexes = CollectionUtils.EMPTY_INT_ARRAY; - - private int mGravity = Gravity.NO_GRAVITY; - - private void generateIndexes() { - if (mTextTexture == null || TextUtils.isEmpty(mText)) { - mIndexes = CollectionUtils.EMPTY_INT_ARRAY; - } else { - mIndexes = mTextTexture.getTextIndexes(mText); - } - } - - public void setTextTexture(MovableTextTexture textTexture) { - if (mTextTexture == textTexture) { - return; - } - mTextTexture = textTexture; - - generateIndexes(); - requestLayout(); - } - - public void setText(String text) { - if (text == null) { - text = ""; - } - if (text.equals(mText)) { - return; - } - mText = text; - - generateIndexes(); - requestLayout(); - } - - public void setGravity(int gravity) { - if (mGravity != gravity) { - mGravity = gravity; - invalidate(); - } - } - - @Override - protected int getSuggestedMinimumWidth() { - if (mTextTexture == null) { - return super.getSuggestedMinimumWidth(); - } else { - return Math.max((int) mTextTexture.getTextWidth(mIndexes) + mPaddings.left + mPaddings.right, - super.getSuggestedMinimumWidth()); - } - } - - @Override - protected int getSuggestedMinimumHeight() { - if (mTextTexture == null) { - return super.getSuggestedMinimumHeight(); - } else { - return Math.max((int) mTextTexture.getTextHeight() + mPaddings.top + mPaddings.bottom, - super.getSuggestedMinimumHeight()); - } - } - - @Override - public void onRender(GLCanvas canvas) { - if (mTextTexture == null || mIndexes == null) { - return; - } - - Rect paddings = getPaddings(); - int x = getDefaultBegin(getWidth(), (int) mTextTexture.getTextWidth(mIndexes), - paddings.left, paddings.right, Gravity.getPosition(mGravity, Gravity.HORIZONTAL)); - int y = getDefaultBegin(getHeight(), (int) mTextTexture.getTextHeight(), - paddings.top, paddings.bottom, Gravity.getPosition(mGravity, Gravity.VERTICAL)); - mTextTexture.drawText(canvas, mIndexes, x, y); - } -} diff --git a/app/src/main/java/com/hippo/glview/widget/GLProgressView.java b/app/src/main/java/com/hippo/glview/widget/GLProgressView.java deleted file mode 100644 index bffb8b639..000000000 --- a/app/src/main/java/com/hippo/glview/widget/GLProgressView.java +++ /dev/null @@ -1,239 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.widget; - -import android.graphics.Color; -import android.graphics.Path; -import android.view.animation.Interpolator; -import android.view.animation.LinearInterpolator; - -import androidx.core.view.animation.PathInterpolatorCompat; - -import com.hippo.glview.anim.Animation; -import com.hippo.glview.anim.FloatAnimation; -import com.hippo.glview.glrenderer.GLCanvas; -import com.hippo.glview.glrenderer.GLPaint; -import com.hippo.glview.view.AnimationTime; -import com.hippo.glview.view.GLView; - -import java.util.ArrayList; -import java.util.List; - -public class GLProgressView extends GLView { - - private static final Interpolator TRIM_START_INTERPOLATOR; - private static final Interpolator TRIM_END_INTERPOLATOR; - private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); - - static { - Path trimStartPath = new Path(); - trimStartPath.moveTo(0.0f, 0.0f); - trimStartPath.lineTo(0.5f, 0.0f); - trimStartPath.cubicTo(0.7f, 0.0f, 0.6f, 1f, 1f, 1f); - TRIM_START_INTERPOLATOR = PathInterpolatorCompat.create(trimStartPath); - - Path trimEndPath = new Path(); - trimEndPath.moveTo(0.0f, 0.0f); - trimEndPath.cubicTo(0.2f, 0.0f, 0.1f, 1f, 0.5f, 1f); - trimEndPath.lineTo(1f, 1f); - TRIM_END_INTERPOLATOR = PathInterpolatorCompat.create(trimEndPath); - } - - private final GLPaint mGLPaint; - private final List mAnimations; - private float mCx; - private float mCy; - private float mRadiusX; - private float mRadiusY; - private float mTrimStart = 0.0f; - private float mTrimEnd = 0.0f; - private float mTrimOffset = 0.0f; - private float mTrimRotation = 0.0f; - private boolean mIndeterminate = false; - - public GLProgressView() { - mGLPaint = new GLPaint(); - mGLPaint.setColor(Color.WHITE); - mGLPaint.setBackgroundColor(Color.BLACK); - mAnimations = new ArrayList<>(); - - setupAnimations(); - } - - public void setupAnimations() { - FloatAnimation trimStart = new FloatAnimation() { - @Override - protected void onCalculate(float progress) { - super.onCalculate(progress); - mTrimStart = get(); - } - }; - trimStart.setRange(0.0f, 0.75f); - trimStart.setDuration(1333L); - trimStart.setInterpolator(TRIM_START_INTERPOLATOR); - trimStart.setRepeatCount(Animation.INFINITE); - - FloatAnimation trimEnd = new FloatAnimation() { - @Override - protected void onCalculate(float progress) { - super.onCalculate(progress); - mTrimEnd = get(); - } - }; - trimEnd.setRange(0.0f, 0.75f); - trimEnd.setDuration(1333L); - trimEnd.setInterpolator(TRIM_END_INTERPOLATOR); - trimEnd.setRepeatCount(Animation.INFINITE); - - FloatAnimation trimOffset = new FloatAnimation() { - @Override - protected void onCalculate(float progress) { - super.onCalculate(progress); - mTrimOffset = get(); - } - }; - trimOffset.setRange(0.0f, 0.25f); - trimOffset.setDuration(1333L); - trimOffset.setInterpolator(LINEAR_INTERPOLATOR); - trimOffset.setRepeatCount(Animation.INFINITE); - - FloatAnimation trimRotation = new FloatAnimation() { - @Override - protected void onCalculate(float progress) { - super.onCalculate(progress); - mTrimRotation = get(); - } - }; - trimRotation.setRange(0.0f, 720.0f); - trimRotation.setDuration(6665L); - trimRotation.setInterpolator(LINEAR_INTERPOLATOR); - trimRotation.setRepeatCount(Animation.INFINITE); - - mAnimations.add(trimStart); - mAnimations.add(trimEnd); - mAnimations.add(trimOffset); - mAnimations.add(trimRotation); - } - - private void startAnimations() { - List animations = mAnimations; - for (int i = 0, n = animations.size(); i < n; i++) { - animations.get(i).reset(); - } - } - - private void stopAnimations() { - List animations = mAnimations; - for (int i = 0, n = animations.size(); i < n; i++) { - animations.get(i).cancel(); - } - } - - @Override - protected void onLayout(boolean changeSize, int left, int top, int right, - int bottom) { - super.onLayout(changeSize, left, top, right, bottom); - - int width = right - left; - int height = bottom - top; - mGLPaint.setLineWidth(Math.min(width, height) / 12.0f); - mCx = width / 2; - mCy = height / 2; - mRadiusX = width / 48.0f * 19.0f; - mRadiusY = height / 48.0f * 19.0f; - } - - public void setColor(int color) { - mGLPaint.setColor(color); - invalidate(); - } - - public void setBgColor(int color) { - mGLPaint.setBackgroundColor(color); - invalidate(); - } - - public boolean isIndeterminate() { - return mIndeterminate; - } - - public void setIndeterminate(boolean indeterminate) { - if (mIndeterminate != indeterminate) { - mIndeterminate = indeterminate; - if (indeterminate) { - startAnimations(); - } else { - stopAnimations(); - } - invalidate(); - } - } - - public void setProgress(float progress) { - if (!mIndeterminate) { - mTrimStart = 0.0f; - mTrimEnd = progress; - mTrimOffset = 0.0f; - mTrimRotation = 0.0f; - invalidate(); - } - } - - @Override - public void onRender(GLCanvas canvas) { - update(); - - int width = getWidth(); - int height = getHeight(); - int cx = width / 2; - int cy = height / 2; - - float startAngle = (mTrimStart + mTrimOffset) * 360.0f - 90; - float sweepAngle = Math.max(12.0f, (mTrimEnd - mTrimStart) * 360.0f); - float rotation = mTrimRotation + startAngle; - - canvas.save(); - - canvas.translate(cx, cy); - canvas.rotate(rotation, 0, 0, 1); - if (rotation % 180 != 0) { - canvas.translate(-cy, -cx); - } else { - canvas.translate(-cx, -cy); - } - - canvas.drawArc(mCx, mCy, mRadiusX, mRadiusY, sweepAngle, mGLPaint); - - canvas.restore(); - } - - private void update() { - boolean invalidate = false; - - if (mIndeterminate) { - long currentTime = AnimationTime.get(); - List animations = mAnimations; - for (int i = 0, n = animations.size(); i < n; i++) { - invalidate |= animations.get(i).calculate(currentTime); - } - } - - if (invalidate) { - invalidate(); - } - } -} diff --git a/app/src/main/java/com/hippo/glview/widget/GLTextureView.java b/app/src/main/java/com/hippo/glview/widget/GLTextureView.java deleted file mode 100644 index 240d19230..000000000 --- a/app/src/main/java/com/hippo/glview/widget/GLTextureView.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.glview.widget; - -import android.graphics.RectF; - -import com.hippo.glview.glrenderer.GLCanvas; -import com.hippo.glview.glrenderer.Texture; -import com.hippo.glview.view.GLView; - -public class GLTextureView extends GLView { - - private final RectF mSrc = new RectF(); - private final RectF mDst = new RectF(); - - private Texture mTexture; - - public Texture getTexture() { - return mTexture; - } - - public void setTexture(Texture texture) { - mTexture = texture; - if (texture != null) { - mSrc.set(0.0f, 0.0f, texture.getWidth(), texture.getHeight()); - } else { - mSrc.setEmpty(); - } - invalidate(); - } - - @Override - protected int getSuggestedMinimumWidth() { - return mTexture == null ? super.getSuggestedMinimumWidth() : mTexture.getWidth() + mPaddings.width(); - } - - @Override - protected int getSuggestedMinimumHeight() { - return mTexture == null ? super.getSuggestedMinimumWidth() : mTexture.getHeight() + mPaddings.height(); - } - - @Override - public void onRender(GLCanvas canvas) { - if (mTexture == null || mSrc.isEmpty()) { - return; - } - - mDst.set(mPaddings.left, mPaddings.top, getWidth() - mPaddings.right, getHeight() - mPaddings.bottom); - if (mDst.isEmpty()) { - return; - } - - mTexture.draw(canvas, mSrc, mDst); - } -} diff --git a/app/src/main/java/com/hippo/image/Image.kt b/app/src/main/java/com/hippo/image/Image.kt index 374b37db9..ccf7073e5 100644 --- a/app/src/main/java/com/hippo/image/Image.kt +++ b/app/src/main/java/com/hippo/image/Image.kt @@ -17,19 +17,14 @@ */ package com.hippo.image -import android.content.res.Resources -import android.graphics.Bitmap -import android.graphics.Canvas import android.graphics.ImageDecoder -import android.graphics.ImageDecoder.ALLOCATOR_SOFTWARE +import android.graphics.ImageDecoder.ALLOCATOR_HARDWARE import android.graphics.ImageDecoder.DecodeException import android.graphics.ImageDecoder.ImageInfo import android.graphics.ImageDecoder.Source -import android.graphics.PixelFormat import android.graphics.drawable.AnimatedImageDrawable import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable -import androidx.core.graphics.drawable.toDrawable import com.hippo.ehviewer.EhApplication import java.io.FileInputStream import java.nio.ByteBuffer @@ -37,23 +32,15 @@ import java.nio.channels.FileChannel import kotlin.math.max import kotlin.math.min -class Image private constructor( - source: Source?, drawable: Drawable? = null, - val release: () -> Unit? = {} -) { - private var mObtainedDrawable: Drawable? - private var mBitmap: Bitmap? = null - private var mCanvas: Canvas? = null +class Image private constructor(source: Source?, val release: () -> Unit = {}) { + var mObtainedDrawable: Drawable? init { mObtainedDrawable = null source?.let { mObtainedDrawable = ImageDecoder.decodeDrawable(source) { decoder: ImageDecoder, info: ImageInfo, src: Source -> - decoder.allocator = ALLOCATOR_SOFTWARE - // Sadly we must use software memory since we need copy it to tile buffer, fuck glgallery - // Idk it will cause how much performance regression - + decoder.allocator = ALLOCATOR_HARDWARE decoder.setTargetSampleSize( max( min( @@ -64,16 +51,10 @@ class Image private constructor( ) } // Should we lazy decode it? } - if (mObtainedDrawable == null) { - mObtainedDrawable = drawable!! - } } - val animated = mObtainedDrawable is AnimatedImageDrawable val width = mObtainedDrawable!!.intrinsicWidth val height = mObtainedDrawable!!.intrinsicHeight - val isRecycled = mObtainedDrawable == null - var started = false @Synchronized @@ -86,59 +67,15 @@ class Image private constructor( } mObtainedDrawable?.callback = null mObtainedDrawable = null - mCanvas = null - mBitmap?.recycle() - mBitmap = null release() } - private fun prepareBitmap() { - if (mBitmap != null) return - mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) - mCanvas = Canvas(mBitmap!!) - } - - private fun updateBitmap() { - prepareBitmap() - mObtainedDrawable!!.draw(mCanvas!!) - } - - fun texImage(init: Boolean, offsetX: Int, offsetY: Int, width: Int, height: Int) { - val bitmap: Bitmap? = if (animated) { - updateBitmap() - mBitmap - } else { - (mObtainedDrawable as BitmapDrawable?)?.bitmap - } - bitmap ?: return - nativeTexImage( - bitmap, - init, - offsetX, - offsetY, - width, - height - ) - } - fun start() { if (!started) { (mObtainedDrawable as AnimatedImageDrawable?)?.start() } } - val delay: Int - get() { - if (animated) - return 10 - return 0 - } - - val isOpaque: Boolean - get() { - return mObtainedDrawable?.opacity == PixelFormat.OPAQUE - } - companion object { var screenWidth: Int = 0 var screenHeight: Int = 0 @@ -169,20 +106,5 @@ class Image private constructor( release() } } - - @JvmStatic - fun create(bitmap: Bitmap): Image { - return Image(null, bitmap.toDrawable(Resources.getSystem())) - } - - @JvmStatic - private external fun nativeTexImage( - bitmap: Bitmap, - init: Boolean, - offsetX: Int, - offsetY: Int, - width: Int, - height: Int - ) } } \ No newline at end of file diff --git a/app/src/main/java/com/hippo/widget/BatteryView.java b/app/src/main/java/com/hippo/widget/BatteryView.java deleted file mode 100644 index f7c9c51dd..000000000 --- a/app/src/main/java/com/hippo/widget/BatteryView.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright (C) 2014 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.widget; - -import android.annotation.SuppressLint; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.res.TypedArray; -import android.graphics.Color; -import android.os.BatteryManager; -import android.util.AttributeSet; - -import androidx.appcompat.widget.AppCompatTextView; - -import com.hippo.drawable.BatteryDrawable; -import com.hippo.ehviewer.R; - -public class BatteryView extends AppCompatTextView { - - private int mColor; - private int mWarningColor; - private int mCurrentColor; - - private int mLevel = 0; - private boolean mCharging = false; - - private BatteryDrawable mDrawable; - private boolean mAttached = false; - private boolean mIsChargerWorking = false; - - public BatteryView(Context context) { - super(context); - init(); - } - - public BatteryView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public BatteryView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - - init(); - - TypedArray typedArray = context.obtainStyledAttributes( - attrs, R.styleable.BatteryView, defStyleAttr, 0); - mColor = typedArray.getColor(R.styleable.BatteryView_color, Color.WHITE); - mWarningColor = typedArray.getColor(R.styleable.BatteryView_warningColor, Color.RED); - typedArray.recycle(); - - mDrawable.setColor(mColor); - mDrawable.setWarningColor(mWarningColor); - } - - private void init() { - mDrawable = new BatteryDrawable(); - int height = (int) getTextSize(); - mDrawable.setBounds(0, 0, (int) (height / 0.618f), height); - setCompoundDrawables(mDrawable, null, null, null); - } - - @Override - public void setTextColor(int color) { - if (mCurrentColor == color) { - return; - } - mCurrentColor = color; - super.setTextColor(color); - } - - private void startCharger() { - if (!mIsChargerWorking) { - getHandler().post(mCharger); - mIsChargerWorking = true; - } - } - - private void stopCharger() { - if (mIsChargerWorking) { - getHandler().removeCallbacks(mCharger); - mIsChargerWorking = false; - } - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - - if (!mAttached) { - mAttached = true; - - registerReceiver(); - } - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - - if (mAttached) { - unregisterReceiver(); - stopCharger(); - mAttached = false; - } - } - - private void registerReceiver() { - final IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_BATTERY_CHANGED); - getContext().registerReceiver(mIntentReceiver, filter, null, getHandler()); - } private final Runnable mCharger = new Runnable() { - - private int level = 0; - - @Override - public void run() { - level += 2; - if (level > 100) { - level = 0; - } - mDrawable.setElect(level, false); - getHandler().postDelayed(mCharger, 200); - } - }; - - private void unregisterReceiver() { - getContext().unregisterReceiver(mIntentReceiver); - } - - - - - private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { - - @Override - @SuppressLint("SetTextI18n") - public void onReceive(Context context, Intent intent) { - - int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0) - * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, 0); - boolean charging = (intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1) - == BatteryManager.BATTERY_STATUS_CHARGING); - - if (mLevel != level || mCharging != charging) { - mLevel = level; - mCharging = charging; - if (mCharging && mLevel != 100) { - startCharger(); - } else { - stopCharger(); - mDrawable.setElect(mLevel); - } - - if (level <= BatteryDrawable.WARN_LIMIT && !charging) { - setTextColor(mWarningColor); - } else { - setTextColor(mColor); - } - setText(mLevel + "%"); - } - } - }; - - -} diff --git a/app/src/main/java/com/hippo/widget/ColorView.java b/app/src/main/java/com/hippo/widget/ColorView.java deleted file mode 100644 index 077bdc239..000000000 --- a/app/src/main/java/com/hippo/widget/ColorView.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (C) 2014 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.widget; - -import android.content.Context; -import android.graphics.Canvas; -import android.util.AttributeSet; -import android.view.View; - -public class ColorView extends View { - - private int mColor; - - public ColorView(Context context) { - super(context); - } - - public ColorView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public ColorView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - public void setColor(int color) { - if (mColor != color) { - mColor = color; - invalidate(); - } - } - - @Override - protected void onDraw(Canvas canvas) { - canvas.drawColor(mColor); - } -} diff --git a/app/src/main/java/com/hippo/widget/DateUtils.java b/app/src/main/java/com/hippo/widget/DateUtils.java deleted file mode 100644 index 74f6ce608..000000000 --- a/app/src/main/java/com/hippo/widget/DateUtils.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (C) 2014 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.widget; - -final class DateUtils { - - public static final char QUOTE = '\''; - public static final char SECONDS = 's'; - - public static boolean hasSeconds(CharSequence inFormat) { - return hasDesignator(inFormat, SECONDS); - } - - public static boolean hasDesignator(CharSequence inFormat, char designator) { - if (inFormat == null) return false; - - final int length = inFormat.length(); - - int c; - int count; - - for (int i = 0; i < length; i += count) { - count = 1; - c = inFormat.charAt(i); - - if (c == QUOTE) { - count = skipQuotedText(inFormat, i, length); - } else if (c == designator) { - return true; - } - } - - return false; - } - - private static int skipQuotedText(CharSequence s, int i, int len) { - if (i + 1 < len && s.charAt(i + 1) == QUOTE) { - return 2; - } - - int count = 1; - // skip leading quote - i++; - - while (i < len) { - char c = s.charAt(i); - - if (c == QUOTE) { - count++; - // QUOTEQUOTE -> QUOTE - if (i + 1 < len && s.charAt(i + 1) == QUOTE) { - i++; - } else { - break; - } - } else { - i++; - count++; - } - } - - return count; - } -} diff --git a/app/src/main/java/com/hippo/widget/TextClock.java b/app/src/main/java/com/hippo/widget/TextClock.java deleted file mode 100644 index e55db6675..000000000 --- a/app/src/main/java/com/hippo/widget/TextClock.java +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Copyright (C) 2014 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.widget; - -import android.content.BroadcastReceiver; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.database.ContentObserver; -import android.net.Uri; -import android.os.Handler; -import android.os.SystemClock; -import android.provider.Settings; -import android.text.format.DateFormat; -import android.util.AttributeSet; -import android.view.ViewDebug.ExportedProperty; - -import androidx.appcompat.widget.AppCompatTextView; - -import java.util.Calendar; -import java.util.TimeZone; - -public class TextClock extends AppCompatTextView { - - public static final CharSequence DEFAULT_FORMAT_12_HOUR = "hh:mm a"; - public static final CharSequence DEFAULT_FORMAT_24_HOUR; - - static { - DEFAULT_FORMAT_24_HOUR = "HH:mm"; - } - - private CharSequence mFormat12; - private CharSequence mFormat24; - - @ExportedProperty - private CharSequence mFormat; - @ExportedProperty - private boolean mHasSeconds; - - private boolean mAttached; - - private Calendar mTime; - private String mTimeZone; - private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (mTimeZone == null && Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) { - final String timeZone = intent.getStringExtra("time-zone"); - createTime(timeZone); - } - onTimeChanged(); - } - }; - - public TextClock(Context context) { - this(context, null); - } - - public TextClock(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public TextClock(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - mFormat12 = DEFAULT_FORMAT_12_HOUR; - mFormat24 = DEFAULT_FORMAT_24_HOUR; - mTimeZone = TimeZone.getDefault().getID(); - - init(); - } - - private void init() { - createTime(mTimeZone); - // Wait until onAttachedToWindow() to handle the ticker - chooseFormat(false); - } - - private void createTime(String timeZone) { - if (timeZone != null) { - mTime = Calendar.getInstance(TimeZone.getTimeZone(timeZone)); - } else { - mTime = Calendar.getInstance(); - } - } - - public CharSequence getFormat12Hour() { - return mFormat12; - } - - public void setFormat12Hour(CharSequence format) { - mFormat12 = format; - - chooseFormat(); - onTimeChanged(); - } - - public CharSequence getFormat24Hour() { - return mFormat24; - } - - public void setFormat24Hour(CharSequence format) { - mFormat24 = format; - - chooseFormat(); - onTimeChanged(); - } - - public boolean is24HourModeEnabled() { - return DateFormat.is24HourFormat(getContext()); - } private final Runnable mTicker = new Runnable() { - @Override - public void run() { - onTimeChanged(); - - long now = SystemClock.uptimeMillis(); - long next = now + (1000 - now % 1000); - - getHandler().postAtTime(mTicker, next); - } - }; - - public String getTimeZone() { - return mTimeZone; - } - - public void setTimeZone(String timeZone) { - mTimeZone = timeZone; - - createTime(timeZone); - onTimeChanged(); - } - - /** - * Selects either one of {@link #getFormat12Hour()} or {@link #getFormat24Hour()} - * depending on whether the user has selected 24-hour format. - *

- * Calling this method does not schedule or unschedule the time ticker. - */ - private void chooseFormat() { - chooseFormat(true); - } - - public CharSequence getFormat() { - return mFormat; - } - - /** - * Selects either one of {@link #getFormat12Hour()} or {@link #getFormat24Hour()} - * depending on whether the user has selected 24-hour format. - * - * @param handleTicker true if calling this method should schedule/unschedule the - * time ticker, false otherwise - */ - private void chooseFormat(boolean handleTicker) { - final boolean format24Requested = is24HourModeEnabled(); - - if (format24Requested) { - mFormat = mFormat24; - } else { - mFormat = mFormat12; - } - - boolean hadSeconds = mHasSeconds; - mHasSeconds = DateUtils.hasSeconds(mFormat); - - if (handleTicker && mAttached && hadSeconds != mHasSeconds) { - if (hadSeconds) getHandler().removeCallbacks(mTicker); - else mTicker.run(); - } - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - - if (!mAttached) { - mAttached = true; - - registerReceiver(); - registerObserver(); - - createTime(mTimeZone); - - if (mHasSeconds) { - mTicker.run(); - } else { - onTimeChanged(); - } - } - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - - if (mAttached) { - unregisterReceiver(); - unregisterObserver(); - - getHandler().removeCallbacks(mTicker); - - mAttached = false; - } - } - - private void registerReceiver() { - final IntentFilter filter = new IntentFilter(); - - filter.addAction(Intent.ACTION_TIME_TICK); - filter.addAction(Intent.ACTION_TIME_CHANGED); - filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); - - getContext().registerReceiver(mIntentReceiver, filter, null, getHandler()); - } - - private void registerObserver() { - final ContentResolver resolver = getContext().getContentResolver(); - resolver.registerContentObserver(Settings.System.CONTENT_URI, true, mFormatChangeObserver); - } - - private void unregisterReceiver() { - getContext().unregisterReceiver(mIntentReceiver); - } - - private void unregisterObserver() { - final ContentResolver resolver = getContext().getContentResolver(); - resolver.unregisterContentObserver(mFormatChangeObserver); - } - - private void onTimeChanged() { - mTime.setTimeInMillis(System.currentTimeMillis()); - setText(DateFormat.format(mFormat, mTime)); - } private final ContentObserver mFormatChangeObserver = new ContentObserver(new Handler()) { - @Override - public void onChange(boolean selfChange) { - chooseFormat(); - onTimeChanged(); - } - - @Override - public void onChange(boolean selfChange, Uri uri) { - chooseFormat(); - onTimeChanged(); - } - }; - - - - - - -} \ No newline at end of file diff --git a/app/src/main/java/com/hippo/yorozuya/ColorUtils.java b/app/src/main/java/com/hippo/yorozuya/ColorUtils.java deleted file mode 100644 index a78306968..000000000 --- a/app/src/main/java/com/hippo/yorozuya/ColorUtils.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2015 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.yorozuya; - -public final class ColorUtils { - private ColorUtils() { - } - - /** - * Check is it the color opaque - * - * @param color the color - * @return true if it is opaque - */ - public static boolean isOpaque(int color) { - return color >>> 24 == 0xFF; - } -} diff --git a/app/src/main/java/com/hippo/yorozuya/Pool.java b/app/src/main/java/com/hippo/yorozuya/Pool.java deleted file mode 100644 index 14440183c..000000000 --- a/app/src/main/java/com/hippo/yorozuya/Pool.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.yorozuya; - -public class Pool { - - private final T[] mArray; - private final int mMaxSize; - private int mSize; - - @SuppressWarnings("unchecked") - public Pool(int size) { - if (size <= 0) { - throw new IllegalStateException("Pool size must > 0, it is " + size); - } - mArray = (T[]) new Object[size]; - mMaxSize = size; - mSize = 0; - } - - public void push(T t) { - if (t != null && mSize < mMaxSize) { - mArray[mSize++] = t; - } - } - - public T pop() { - if (mSize > 0) { - T t = mArray[--mSize]; - mArray[mSize] = null; - return t; - } else { - return null; - } - } -} diff --git a/app/src/main/java/com/hippo/yorozuya/ResourcesUtils.java b/app/src/main/java/com/hippo/yorozuya/ResourcesUtils.java deleted file mode 100644 index 74a4091ce..000000000 --- a/app/src/main/java/com/hippo/yorozuya/ResourcesUtils.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2015 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.yorozuya; - -import android.content.Context; -import android.content.res.Resources; -import android.util.TypedValue; - -import androidx.annotation.AttrRes; - -public final class ResourcesUtils { - private static final Object mAccessLock = new Object(); - private static TypedValue mTmpValue = new TypedValue(); - - private ResourcesUtils() { - } - - public static float getFloat(Resources resources, int resId) { - TypedValue outValue = new TypedValue(); - resources.getValue(resId, outValue, true); - return outValue.getFloat(); - } - - private static void getAttrValue(Context context, int attrId, TypedValue value) { - context.getTheme().resolveAttribute(attrId, value, true); - } - - public static int getAttrColor(Context context, @AttrRes int attrId) { - TypedValue value; - synchronized (mAccessLock) { - value = mTmpValue; - if (value == null) { - value = new TypedValue(); - } - getAttrValue(context, attrId, value); - if (value.type >= TypedValue.TYPE_FIRST_INT - && value.type <= TypedValue.TYPE_LAST_INT) { - mTmpValue = value; - return value.data; - } else { - throw new Resources.NotFoundException( - "Attribute ID #0x" + Integer.toHexString(attrId) + " type #0x" - + Integer.toHexString(value.type) + " is not valid"); - } - } - } - - public static boolean getAttrBoolean(Context context, @AttrRes int attrId) { - synchronized (mAccessLock) { - TypedValue value = mTmpValue; - if (value == null) { - mTmpValue = value = new TypedValue(); - } - getAttrValue(context, attrId, value); - if (value.type >= TypedValue.TYPE_FIRST_INT - && value.type <= TypedValue.TYPE_LAST_INT) { - return value.data != 0; - } - throw new Resources.NotFoundException( - "Attribute ID #0x" + Integer.toHexString(attrId) + " type #0x" - + Integer.toHexString(value.type) + " is not valid"); - } - } - - public static int getAttrDimensionPixelOffset(Context context, @AttrRes int attrId) { - synchronized (mAccessLock) { - TypedValue value = mTmpValue; - if (value == null) { - mTmpValue = value = new TypedValue(); - } - getAttrValue(context, attrId, value); - if (value.type == TypedValue.TYPE_DIMENSION) { - return TypedValue.complexToDimensionPixelOffset( - value.data, context.getResources().getDisplayMetrics()); - } - throw new Resources.NotFoundException( - "Attribute ID #0x" + Integer.toHexString(attrId) + " type #0x" - + Integer.toHexString(value.type) + " is not valid"); - } - } -} diff --git a/app/src/main/java/com/hippo/yorozuya/thread/InfiniteThreadExecutor.java b/app/src/main/java/com/hippo/yorozuya/thread/InfiniteThreadExecutor.java deleted file mode 100644 index 38b8a777f..000000000 --- a/app/src/main/java/com/hippo/yorozuya/thread/InfiniteThreadExecutor.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2015-2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.yorozuya.thread; - -import androidx.annotation.NonNull; - -import java.util.Queue; -import java.util.concurrent.Executor; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.atomic.AtomicInteger; - -public class InfiniteThreadExecutor implements Executor { - - private final long mKeepAliveMillis; - private final Queue mWorkQueue; - private final ThreadFactory mThreadFactory; - - private final AtomicInteger mThreadCount = new AtomicInteger(); - private final Object mLock = new Object(); - private int mEmptyThreadCount; - - public InfiniteThreadExecutor(long keepAliveMillis, Queue workQueue, ThreadFactory threadFactory) { - mKeepAliveMillis = keepAliveMillis; - mWorkQueue = workQueue; - mThreadFactory = threadFactory; - } - - @Override - public void execute(@NonNull Runnable command) { - synchronized (mLock) { - mWorkQueue.add(command); - if (mEmptyThreadCount > 0) { - --mEmptyThreadCount; - mLock.notify(); - return; - } - } - - mThreadFactory.newThread(new Task()).start(); - } - - public int getThreadCount() { - return mThreadCount.get(); - } - - private class Task implements Runnable { - - @Override - public void run() { - mThreadCount.incrementAndGet(); - - boolean hasWait = false; - for (; ; ) { - Runnable command; - synchronized (mLock) { - command = mWorkQueue.poll(); - if (command == null) { - if (hasWait) { - --mEmptyThreadCount; - } - break; - } - } - - try { - command.run(); - } catch (Exception e) { - e.printStackTrace(); - } - - synchronized (mLock) { - ++mEmptyThreadCount; - try { - mLock.wait(mKeepAliveMillis); - } catch (InterruptedException e) { - // Ignore - } - } - - hasWait = true; - } - - mThreadCount.decrementAndGet(); - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/core/preference/AndroidPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/core/preference/AndroidPreference.kt new file mode 100644 index 000000000..05d63ee18 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/core/preference/AndroidPreference.kt @@ -0,0 +1,175 @@ +package eu.kanade.tachiyomi.core.preference + +import android.content.SharedPreferences +import android.content.SharedPreferences.Editor +import androidx.core.content.edit +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn + +sealed class AndroidPreference( + private val preferences: SharedPreferences, + private val keyFlow: Flow, + private val key: String, + private val defaultValue: T, +) : Preference { + + abstract fun read(preferences: SharedPreferences, key: String, defaultValue: T): T + + abstract fun write(key: String, value: T): Editor.() -> Unit + + override fun key(): String { + return key + } + + override fun get(): T { + return read(preferences, key, defaultValue) + } + + override fun set(value: T) { + preferences.edit(action = write(key, value)) + } + + override fun isSet(): Boolean { + return preferences.contains(key) + } + + override fun delete() { + preferences.edit { + remove(key) + } + } + + override fun defaultValue(): T { + return defaultValue + } + + override fun changes(): Flow { + return keyFlow + .filter { it == key || it == null } + .onStart { emit("ignition") } + .map { get() } + .conflate() + } + + override fun stateIn(scope: CoroutineScope): StateFlow { + return changes().stateIn(scope, SharingStarted.Eagerly, get()) + } + + class StringPrimitive( + preferences: SharedPreferences, + keyFlow: Flow, + key: String, + defaultValue: String, + ) : AndroidPreference(preferences, keyFlow, key, defaultValue) { + override fun read(preferences: SharedPreferences, key: String, defaultValue: String): String { + return preferences.getString(key, defaultValue) ?: defaultValue + } + + override fun write(key: String, value: String): Editor.() -> Unit = { + putString(key, value) + } + } + + class LongPrimitive( + preferences: SharedPreferences, + keyFlow: Flow, + key: String, + defaultValue: Long, + ) : AndroidPreference(preferences, keyFlow, key, defaultValue) { + override fun read(preferences: SharedPreferences, key: String, defaultValue: Long): Long { + return preferences.getLong(key, defaultValue) + } + + override fun write(key: String, value: Long): Editor.() -> Unit = { + putLong(key, value) + } + } + + class IntPrimitive( + preferences: SharedPreferences, + keyFlow: Flow, + key: String, + defaultValue: Int, + ) : AndroidPreference(preferences, keyFlow, key, defaultValue) { + override fun read(preferences: SharedPreferences, key: String, defaultValue: Int): Int { + return preferences.getInt(key, defaultValue) + } + + override fun write(key: String, value: Int): Editor.() -> Unit = { + putInt(key, value) + } + } + + class FloatPrimitive( + preferences: SharedPreferences, + keyFlow: Flow, + key: String, + defaultValue: Float, + ) : AndroidPreference(preferences, keyFlow, key, defaultValue) { + override fun read(preferences: SharedPreferences, key: String, defaultValue: Float): Float { + return preferences.getFloat(key, defaultValue) + } + + override fun write(key: String, value: Float): Editor.() -> Unit = { + putFloat(key, value) + } + } + + class BooleanPrimitive( + preferences: SharedPreferences, + keyFlow: Flow, + key: String, + defaultValue: Boolean, + ) : AndroidPreference(preferences, keyFlow, key, defaultValue) { + override fun read(preferences: SharedPreferences, key: String, defaultValue: Boolean): Boolean { + return preferences.getBoolean(key, defaultValue) + } + + override fun write(key: String, value: Boolean): Editor.() -> Unit = { + putBoolean(key, value) + } + } + + class StringSetPrimitive( + preferences: SharedPreferences, + keyFlow: Flow, + key: String, + defaultValue: Set, + ) : AndroidPreference>(preferences, keyFlow, key, defaultValue) { + override fun read(preferences: SharedPreferences, key: String, defaultValue: Set): Set { + return preferences.getStringSet(key, defaultValue) ?: defaultValue + } + + override fun write(key: String, value: Set): Editor.() -> Unit = { + putStringSet(key, value) + } + } + + class Object( + preferences: SharedPreferences, + keyFlow: Flow, + key: String, + defaultValue: T, + val serializer: (T) -> String, + val deserializer: (String) -> T, + ) : AndroidPreference(preferences, keyFlow, key, defaultValue) { + override fun read(preferences: SharedPreferences, key: String, defaultValue: T): T { + return try { + preferences.getString(key, null)?.let(deserializer) ?: defaultValue + } catch (e: Exception) { + defaultValue + } + } + + override fun write(key: String, value: T): Editor.() -> Unit = { + putString(key, serializer(value)) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/core/preference/AndroidPreferenceStore.kt b/app/src/main/java/eu/kanade/tachiyomi/core/preference/AndroidPreferenceStore.kt new file mode 100644 index 000000000..f028148d2 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/core/preference/AndroidPreferenceStore.kt @@ -0,0 +1,72 @@ +package eu.kanade.tachiyomi.core.preference + +import android.content.Context +import android.content.SharedPreferences +import androidx.preference.PreferenceManager +import eu.kanade.tachiyomi.core.preference.AndroidPreference.BooleanPrimitive +import eu.kanade.tachiyomi.core.preference.AndroidPreference.FloatPrimitive +import eu.kanade.tachiyomi.core.preference.AndroidPreference.IntPrimitive +import eu.kanade.tachiyomi.core.preference.AndroidPreference.LongPrimitive +import eu.kanade.tachiyomi.core.preference.AndroidPreference.Object +import eu.kanade.tachiyomi.core.preference.AndroidPreference.StringPrimitive +import eu.kanade.tachiyomi.core.preference.AndroidPreference.StringSetPrimitive +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow + +class AndroidPreferenceStore( + context: Context, +) : PreferenceStore { + + private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + + private val keyFlow = sharedPreferences.keyFlow + + override fun getString(key: String, defaultValue: String): Preference { + return StringPrimitive(sharedPreferences, keyFlow, key, defaultValue) + } + + override fun getLong(key: String, defaultValue: Long): Preference { + return LongPrimitive(sharedPreferences, keyFlow, key, defaultValue) + } + + override fun getInt(key: String, defaultValue: Int): Preference { + return IntPrimitive(sharedPreferences, keyFlow, key, defaultValue) + } + + override fun getFloat(key: String, defaultValue: Float): Preference { + return FloatPrimitive(sharedPreferences, keyFlow, key, defaultValue) + } + + override fun getBoolean(key: String, defaultValue: Boolean): Preference { + return BooleanPrimitive(sharedPreferences, keyFlow, key, defaultValue) + } + + override fun getStringSet(key: String, defaultValue: Set): Preference> { + return StringSetPrimitive(sharedPreferences, keyFlow, key, defaultValue) + } + + override fun getObject( + key: String, + defaultValue: T, + serializer: (T) -> String, + deserializer: (String) -> T, + ): Preference { + return Object( + preferences = sharedPreferences, + keyFlow = keyFlow, + key = key, + defaultValue = defaultValue, + serializer = serializer, + deserializer = deserializer, + ) + } +} + +private val SharedPreferences.keyFlow + get() = callbackFlow { + val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key: String? -> trySend(key) } + registerOnSharedPreferenceChangeListener(listener) + awaitClose { + unregisterOnSharedPreferenceChangeListener(listener) + } + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/core/preference/Preference.kt b/app/src/main/java/eu/kanade/tachiyomi/core/preference/Preference.kt new file mode 100644 index 000000000..dc898f4d8 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/core/preference/Preference.kt @@ -0,0 +1,26 @@ +package eu.kanade.tachiyomi.core.preference + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +interface Preference { + + fun key(): String + + fun get(): T + + fun set(value: T) + + fun isSet(): Boolean + + fun delete() + + fun defaultValue(): T + + fun changes(): Flow + + fun stateIn(scope: CoroutineScope): StateFlow +} + +inline fun Preference.getAndSet(crossinline block: (T) -> R) = set(block(get())) diff --git a/app/src/main/java/eu/kanade/tachiyomi/core/preference/PreferenceStore.kt b/app/src/main/java/eu/kanade/tachiyomi/core/preference/PreferenceStore.kt new file mode 100644 index 000000000..c4a1abe83 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/core/preference/PreferenceStore.kt @@ -0,0 +1,41 @@ +package eu.kanade.tachiyomi.core.preference + +interface PreferenceStore { + + fun getString(key: String, defaultValue: String = ""): Preference + + fun getLong(key: String, defaultValue: Long = 0): Preference + + fun getInt(key: String, defaultValue: Int = 0): Preference + + fun getFloat(key: String, defaultValue: Float = 0f): Preference + + fun getBoolean(key: String, defaultValue: Boolean = false): Preference + + fun getStringSet(key: String, defaultValue: Set = emptySet()): Preference> + + fun getObject( + key: String, + defaultValue: T, + serializer: (T) -> String, + deserializer: (String) -> T, + ): Preference +} + +inline fun > PreferenceStore.getEnum( + key: String, + defaultValue: T, +): Preference { + return getObject( + key = key, + defaultValue = defaultValue, + serializer = { it.name }, + deserializer = { + try { + enumValueOf(it) + } catch (e: IllegalArgumentException) { + defaultValue + } + }, + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceValues.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceValues.kt new file mode 100644 index 000000000..50ed91a91 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceValues.kt @@ -0,0 +1,25 @@ +package eu.kanade.tachiyomi.data.preference + +const val MANGA_NON_COMPLETED = "manga_ongoing" +const val MANGA_HAS_UNREAD = "manga_fully_read" +const val MANGA_NON_READ = "manga_started" + +/** + * This class stores the values for the preferences in the application. + */ +object PreferenceValues { + + enum class TappingInvertMode(val shouldInvertHorizontal: Boolean = false, val shouldInvertVertical: Boolean = false) { + NONE, + HORIZONTAL(shouldInvertHorizontal = true), + VERTICAL(shouldInvertVertical = true), + BOTH(shouldInvertHorizontal = true, shouldInvertVertical = true), + } + + enum class ReaderHideThreshold(val threshold: Int) { + HIGHEST(5), + HIGH(13), + LOW(31), + LOWEST(47), + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt new file mode 100644 index 000000000..586067597 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt @@ -0,0 +1,21 @@ +package eu.kanade.tachiyomi.source.model + +import kotlinx.coroutines.flow.MutableStateFlow + +open class Page(val index: Int) { + + val number: Int + get() = index + 1 + + var status = MutableStateFlow(State.QUEUE) + + var progress = MutableStateFlow(0f) + + enum class State { + QUEUE, + LOAD_PAGE, + DOWNLOAD_IMAGE, + READY, + ERROR, + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/PageIndicatorTextView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/PageIndicatorTextView.kt new file mode 100644 index 000000000..d0cd94f2a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/PageIndicatorTextView.kt @@ -0,0 +1,52 @@ +package eu.kanade.tachiyomi.ui.reader + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Color +import android.text.Spannable +import android.text.SpannableString +import android.text.style.ScaleXSpan +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView +import eu.kanade.tachiyomi.widget.OutlineSpan + +/** + * Page indicator found at the bottom of the reader + */ +class PageIndicatorTextView( + context: Context, + attrs: AttributeSet? = null, +) : AppCompatTextView(context, attrs) { + + init { + setTextColor(fillColor) + } + + @SuppressLint("SetTextI18n") + override fun setText(text: CharSequence?, type: BufferType?) { + // Add spaces at the start & end of the text, otherwise the stroke is cut-off because it's + // not taken into account when measuring the text (view's padding doesn't help). + val currText = " $text " + + // Also add a bit of spacing between each character, as the stroke overlaps them + val finalText = SpannableString(currText.asIterable().joinToString("\u00A0")).apply { + // Apply text outline + setSpan(spanOutline, 1, length - 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + + for (i in 1..lastIndex step 2) { + setSpan(ScaleXSpan(0.2f), i, i + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + } + + super.setText(finalText, BufferType.SPANNABLE) + } +} + +private val fillColor = Color.rgb(235, 235, 235) +private val strokeColor = Color.rgb(45, 45, 45) + +// A span object with text outlining properties +private val spanOutline = OutlineSpan( + strokeColor = strokeColor, + strokeWidth = 4f, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt new file mode 100644 index 000000000..2433e5807 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -0,0 +1,1190 @@ +/* + * Copyright 2016 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.kanade.tachiyomi.ui.reader + +import android.Manifest +import android.annotation.SuppressLint +import android.app.assist.AssistContent +import android.content.ClipData +import android.content.ClipboardManager +import android.content.ContentValues +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Color +import android.graphics.ColorMatrix +import android.graphics.ColorMatrixColorFilter +import android.graphics.Paint +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Environment +import android.os.Handler +import android.os.Message +import android.provider.MediaStore +import android.text.TextUtils +import android.view.HapticFeedbackConstants +import android.view.KeyEvent +import android.view.MotionEvent +import android.view.View.LAYER_TYPE_HARDWARE +import android.view.WindowManager +import android.view.animation.Animation +import android.view.animation.AnimationUtils +import android.webkit.MimeTypeMap +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.result.contract.ActivityResultContracts.CreateDocument +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat +import androidx.core.content.FileProvider +import androidx.core.graphics.ColorUtils +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope +import com.google.android.material.shape.MaterialShapeDrawable +import com.google.android.material.slider.Slider +import com.google.android.material.transition.MaterialContainerTransform +import com.hippo.app.EditTextDialogBuilder +import com.hippo.ehviewer.AppConfig +import com.hippo.ehviewer.BuildConfig +import com.hippo.ehviewer.EhApplication +import com.hippo.ehviewer.R +import com.hippo.ehviewer.client.EhUrl +import com.hippo.ehviewer.client.data.GalleryInfo +import com.hippo.ehviewer.databinding.ReaderActivityBinding +import com.hippo.ehviewer.gallery.ArchivePageLoader +import com.hippo.ehviewer.gallery.DirPageLoader +import com.hippo.ehviewer.gallery.EhPageLoader +import com.hippo.ehviewer.gallery.PageLoader2 +import com.hippo.ehviewer.ui.EhActivity +import com.hippo.unifile.UniFile +import com.hippo.util.ExceptionUtils +import com.hippo.yorozuya.FileUtils +import com.hippo.yorozuya.IOUtils +import dev.chrisbanes.insetter.applyInsetter +import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore +import eu.kanade.tachiyomi.ui.reader.loader.PageLoader +import eu.kanade.tachiyomi.ui.reader.model.ReaderPage +import eu.kanade.tachiyomi.ui.reader.setting.OrientationType +import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences +import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsSheet +import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType +import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer +import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer +import eu.kanade.tachiyomi.util.preference.toggle +import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale +import eu.kanade.tachiyomi.util.system.hasDisplayCutout +import eu.kanade.tachiyomi.util.system.isNightMode +import eu.kanade.tachiyomi.util.view.copy +import eu.kanade.tachiyomi.util.view.popupMenu +import eu.kanade.tachiyomi.util.view.setTooltip +import eu.kanade.tachiyomi.widget.listener.SimpleAnimationListener +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.sample +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.lang.ref.WeakReference +import kotlin.math.abs +import kotlin.math.max + +class ReaderActivity : EhActivity() { + lateinit var binding: ReaderActivityBinding + private var mAction: String? = null + private var mFilename: String? = null + private var mUri: Uri? = null + private var mGalleryInfo: GalleryInfo? = null + private var mPage: Int = 0 + private var mCacheFileName: String? = null + + /** + * Whether the menu is currently visible. + */ + var menuVisible = false + private set + + private var saveImageToLauncher = registerForActivityResult( + CreateDocument("todo/todo") + ) { uri -> + if (uri != null) { + val filepath = + AppConfig.getExternalTempDir().toString() + File.separator + mCacheFileName + val cachefile = File(filepath) + val resolver = contentResolver + lifecycleScope.launch { + withContext(Dispatchers.IO) { + var `is`: InputStream? = null + var os: OutputStream? = null + try { + `is` = FileInputStream(cachefile) + os = resolver.openOutputStream(uri) + IOUtils.copy(`is`, os) + } catch (e: IOException) { + e.printStackTrace() + } finally { + IOUtils.closeQuietly(`is`) + IOUtils.closeQuietly(os) + runOnUiThread { + Toast.makeText( + this@ReaderActivity, + getString(R.string.image_saved, uri.path), + Toast.LENGTH_SHORT + ).show() + } + } + cachefile.delete() + } + } + } + } + private var mGalleryProvider: PageLoader2? = null + private var mSize: Int = 0 + private var mCurrentIndex: Int = 0 + private var mSavingPage = -1 + private var builder: EditTextDialogBuilder? = null + private var dialogShown = false + private var dialog: AlertDialog? = null + private var requestStoragePermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { result -> + if (result!! && mSavingPage != -1) { + saveImage(mSavingPage) + } else { + Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show() + } + mSavingPage = -1 + } + + private val galleryDetailUrl: String? + get() { + val gid: Long + val token: String + if (mGalleryInfo != null) { + gid = mGalleryInfo!!.gid + token = mGalleryInfo!!.token + } else { + return null + } + return EhUrl.getGalleryDetailUrl(gid, token, 0, false) + } + + private fun buildProvider() { + if (mGalleryProvider != null) { + return + } + + if (ACTION_DIR == mAction) { + if (mFilename != null) { + mGalleryProvider = DirPageLoader( + UniFile.fromFile(File(mFilename!!))!! + ) + } + } else if (ACTION_EH == mAction) { + if (mGalleryInfo != null) { + mGalleryProvider = + EhPageLoader(this, mGalleryInfo) + } + } else if (Intent.ACTION_VIEW == mAction) { + if (mUri != null) { + try { + grantUriPermission( + BuildConfig.APPLICATION_ID, + mUri, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + } catch (e: Exception) { + Toast.makeText(this, R.string.error_reading_failed, Toast.LENGTH_SHORT).show() + } + + mGalleryProvider = + ArchivePageLoader(this, mUri) + } + } + } + + private fun onInit() { + val intent = intent ?: return + mAction = intent.action + mFilename = intent.getStringExtra(KEY_FILENAME) + mUri = intent.data + mGalleryInfo = intent.getParcelableExtra(KEY_GALLERY_INFO) + mPage = intent.getIntExtra(KEY_PAGE, -1) + buildProvider() + } + + private fun onRestore(savedInstanceState: Bundle) { + mAction = savedInstanceState.getString(KEY_ACTION) + mFilename = savedInstanceState.getString(KEY_FILENAME) + mUri = savedInstanceState.getParcelable(KEY_URI) + mGalleryInfo = savedInstanceState.getParcelable(KEY_GALLERY_INFO) + mPage = savedInstanceState.getInt(KEY_PAGE, -1) + mCurrentIndex = savedInstanceState.getInt(KEY_CURRENT_INDEX) + buildProvider() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putString(KEY_ACTION, mAction) + outState.putString(KEY_FILENAME, mFilename) + outState.putParcelable(KEY_URI, mUri) + if (mGalleryInfo != null) { + outState.putParcelable(KEY_GALLERY_INFO, mGalleryInfo) + } + outState.putInt(KEY_PAGE, mPage) + outState.putInt(KEY_CURRENT_INDEX, mCurrentIndex) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (savedInstanceState == null) { + onInit() + } else { + onRestore(savedInstanceState) + } + binding = ReaderActivityBinding.inflate(layoutInflater) + setContentView(binding.root) + builder = EditTextDialogBuilder(this, null, getString(R.string.archive_passwd)) + builder!!.setTitle(getString(R.string.archive_need_passwd)) + builder!!.setPositiveButton(getString(android.R.string.ok), null) + dialog = builder!!.create() + dialog!!.setCanceledOnTouchOutside(false) + if (mGalleryProvider == null) { + finish() + return + } + ArchivePageLoader.showPasswd = ShowPasswdDialogHandler(this) + + // Get start page + val startPage: Int = if (savedInstanceState == null) { + if (mPage >= 0) mPage else mGalleryProvider!!.startPage + } else { + mCurrentIndex + } + mCurrentIndex = startPage + + lifecycleScope.launch(Dispatchers.Main) { + mGalleryProvider!!.state.collect { + if (it == PageLoader.STATE_READY) { + setGallery() + cancel() + } + } + } + + mGalleryProvider!!.start() + + config = ReaderConfig() + initializeMenu() + } + + fun setGallery() { + mSize = mGalleryProvider!!.size() + val viewerMode = + ReadingModeType.fromPreference(readerPreferences.defaultReadingMode().get()) + binding.actionReadingMode.setImageResource(viewerMode.iconRes) + viewer?.destroy() + viewer = ReadingModeType.toViewer(readerPreferences.defaultReadingMode().get(), this) + updateViewerInset(readerPreferences.fullscreen().get()) + binding.viewerContainer.removeAllViews() + setOrientation(readerPreferences.defaultOrientationType().get()) + binding.viewerContainer.addView(viewer?.getView()) + viewer?.setGalleryProvider(mGalleryProvider!!) + moveToPageIndex(mCurrentIndex) + } + + override fun onDestroy() { + super.onDestroy() + config = null + viewer?.destroy() + viewer = null + if (mGalleryProvider != null) { + mGalleryProvider!!.stop() + mGalleryProvider = null + } + } + + fun shareImage(page: Int) { + if (null == mGalleryProvider) { + return + } + + val dir = AppConfig.getExternalTempDir() + if (null == dir) { + Toast.makeText(this, R.string.error_cant_create_temp_file, Toast.LENGTH_SHORT).show() + return + } + val file = mGalleryProvider!!.save( + page, + UniFile.fromFile(dir)!!, + mGalleryProvider!!.getImageFilename(page) + ) + if (file == null) { + Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show() + return + } + val filename = file.name + if (filename == null) { + Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show() + return + } + + var mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension( + MimeTypeMap.getFileExtensionFromUrl(filename) + ) + if (TextUtils.isEmpty(mimeType)) { + mimeType = "image/jpeg" + } + + val uri = FileProvider.getUriForFile( + this, + BuildConfig.APPLICATION_ID + ".fileprovider", + File(dir, filename) + ) + + val intent = Intent() + intent.action = Intent.ACTION_SEND + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + intent.putExtra(Intent.EXTRA_STREAM, uri) + if (mGalleryInfo != null) + intent.putExtra( + Intent.EXTRA_TEXT, + EhUrl.getGalleryDetailUrl(mGalleryInfo!!.gid, mGalleryInfo!!.token) + ) + intent.setDataAndType(uri, mimeType) + + try { + startActivity(Intent.createChooser(intent, getString(R.string.share_image))) + } catch (e: Throwable) { + ExceptionUtils.throwIfFatal(e) + Toast.makeText(this, R.string.error_cant_find_activity, Toast.LENGTH_SHORT).show() + } + + } + + private fun copyImage(page: Int) { + if (null == mGalleryProvider) { + return + } + + val dir = AppConfig.getExternalCopyTempDir() + if (null == dir) { + Toast.makeText(this, R.string.error_cant_create_temp_file, Toast.LENGTH_SHORT).show() + return + } + val file = mGalleryProvider!!.save( + page, + UniFile.fromFile(dir)!!, + mGalleryProvider!!.getImageFilename(page) + ) + if (file == null) { + Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show() + return + } + val filename = file.name + if (filename == null) { + Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show() + return + } + + val uri = FileProvider.getUriForFile( + this, + BuildConfig.APPLICATION_ID + ".fileprovider", + File(dir, filename) + ) + + val clipboardManager = getSystemService(ClipboardManager::class.java) + if (clipboardManager != null) { + val clipData = ClipData.newUri(contentResolver, "ehviewer", uri) + clipboardManager.setPrimaryClip(clipData) + Toast.makeText(this, getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT).show() + } + } + + private fun saveImage(page: Int) { + if (null == mGalleryProvider) { + return + } + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission( + this, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) != PackageManager.PERMISSION_GRANTED + ) { + mSavingPage = page + requestStoragePermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + return + } + + val filename = mGalleryProvider!!.getImageFilenameWithExtension(page) + var mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension( + MimeTypeMap.getFileExtensionFromUrl(filename) + ) + if (TextUtils.isEmpty(mimeType)) { + mimeType = "image/jpeg" + } + + val realPath: String + val resolver = contentResolver + val values = ContentValues() + values.put(MediaStore.MediaColumns.DISPLAY_NAME, filename) + values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis()) + values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + values.put( + MediaStore.MediaColumns.RELATIVE_PATH, + Environment.DIRECTORY_PICTURES + File.separator + AppConfig.APP_DIRNAME + ) + values.put(MediaStore.MediaColumns.IS_PENDING, 1) + realPath = Environment.DIRECTORY_PICTURES + File.separator + AppConfig.APP_DIRNAME + } else { + val path = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), + AppConfig.APP_DIRNAME + ) + realPath = path.toString() + if (!FileUtils.ensureDirectory(path)) { + Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show() + return + } + values.put(MediaStore.MediaColumns.DATA, path.toString() + File.separator + filename) + } + val imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) + if (imageUri == null) { + Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show() + return + } + if (!mGalleryProvider!!.save(page, UniFile.fromMediaUri(this, imageUri))) { + try { + resolver.delete(imageUri, null, null) + } catch (e: Exception) { + e.printStackTrace() + } + + Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show() + return + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val contentValues = ContentValues() + contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0) + resolver.update(imageUri, contentValues, null, null) + } + + Toast.makeText( + this, + getString(R.string.image_saved, realPath + File.separator + filename), + Toast.LENGTH_SHORT + ).show() + } + + fun saveImageTo(page: Int) { + if (null == mGalleryProvider) { + return + } + val dir = AppConfig.getExternalTempDir() + if (null == dir) { + Toast.makeText(this, R.string.error_cant_create_temp_file, Toast.LENGTH_SHORT).show() + return + } + val file = mGalleryProvider!!.save( + page, + UniFile.fromFile(dir)!!, + mGalleryProvider!!.getImageFilename(page) + ) + if (file == null) { + Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show() + return + } + val filename = file.name + if (filename == null) { + Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show() + return + } + mCacheFileName = filename + try { + saveImageToLauncher.launch(filename) + } catch (e: Throwable) { + ExceptionUtils.throwIfFatal(e) + Toast.makeText(this, R.string.error_cant_find_activity, Toast.LENGTH_SHORT).show() + } + + } + + override fun onProvideAssistContent(outContent: AssistContent) { + super.onProvideAssistContent(outContent) + + val url = galleryDetailUrl + if (url != null) { + outContent.webUri = Uri.parse(url) + } + } + + private fun showPasswdDialog() { + if (!dialogShown) { + dialogShown = true + dialog!!.show() + if (dialog!!.getButton(AlertDialog.BUTTON_POSITIVE) != null) { + dialog!!.getButton(AlertDialog.BUTTON_POSITIVE) + .setOnClickListener { v -> this@ReaderActivity.onProvidePasswd() } + } + dialog!!.setOnCancelListener { v -> finish() } + } + } + + private fun onProvidePasswd() { + val passwd = builder!!.text + if (passwd.isEmpty()) + builder!!.setError(getString(R.string.passwd_cannot_be_empty)) + else { + ArchivePageLoader.passwd = passwd + ArchivePageLoader.pv.v() + } + } + + private fun onPasswdWrong() { + builder!!.setError(getString(R.string.passwd_wrong)) + } + + private fun onPasswdCorrect() { + dialog!!.dismiss() + } + + private class ShowPasswdDialogHandler(activity: ReaderActivity) : Handler() { + private val weakReference: WeakReference + + init { + this.weakReference = WeakReference(activity) + } + + override fun handleMessage(msg: Message) { + val activity = weakReference.get() + super.handleMessage(msg) + if (null != activity) { + when (msg.what) { + 0 -> activity.showPasswdDialog() + 1 -> activity.onPasswdWrong() + 2 -> activity.onPasswdCorrect() + } + } + } + } + + companion object { + const val ACTION_DIR = "dir" + const val ACTION_EH = "eh" + const val KEY_ACTION = "action" + const val KEY_FILENAME = "filename" + const val KEY_URI = "uri" + const val KEY_GALLERY_INFO = "gallery_info" + const val KEY_PAGE = "page" + const val KEY_CURRENT_INDEX = "current_index" + + val readerPreferences by lazy { ReaderPreferences(AndroidPreferenceStore(EhApplication.application)) } + } + + /* Tachiyomi funcs */ + + var isScrollingThroughPages = false + private set + + /** + * Viewer used to display the pages (pager, webtoon, ...). + */ + var viewer: BaseViewer? = null + private set + + val hasCutout by lazy { hasDisplayCutout() } + + private var config: ReaderConfig? = null + + private val windowInsetsController by lazy { + WindowInsetsControllerCompat( + window, + binding.root + ) + } + + /** + * Sets the visibility of the menu according to [visible] and with an optional parameter to + * [animate] the views. + */ + fun setMenuVisibility(visible: Boolean, animate: Boolean = true) { + menuVisible = visible + if (visible) { + windowInsetsController.show(WindowInsetsCompat.Type.systemBars()) + binding.readerMenu.isVisible = true + + if (animate) { + val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_top) + toolbarAnimation.applySystemAnimatorScale(this) + toolbarAnimation.setAnimationListener( + object : SimpleAnimationListener() { + override fun onAnimationStart(animation: Animation) { + // Fix status bar being translucent the first time it's opened. + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) + } + }, + ) + binding.toolbar.startAnimation(toolbarAnimation) + + val bottomAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_bottom) + bottomAnimation.applySystemAnimatorScale(this) + binding.readerMenuBottom.startAnimation(bottomAnimation) + } + + if (readerPreferences.showPageNumber().get()) { + config?.setPageNumberVisibility(false) + } + } else { + if (readerPreferences.fullscreen().get()) { + windowInsetsController.hide(WindowInsetsCompat.Type.systemBars()) + windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + + if (animate) { + val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_top) + toolbarAnimation.applySystemAnimatorScale(this) + toolbarAnimation.setAnimationListener( + object : SimpleAnimationListener() { + override fun onAnimationEnd(animation: Animation) { + binding.readerMenu.isVisible = false + } + }, + ) + binding.toolbar.startAnimation(toolbarAnimation) + + val bottomAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_bottom) + bottomAnimation.applySystemAnimatorScale(this) + binding.readerMenuBottom.startAnimation(bottomAnimation) + } + + if (readerPreferences.showPageNumber().get()) { + config?.setPageNumberVisibility(true) + } + } + } + + /** + * Initializes the reader menu. It sets up click listeners and the initial visibility. + */ + @SuppressLint("PrivateResource") + private fun initializeMenu() { + setSupportActionBar(binding.toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + binding.toolbar.setNavigationOnClickListener { + onBackPressedDispatcher.onBackPressed() + } + + binding.toolbar.applyInsetter { + type(navigationBars = true, statusBars = true) { + margin(top = true, horizontal = true) + } + } + binding.readerMenuBottom.applyInsetter { + type(navigationBars = true) { + margin(bottom = true, horizontal = true) + } + } + + // Init listeners on bottom menu + binding.pageSlider.addOnSliderTouchListener( + object : Slider.OnSliderTouchListener { + override fun onStartTrackingTouch(slider: Slider) { + isScrollingThroughPages = true + } + + override fun onStopTrackingTouch(slider: Slider) { + isScrollingThroughPages = false + } + }, + ) + binding.pageSlider.addOnChangeListener { slider, value, fromUser -> + if (viewer != null && fromUser) { + isScrollingThroughPages = true + moveToPageIndex(value.toInt()) + slider.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK) + } + } + + initBottomShortcuts() + + val toolbarBackground = (binding.toolbar.background as MaterialShapeDrawable).apply { + elevation = + resources.getDimension(com.google.android.material.R.dimen.m3_sys_elevation_level2) + alpha = 0 + } + binding.toolbarBottom.background = toolbarBackground.copy(this@ReaderActivity) + + binding.readerSeekbar.background = toolbarBackground.copy(this@ReaderActivity)?.apply { + setCornerSize(999F) + } + + val toolbarColor = ColorUtils.setAlphaComponent( + toolbarBackground.resolvedTintColor, + toolbarBackground.alpha, + ) + + window.statusBarColor = toolbarColor + window.navigationBarColor = toolbarColor + + // Set initial visibility + setMenuVisibility(menuVisible) + } + + private fun initBottomShortcuts() { + // Reading mode + with(binding.actionReadingMode) { + setTooltip(R.string.viewer) + + setOnClickListener { + popupMenu( + items = ReadingModeType.values().map { it.flagValue to it.stringRes }, + selectedItemId = readerPreferences.defaultReadingMode().get(), + ) { + val newReadingMode = ReadingModeType.fromPreference(itemId) + + readerPreferences.defaultReadingMode().set(newReadingMode.flagValue) + setGallery() + + updateCropBordersShortcut() + } + } + } + + // Crop borders + with(binding.actionCropBorders) { + setTooltip(R.string.pref_crop_borders) + + setOnClickListener { + val isPagerType = ReadingModeType.isPagerType(readerPreferences.defaultReadingMode().get()) + if (isPagerType) { + readerPreferences.cropBorders().toggle() + } else { + readerPreferences.cropBordersWebtoon().toggle() + } + } + } + updateCropBordersShortcut() + listOf(readerPreferences.cropBorders(), readerPreferences.cropBordersWebtoon()) + .forEach { pref -> + pref.changes() + .onEach { updateCropBordersShortcut() } + .launchIn(lifecycleScope) + } + + // Rotation + with(binding.actionRotation) { + setTooltip(R.string.rotation_type) + + setOnClickListener { + popupMenu( + items = OrientationType.values().map { it.flagValue to it.stringRes }, + selectedItemId = readerPreferences.defaultOrientationType().get(), + ) { + val newOrientation = OrientationType.fromPreference(itemId) + + readerPreferences.defaultOrientationType().set(newOrientation.flagValue) + setGallery() + } + } + } + + // Settings sheet + with(binding.actionSettings) { + setTooltip(R.string.action_settings) + val readerSettingSheetDialog = ReaderSettingsSheet(this@ReaderActivity) + setOnClickListener { + if (!readerSettingSheetDialog.isShowing()) { + readerSettingSheetDialog.show() + } + } + + setOnLongClickListener { + ReaderSettingsSheet(this@ReaderActivity, showColorFilterSettings = true).show() + true + } + } + } + + private fun updateOrientationShortcut(preference: Int) { + val orientation = OrientationType.fromPreference(preference) + binding.actionRotation.setImageResource(orientation.iconRes) + } + + private fun updateCropBordersShortcut() { + val isPagerType = ReadingModeType.isPagerType(readerPreferences.defaultReadingMode().get()) + val enabled = if (isPagerType) { + readerPreferences.cropBorders().get() + } else { + readerPreferences.cropBordersWebtoon().get() + } + + binding.actionCropBorders.setImageResource( + if (enabled) { + R.drawable.ic_crop_24dp + } else { + R.drawable.ic_crop_off_24dp + }, + ) + } + + + /** + * Dispatches a key event. If the viewer doesn't handle it, call the default implementation. + */ + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + val handled = viewer?.handleKeyEvent(event) ?: false + return handled || super.dispatchKeyEvent(event) + } + + /** + * Dispatches a generic motion event. If the viewer doesn't handle it, call the default + * implementation. + */ + override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean { + val handled = viewer?.handleGenericMotionEvent(event) ?: false + return handled || super.dispatchGenericMotionEvent(event) + } + + private fun buildContainerTransform(entering: Boolean): MaterialContainerTransform { + return MaterialContainerTransform(this, entering).apply { + duration = 350 // ms + addTarget(android.R.id.content) + } + } + + /** + * Moves the viewer to the given page [index]. It does nothing if the viewer is null or the + * page is not found. + */ + fun moveToPageIndex(index: Int) { + val viewer = viewer ?: return + val page = mGalleryProvider?.mPages?.getOrNull(index) ?: return + viewer.moveToPage(page) + } + + /** + * Called from the viewer whenever a [page] is marked as active. It updates the values of the + * bottom menu and delegates the change to the presenter. + */ + @SuppressLint("SetTextI18n") + fun onPageSelected(page: ReaderPage) { + val pages = mGalleryProvider?.mPages ?: return + + // Set bottom page number + binding.pageNumber.text = "${page.number}/${pages.size}" + + // Set page numbers + if (viewer !is R2LPagerViewer) { + binding.leftPageText.text = "${page.number}" + binding.rightPageText.text = "${pages.size}" + } else { + binding.rightPageText.text = "${page.number}" + binding.leftPageText.text = "${pages.size}" + } + + // Set slider progress + binding.pageSlider.isEnabled = pages.size > 1 + binding.pageSlider.valueTo = max(pages.lastIndex.toFloat(), 1f) + binding.pageSlider.value = page.index.toFloat() + } + + /** + * Called from the viewer whenever a [page] is long clicked. A bottom sheet with a list of + * actions to perform is shown. + */ + fun onPageLongTap(page: ReaderPage) { + ReaderPageSheet(this, page).show() + } + + /** + * Called from the viewer to toggle the visibility of the menu. It's implemented on the + * viewer because each one implements its own touch and key events. + */ + fun toggleMenu() { + setMenuVisibility(!menuVisible) + } + + /** + * Called from the viewer to show the menu. + */ + fun showMenu() { + if (!menuVisible) { + setMenuVisibility(true) + } + } + + /** + * Called from the viewer to hide the menu. + */ + fun hideMenu() { + if (menuVisible) { + setMenuVisibility(false) + } + } + + /** + * Forces the user preferred [orientation] on the activity. + */ + private fun setOrientation(orientation: Int) { + val newOrientation = OrientationType.fromPreference(orientation) + if (newOrientation.flag != requestedOrientation) { + requestedOrientation = newOrientation.flag + } + updateOrientationShortcut(readerPreferences.defaultOrientationType().get()) + } + + /** + * Updates viewer inset depending on fullscreen reader preferences. + */ + fun updateViewerInset(fullscreen: Boolean) { + viewer?.getView()?.applyInsetter { + if (!fullscreen) { + type(navigationBars = true, statusBars = true) { + padding() + } + } + } + } + + /** + * Class that handles the user preferences of the reader. + */ + private inner class ReaderConfig { + + private fun getCombinedPaint(grayscale: Boolean, invertedColors: Boolean): Paint { + return Paint().apply { + colorFilter = ColorMatrixColorFilter( + ColorMatrix().apply { + if (grayscale) { + setSaturation(0f) + } + if (invertedColors) { + postConcat( + ColorMatrix( + floatArrayOf( + -1f, 0f, 0f, 0f, 255f, + 0f, -1f, 0f, 0f, 255f, + 0f, 0f, -1f, 0f, 255f, + 0f, 0f, 0f, 1f, 0f, + ), + ), + ) + } + }, + ) + } + } + + /** + * Initializes the reader subscriptions. + */ + init { + readerPreferences.readerTheme().changes() + .onEach { theme -> + binding.readerContainer.setBackgroundResource( + when (theme) { + 0 -> android.R.color.white + 2 -> R.color.reader_background_dark + 3 -> automaticBackgroundColor() + else -> android.R.color.black + }, + ) + } + .launchIn(lifecycleScope) + + readerPreferences.showPageNumber().changes() + .onEach { setPageNumberVisibility(it) } + .launchIn(lifecycleScope) + + readerPreferences.trueColor().changes() + .onEach { setTrueColor(it) } + .launchIn(lifecycleScope) + + readerPreferences.cutoutShort().changes() + .onEach { setCutoutShort(it) } + .launchIn(lifecycleScope) + + readerPreferences.keepScreenOn().changes() + .onEach { setKeepScreenOn(it) } + .launchIn(lifecycleScope) + + readerPreferences.customBrightness().changes() + .onEach { setCustomBrightness(it) } + .launchIn(lifecycleScope) + + readerPreferences.colorFilter().changes() + .onEach { setColorFilter(it) } + .launchIn(lifecycleScope) + + readerPreferences.colorFilterMode().changes() + .onEach { setColorFilter(readerPreferences.colorFilter().get()) } + .launchIn(lifecycleScope) + + merge( + readerPreferences.grayscale().changes(), + readerPreferences.invertedColors().changes() + ) + .onEach { + setLayerPaint( + readerPreferences.grayscale().get(), + readerPreferences.invertedColors().get() + ) + } + .launchIn(lifecycleScope) + + readerPreferences.fullscreen().changes() + .onEach { + WindowCompat.setDecorFitsSystemWindows(window, !it) + updateViewerInset(it) + } + .launchIn(lifecycleScope) + } + + /** + * Picks background color for [ReaderActivity] based on light/dark theme preference + */ + private fun automaticBackgroundColor(): Int { + return if (baseContext.isNightMode()) { + R.color.reader_background_dark + } else { + android.R.color.white + } + } + + /** + * Sets the visibility of the bottom page indicator according to [visible]. + */ + fun setPageNumberVisibility(visible: Boolean) { + binding.pageNumber.isVisible = visible + } + + /** + * Sets the 32-bit color mode according to [enabled]. + */ + private fun setTrueColor(enabled: Boolean) { + // TODO() + } + + private fun setCutoutShort(enabled: Boolean) { + window.attributes.layoutInDisplayCutoutMode = when (enabled) { + true -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + false -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER + } + + // Trigger relayout + setMenuVisibility(menuVisible) + } + + /** + * Sets the keep screen on mode according to [enabled]. + */ + private fun setKeepScreenOn(enabled: Boolean) { + if (enabled) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } + + /** + * Sets the custom brightness overlay according to [enabled]. + */ + @OptIn(FlowPreview::class) + private fun setCustomBrightness(enabled: Boolean) { + if (enabled) { + readerPreferences.customBrightnessValue().changes() + .sample(100) + .onEach { setCustomBrightnessValue(it) } + .launchIn(lifecycleScope) + } else { + setCustomBrightnessValue(0) + } + } + + /** + * Sets the color filter overlay according to [enabled]. + */ + @OptIn(FlowPreview::class) + private fun setColorFilter(enabled: Boolean) { + if (enabled) { + readerPreferences.colorFilterValue().changes() + .sample(100) + .onEach { setColorFilterValue(it) } + .launchIn(lifecycleScope) + } else { + binding.colorOverlay.isVisible = false + } + } + + /** + * Sets the brightness of the screen. Range is [-75, 100]. + * From -75 to -1 a semi-transparent black view is overlaid with the minimum brightness. + * From 1 to 100 it sets that value as brightness. + * 0 sets system brightness and hides the overlay. + */ + private fun setCustomBrightnessValue(value: Int) { + // Calculate and set reader brightness. + val readerBrightness = when { + value > 0 -> { + value / 100f + } + + value < 0 -> { + 0.01f + } + + else -> WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE + } + + window.attributes = window.attributes.apply { screenBrightness = readerBrightness } + + // Set black overlay visibility. + if (value < 0) { + binding.brightnessOverlay.isVisible = true + val alpha = (abs(value) * 2.56).toInt() + binding.brightnessOverlay.setBackgroundColor(Color.argb(alpha, 0, 0, 0)) + } else { + binding.brightnessOverlay.isVisible = false + } + } + + /** + * Sets the color filter [value]. + */ + private fun setColorFilterValue(value: Int) { + binding.colorOverlay.isVisible = true + binding.colorOverlay.setFilterColor(value, readerPreferences.colorFilterMode().get()) + } + + private fun setLayerPaint(grayscale: Boolean, invertedColors: Boolean) { + val paint = if (grayscale || invertedColors) getCombinedPaint( + grayscale, + invertedColors + ) else null + binding.viewerContainer.setLayerType(LAYER_TYPE_HARDWARE, paint) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderColorFilterView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderColorFilterView.kt new file mode 100644 index 000000000..6266ab239 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderColorFilterView.kt @@ -0,0 +1,36 @@ +package eu.kanade.tachiyomi.ui.reader + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.PorterDuff +import android.util.AttributeSet +import android.view.View +import androidx.core.graphics.toXfermode + +class ReaderColorFilterView( + context: Context, + attrs: AttributeSet? = null, +) : View(context, attrs) { + + private val colorFilterPaint: Paint = Paint() + + fun setFilterColor(color: Int, filterMode: Int) { + colorFilterPaint.color = color + colorFilterPaint.xfermode = when (filterMode) { + 1 -> PorterDuff.Mode.MULTIPLY + 2 -> PorterDuff.Mode.SCREEN + 3 -> PorterDuff.Mode.OVERLAY + 4 -> PorterDuff.Mode.LIGHTEN + 5 -> PorterDuff.Mode.DARKEN + else -> PorterDuff.Mode.SRC_OVER + }.toXfermode() + + invalidate() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + canvas.drawPaint(colorFilterPaint) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderNavigationOverlayView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderNavigationOverlayView.kt new file mode 100644 index 000000000..6e17affc7 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderNavigationOverlayView.kt @@ -0,0 +1,112 @@ +package eu.kanade.tachiyomi.ui.reader + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import android.view.ViewPropertyAnimator +import androidx.core.graphics.withScale +import androidx.core.graphics.withTranslation +import androidx.core.view.isVisible +import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation +import eu.kanade.tachiyomi.ui.reader.viewer.navigation.DisabledNavigation +import kotlin.math.abs + +class ReaderNavigationOverlayView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) { + + private var viewPropertyAnimator: ViewPropertyAnimator? = null + + private var navigation: ViewerNavigation? = null + + fun setNavigation(navigation: ViewerNavigation, showOnStart: Boolean) { + val firstLaunch = this.navigation == null + this.navigation = navigation + invalidate() + + if (isVisible || (!showOnStart && firstLaunch) || navigation is DisabledNavigation) { + return + } + + viewPropertyAnimator = animate() + .alpha(1f) + .setDuration(FADE_DURATION) + .withStartAction { + isVisible = true + } + .withEndAction { + viewPropertyAnimator = null + } + viewPropertyAnimator?.start() + } + + private val regionPaint = Paint() + + private val textPaint = Paint().apply { + textAlign = Paint.Align.CENTER + color = Color.WHITE + textSize = 64f + } + + private val textBorderPaint = Paint().apply { + textAlign = Paint.Align.CENTER + color = Color.BLACK + textSize = 64f + style = Paint.Style.STROKE + strokeWidth = 8f + } + + override fun onDraw(canvas: Canvas?) { + if (navigation == null) return + + navigation?.regions?.forEach { region -> + val rect = region.rectF + + // Scale rect from 1f,1f to screen width and height + canvas?.withScale(width.toFloat(), height.toFloat()) { + regionPaint.color = context.getColor(region.type.colorRes) + drawRect(rect, regionPaint) + } + + // Don't want scale anymore because it messes with drawText + // Translate origin to rect start (left, top) + canvas?.withTranslation(x = (width * rect.left), y = (height * rect.top)) { + // Calculate center of rect width on screen + val x = width * (abs(rect.left - rect.right) / 2) + + // Calculate center of rect height on screen + val y = height * (abs(rect.top - rect.bottom) / 2) + + drawText(context.getString(region.type.nameRes), x, y, textBorderPaint) + drawText(context.getString(region.type.nameRes), x, y, textPaint) + } + } + } + + override fun performClick(): Boolean { + super.performClick() + + if (viewPropertyAnimator == null && isVisible) { + viewPropertyAnimator = animate() + .alpha(0f) + .setDuration(FADE_DURATION) + .withEndAction { + isVisible = false + viewPropertyAnimator = null + } + viewPropertyAnimator?.start() + } + + return true + } + + override fun onTouchEvent(event: MotionEvent?): Boolean { + // Hide overlay if user start tapping or swiping + performClick() + return super.onTouchEvent(event) + } +} + +private const val FADE_DURATION = 1000L diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPageSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPageSheet.kt new file mode 100644 index 000000000..159153466 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPageSheet.kt @@ -0,0 +1,43 @@ +package eu.kanade.tachiyomi.ui.reader + +import android.view.LayoutInflater +import android.view.View +import com.hippo.ehviewer.databinding.ReaderPageSheetBinding +import eu.kanade.tachiyomi.ui.reader.model.ReaderPage +import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog + +/** + * Sheet to show when a page is long clicked. + */ +class ReaderPageSheet( + private val activity: ReaderActivity, + private val page: ReaderPage, +) : BaseBottomSheetDialog(activity) { + + private lateinit var binding: ReaderPageSheetBinding + + override fun createView(inflater: LayoutInflater): View { + binding = ReaderPageSheetBinding.inflate(activity.layoutInflater, null, false) + + binding.share.setOnClickListener { share() } + binding.save.setOnClickListener { save() } + + return binding.root + } + + /** + * Shares the image of this page with external apps. + */ + private fun share() { + activity.shareImage(page.index) + dismiss() + } + + /** + * Saves the image of this page on external storage. + */ + private fun save() { + activity.saveImageTo(page.index) + dismiss() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSlider.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSlider.kt new file mode 100644 index 000000000..94f857785 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSlider.kt @@ -0,0 +1,30 @@ +package eu.kanade.tachiyomi.ui.reader + +import android.content.Context +import android.util.AttributeSet +import com.google.android.material.slider.Slider + +/** + * Slider to show current chapter progress. + */ +class ReaderSlider @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, +) : Slider(context, attrs) { + + init { + stepSize = 1f + setLabelFormatter { value -> + (value.toInt() + 1).toString() + } + } + + /** + * Whether the slider should draw from right to left. + */ + var isRTL: Boolean + set(value) { + layoutDirection = if (value) LAYOUT_DIRECTION_RTL else LAYOUT_DIRECTION_LTR + } + get() = layoutDirection == LAYOUT_DIRECTION_RTL +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/PageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/PageLoader.kt new file mode 100644 index 000000000..db4b3b248 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/PageLoader.kt @@ -0,0 +1,74 @@ +package eu.kanade.tachiyomi.ui.reader.loader + +import com.hippo.image.Image +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.ui.reader.model.ReaderPage +import kotlinx.coroutines.flow.MutableStateFlow + +abstract class PageLoader { + val mPages by lazy { + check(size() > 0) + (0 until size()).map { ReaderPage(it) } + } + + var state = MutableStateFlow(STATE_WAIT) + + abstract val error: String + + abstract fun start() + + abstract fun stop() + + abstract fun size(): Int + + fun request(index: Int) { + onRequest(index) + } + + protected abstract fun onRequest(index: Int) + + protected abstract fun onForceRequest(index: Int) + + fun cancelRequest(index: Int) { + onCancelRequest(index) + } + + protected abstract fun onCancelRequest(index: Int) + + fun notifyDataChanged() { + if (size() == STATE_ERROR) + state.value = STATE_ERROR + else + state.compareAndSet(STATE_WAIT, STATE_READY) + } + + fun notifyDataChanged(index: Int) { + onRequest(index) + } + + fun notifyPageWait(index: Int) { + mPages[index].status.value = Page.State.QUEUE + } + + fun notifyPagePercent(index: Int, percent: Float) { + mPages[index].status.compareAndSet(Page.State.QUEUE, Page.State.DOWNLOAD_IMAGE) + mPages[index].progress.value = percent + } + + fun notifyPageSucceed(index: Int, image: Image) { + val priv = mPages[index].image + mPages[index].image = image + mPages[index].status.value = Page.State.READY + priv?.recycle() + } + + fun notifyPageFailed(index: Int) { + mPages[index].status.value = Page.State.ERROR + } + + companion object { + const val STATE_WAIT = -1 + const val STATE_ERROR = -2 + const val STATE_READY = -3 + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderPage.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderPage.kt new file mode 100644 index 000000000..3fba23c1c --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderPage.kt @@ -0,0 +1,8 @@ +package eu.kanade.tachiyomi.ui.reader.model + +import com.hippo.image.Image +import eu.kanade.tachiyomi.source.model.Page + +class ReaderPage(index: Int) : Page(index) { + var image: Image? = null +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/OrientationType.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/OrientationType.kt new file mode 100644 index 000000000..ff158cd54 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/OrientationType.kt @@ -0,0 +1,25 @@ +package eu.kanade.tachiyomi.ui.reader.setting + +import android.content.pm.ActivityInfo +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import com.hippo.ehviewer.R + +enum class OrientationType(val prefValue: Int, val flag: Int, @StringRes val stringRes: Int, @DrawableRes val iconRes: Int, val flagValue: Int) { + DEFAULT(0, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, R.string.label_default, R.drawable.ic_screen_rotation_24dp, 0x00000000), + FREE(1, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, R.string.rotation_free, R.drawable.ic_screen_rotation_24dp, 0x00000008), + PORTRAIT(2, ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT, R.string.rotation_portrait, R.drawable.ic_stay_current_portrait_24dp, 0x00000010), + LANDSCAPE(3, ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE, R.string.rotation_landscape, R.drawable.ic_stay_current_landscape_24dp, 0x00000018), + LOCKED_PORTRAIT(4, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT, R.string.rotation_force_portrait, R.drawable.ic_screen_lock_portrait_24dp, 0x00000020), + LOCKED_LANDSCAPE(5, ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, R.string.rotation_force_landscape, R.drawable.ic_screen_lock_landscape_24dp, 0x00000028), + REVERSE_PORTRAIT(6, ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT, R.string.rotation_reverse_portrait, R.drawable.ic_stay_current_portrait_24dp, 0x00000030), + ; + + companion object { + const val MASK = 0x00000038 + + fun fromPreference(preference: Int?): OrientationType = values().find { it.flagValue == preference } ?: FREE + + fun fromSpinner(position: Int?) = values().find { value -> value.prefValue == position } ?: DEFAULT + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderColorFilterSettings.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderColorFilterSettings.kt new file mode 100644 index 000000000..3e42c6dab --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderColorFilterSettings.kt @@ -0,0 +1,201 @@ +package eu.kanade.tachiyomi.ui.reader.setting + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.annotation.ColorInt +import androidx.core.graphics.alpha +import androidx.core.graphics.blue +import androidx.core.graphics.green +import androidx.core.graphics.red +import androidx.core.widget.NestedScrollView +import androidx.lifecycle.lifecycleScope +import eu.kanade.tachiyomi.core.preference.getAndSet +import com.hippo.ehviewer.databinding.ReaderColorFilterSettingsBinding +import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import eu.kanade.tachiyomi.util.preference.bindToPreference +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.sample + +/** + * Color filter sheet to toggle custom filter and brightness overlay. + */ +class ReaderColorFilterSettings @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + NestedScrollView(context, attrs) { + + private val readerPreferences: ReaderPreferences = ReaderActivity.readerPreferences + + private val binding = ReaderColorFilterSettingsBinding.inflate(LayoutInflater.from(context), this, false) + + init { + addView(binding.root) + + readerPreferences.colorFilter().changes() + .onEach { setColorFilter(it) } + .launchIn((context as ReaderActivity).lifecycleScope) + + readerPreferences.colorFilterMode().changes() + .onEach { setColorFilter(readerPreferences.colorFilter().get()) } + .launchIn(context.lifecycleScope) + + readerPreferences.customBrightness().changes() + .onEach { setCustomBrightness(it) } + .launchIn(context.lifecycleScope) + + // Get color and update values + val color = readerPreferences.colorFilterValue().get() + val brightness = readerPreferences.customBrightnessValue().get() + + val argb = setValues(color) + + // Set brightness value + binding.txtBrightnessSeekbarValue.text = brightness.toString() + binding.sliderBrightness.value = brightness.toFloat() + + // Initialize seekBar progress + binding.sliderColorFilterAlpha.value = argb[0].toFloat() + binding.sliderColorFilterRed.value = argb[1].toFloat() + binding.sliderColorFilterGreen.value = argb[2].toFloat() + binding.sliderColorFilterBlue.value = argb[3].toFloat() + + // Set listeners + binding.switchColorFilter.bindToPreference(readerPreferences.colorFilter()) + binding.customBrightness.bindToPreference(readerPreferences.customBrightness()) + binding.colorFilterMode.bindToPreference(readerPreferences.colorFilterMode()) + binding.grayscale.bindToPreference(readerPreferences.grayscale()) + binding.invertedColors.bindToPreference(readerPreferences.invertedColors()) + + binding.sliderColorFilterAlpha.addOnChangeListener { _, value, fromUser -> + if (fromUser) { + setColorValue(value.toInt(), ALPHA_MASK, 24) + } + } + binding.sliderColorFilterRed.addOnChangeListener { _, value, fromUser -> + if (fromUser) { + setColorValue(value.toInt(), RED_MASK, 16) + } + } + binding.sliderColorFilterGreen.addOnChangeListener { _, value, fromUser -> + if (fromUser) { + setColorValue(value.toInt(), GREEN_MASK, 8) + } + } + binding.sliderColorFilterBlue.addOnChangeListener { _, value, fromUser -> + if (fromUser) { + setColorValue(value.toInt(), BLUE_MASK, 0) + } + } + + binding.sliderBrightness.addOnChangeListener { _, value, fromUser -> + if (fromUser) { + readerPreferences.customBrightnessValue().set(value.toInt()) + } + } + } + + /** + * Set enabled status of seekBars belonging to color filter + * @param enabled determines if seekBar gets enabled + */ + private fun setColorFilterSeekBar(enabled: Boolean) { + binding.sliderColorFilterRed.isEnabled = enabled + binding.sliderColorFilterGreen.isEnabled = enabled + binding.sliderColorFilterBlue.isEnabled = enabled + binding.sliderColorFilterAlpha.isEnabled = enabled + } + + /** + * Set enabled status of seekBars belonging to custom brightness + * @param enabled value which determines if seekBar gets enabled + */ + private fun setCustomBrightnessSeekBar(enabled: Boolean) { + binding.sliderBrightness.isEnabled = enabled + } + + /** + * Set the text value's of color filter + * @param color integer containing color information + */ + private fun setValues(color: Int): Array { + val alpha = color.alpha + val red = color.red + val green = color.green + val blue = color.blue + + // Initialize values + binding.txtColorFilterAlphaValue.text = "$alpha" + binding.txtColorFilterRedValue.text = "$red" + binding.txtColorFilterGreenValue.text = "$green" + binding.txtColorFilterBlueValue.text = "$blue" + + return arrayOf(alpha, red, green, blue) + } + + /** + * Manages the custom brightness value subscription + * @param enabled determines if the subscription get (un)subscribed + */ + private fun setCustomBrightness(enabled: Boolean) { + if (enabled) { + readerPreferences.customBrightnessValue().changes() + .sample(100) + .onEach { setCustomBrightnessValue(it) } + .launchIn((context as ReaderActivity).lifecycleScope) + } else { + setCustomBrightnessValue(0, true) + } + setCustomBrightnessSeekBar(enabled) + } + + /** + * Sets the brightness of the screen. Range is [-75, 100]. + * From -75 to -1 a semi-transparent black view is shown at the top with the minimum brightness. + * From 1 to 100 it sets that value as brightness. + * 0 sets system brightness and hides the overlay. + */ + private fun setCustomBrightnessValue(value: Int, isDisabled: Boolean = false) { + if (!isDisabled) { + binding.txtBrightnessSeekbarValue.text = value.toString() + } + } + + /** + * Manages the color filter value subscription + * @param enabled determines if the subscription get (un)subscribed + */ + private fun setColorFilter(enabled: Boolean) { + if (enabled) { + readerPreferences.colorFilterValue().changes() + .sample(100) + .onEach { setColorFilterValue(it) } + .launchIn((context as ReaderActivity).lifecycleScope) + } + setColorFilterSeekBar(enabled) + } + + /** + * Sets the color filter overlay of the screen. Determined by HEX of integer + * @param color hex of color. + */ + private fun setColorFilterValue(@ColorInt color: Int) { + setValues(color) + } + + /** + * Updates the color value in preference + * @param color value of color range [0,255] + * @param mask contains hex mask of chosen color + * @param bitShift amounts of bits that gets shifted to receive value + */ + private fun setColorValue(color: Int, mask: Long, bitShift: Int) { + readerPreferences.colorFilterValue().getAndSet { currentColor -> + (color shl bitShift) or (currentColor and mask.inv().toInt()) + } + } +} + +private const val ALPHA_MASK: Long = 0xFF000000 +private const val RED_MASK: Long = 0x00FF0000 +private const val GREEN_MASK: Long = 0x0000FF00 +private const val BLUE_MASK: Long = 0x000000FF diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderGeneralSettings.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderGeneralSettings.kt new file mode 100644 index 000000000..de67892cd --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderGeneralSettings.kt @@ -0,0 +1,52 @@ +package eu.kanade.tachiyomi.ui.reader.setting + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.core.view.isVisible +import androidx.core.widget.NestedScrollView +import androidx.lifecycle.lifecycleScope +import com.hippo.ehviewer.R +import com.hippo.ehviewer.databinding.ReaderGeneralSettingsBinding +import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import eu.kanade.tachiyomi.util.preference.asHotFlow +import eu.kanade.tachiyomi.util.preference.bindToPreference +import kotlinx.coroutines.flow.launchIn + +/** + * Sheet to show reader and viewer preferences. + */ +class ReaderGeneralSettings @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + NestedScrollView(context, attrs) { + + private val readerPreferences: ReaderPreferences = ReaderActivity.readerPreferences + + private val binding = ReaderGeneralSettingsBinding.inflate(LayoutInflater.from(context), this, false) + + init { + addView(binding.root) + + initGeneralPreferences() + } + + /** + * Init general reader preferences. + */ + private fun initGeneralPreferences() { + binding.backgroundColor.bindToIntPreference(readerPreferences.readerTheme(), R.array.reader_themes_values) + binding.showPageNumber.bindToPreference(readerPreferences.showPageNumber()) + binding.fullscreen.bindToPreference(readerPreferences.fullscreen()) + readerPreferences.fullscreen() + .asHotFlow { + // If the preference is explicitly disabled, that means the setting was configured since there is a cutout + binding.cutoutShort.isVisible = it && ((context as ReaderActivity).hasCutout || !readerPreferences.cutoutShort().get()) + binding.cutoutShort.bindToPreference(readerPreferences.cutoutShort()) + } + .launchIn((context as ReaderActivity).lifecycleScope) + + binding.keepscreen.bindToPreference(readerPreferences.keepScreenOn()) + binding.longTap.bindToPreference(readerPreferences.readWithLongTap()) + binding.alwaysShowChapterTransition.bindToPreference(readerPreferences.alwaysShowChapterTransition()) + binding.pageTransitions.bindToPreference(readerPreferences.pageTransitions()) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt new file mode 100644 index 000000000..03f695bdf --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt @@ -0,0 +1,110 @@ +package eu.kanade.tachiyomi.ui.reader.setting + +import eu.kanade.tachiyomi.core.preference.PreferenceStore +import eu.kanade.tachiyomi.core.preference.getEnum +import eu.kanade.tachiyomi.data.preference.PreferenceValues + +class ReaderPreferences(private val preferenceStore: PreferenceStore) { + + // region General + + fun pageTransitions() = preferenceStore.getBoolean("pref_enable_transitions_key", true) + + fun doubleTapAnimSpeed() = preferenceStore.getInt("pref_double_tap_anim_speed", 500) + + fun showPageNumber() = preferenceStore.getBoolean("pref_show_page_number_key", true) + + fun showReadingMode() = preferenceStore.getBoolean("pref_show_reading_mode", true) + + fun trueColor() = preferenceStore.getBoolean("pref_true_color_key", false) + + fun fullscreen() = preferenceStore.getBoolean("fullscreen", true) + + fun cutoutShort() = preferenceStore.getBoolean("cutout_short", true) + + fun keepScreenOn() = preferenceStore.getBoolean("pref_keep_screen_on_key", true) + + fun defaultReadingMode() = preferenceStore.getInt("pref_default_reading_mode_key", ReadingModeType.LEFT_TO_RIGHT.flagValue) + + fun defaultOrientationType() = preferenceStore.getInt("pref_default_orientation_type_key", OrientationType.FREE.flagValue) + + fun imageScaleType() = preferenceStore.getInt("pref_image_scale_type_key", 1) + + fun zoomStart() = preferenceStore.getInt("pref_zoom_start_key", 1) + + fun readerTheme() = preferenceStore.getInt("pref_reader_theme_key", 1) + + fun alwaysShowChapterTransition() = preferenceStore.getBoolean("always_show_chapter_transition", true) + + fun cropBorders() = preferenceStore.getBoolean("crop_borders", false) + + fun navigateToPan() = preferenceStore.getBoolean("navigate_pan", true) + + fun landscapeZoom() = preferenceStore.getBoolean("landscape_zoom", true) + + fun cropBordersWebtoon() = preferenceStore.getBoolean("crop_borders_webtoon", false) + + fun webtoonSidePadding() = preferenceStore.getInt("webtoon_side_padding", 0) + + fun readerHideThreshold() = preferenceStore.getEnum("reader_hide_threshold", PreferenceValues.ReaderHideThreshold.LOW) + + fun folderPerManga() = preferenceStore.getBoolean("create_folder_per_manga", false) + + fun skipRead() = preferenceStore.getBoolean("skip_read", false) + + fun skipFiltered() = preferenceStore.getBoolean("skip_filtered", true) + + // endregion + + // region Split two page spread + + fun dualPageSplitPaged() = preferenceStore.getBoolean("pref_dual_page_split", false) + + fun dualPageInvertPaged() = preferenceStore.getBoolean("pref_dual_page_invert", false) + + fun dualPageSplitWebtoon() = preferenceStore.getBoolean("pref_dual_page_split_webtoon", false) + + fun dualPageInvertWebtoon() = preferenceStore.getBoolean("pref_dual_page_invert_webtoon", false) + + // endregion + + // region Color filter + + fun customBrightness() = preferenceStore.getBoolean("pref_custom_brightness_key", false) + + fun customBrightnessValue() = preferenceStore.getInt("custom_brightness_value", 0) + + fun colorFilter() = preferenceStore.getBoolean("pref_color_filter_key", false) + + fun colorFilterValue() = preferenceStore.getInt("color_filter_value", 0) + + fun colorFilterMode() = preferenceStore.getInt("color_filter_mode", 0) + + fun grayscale() = preferenceStore.getBoolean("pref_grayscale", false) + + fun invertedColors() = preferenceStore.getBoolean("pref_inverted_colors", false) + + // endregion + + // region Controls + + fun readWithLongTap() = preferenceStore.getBoolean("reader_long_tap", true) + + fun readWithVolumeKeys() = preferenceStore.getBoolean("reader_volume_keys", false) + + fun readWithVolumeKeysInverted() = preferenceStore.getBoolean("reader_volume_keys_inverted", false) + + fun navigationModePager() = preferenceStore.getInt("reader_navigation_mode_pager", 0) + + fun navigationModeWebtoon() = preferenceStore.getInt("reader_navigation_mode_webtoon", 0) + + fun pagerNavInverted() = preferenceStore.getEnum("reader_tapping_inverted", PreferenceValues.TappingInvertMode.NONE) + + fun webtoonNavInverted() = preferenceStore.getEnum("reader_tapping_inverted_webtoon", PreferenceValues.TappingInvertMode.NONE) + + fun showNavigationOverlayNewUser() = preferenceStore.getBoolean("reader_navigation_overlay_new_user", true) + + fun showNavigationOverlayOnStart() = preferenceStore.getBoolean("reader_navigation_overlay_on_start", false) + + // endregion +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderReadingModeSettings.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderReadingModeSettings.kt new file mode 100644 index 000000000..e6ff0db08 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderReadingModeSettings.kt @@ -0,0 +1,125 @@ +package eu.kanade.tachiyomi.ui.reader.setting + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.core.view.isVisible +import androidx.core.widget.NestedScrollView +import androidx.lifecycle.lifecycleScope +import com.hippo.ehviewer.R +import eu.kanade.tachiyomi.data.preference.PreferenceValues +import com.hippo.ehviewer.databinding.ReaderReadingModeSettingsBinding +import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerViewer +import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer +import eu.kanade.tachiyomi.util.preference.asHotFlow +import eu.kanade.tachiyomi.util.preference.bindToPreference +import kotlinx.coroutines.flow.launchIn + +/** + * Sheet to show reader and viewer preferences. + */ +class ReaderReadingModeSettings @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + NestedScrollView(context, attrs) { + + private val readerPreferences: ReaderPreferences = ReaderActivity.readerPreferences + + private val binding = ReaderReadingModeSettingsBinding.inflate(LayoutInflater.from(context), this, false) + + init { + addView(binding.root) + + initGeneralPreferences() + + when ((context as ReaderActivity).viewer) { + is PagerViewer -> initPagerPreferences() + is WebtoonViewer -> initWebtoonPreferences() + } + } + + /** + * Init general reader preferences. + */ + private fun initGeneralPreferences() { + binding.viewer.onItemSelectedListener = { position -> + val readingModeType = ReadingModeType.fromSpinner(position) + ReaderActivity.readerPreferences.defaultReadingMode().set(readingModeType.flagValue) + (context as ReaderActivity).setGallery() + + val mangaViewer = ReaderActivity.readerPreferences.defaultReadingMode().get() + if (mangaViewer == ReadingModeType.WEBTOON.flagValue || mangaViewer == ReadingModeType.CONTINUOUS_VERTICAL.flagValue) { + initWebtoonPreferences() + } else { + initPagerPreferences() + } + } + binding.viewer.setSelection(ReaderActivity.readerPreferences.defaultReadingMode().get().let { ReadingModeType.fromPreference(it).prefValue }) + + binding.rotationMode.onItemSelectedListener = { position -> + val rotationType = OrientationType.fromSpinner(position) + readerPreferences.defaultOrientationType().set(rotationType.flagValue) + (context as ReaderActivity).setGallery() + } + binding.rotationMode.setSelection(ReaderActivity.readerPreferences.defaultOrientationType().get().let { OrientationType.fromPreference(it).prefValue }) + } + + /** + * Init the preferences for the pager reader. + */ + private fun initPagerPreferences() { + binding.webtoonPrefsGroup.root.isVisible = false + binding.pagerPrefsGroup.root.isVisible = true + + binding.pagerPrefsGroup.tappingInverted.bindToPreference(readerPreferences.pagerNavInverted(), PreferenceValues.TappingInvertMode::class.java) + binding.pagerPrefsGroup.navigatePan.bindToPreference(readerPreferences.navigateToPan()) + + binding.pagerPrefsGroup.pagerNav.bindToPreference(readerPreferences.navigationModePager()) + readerPreferences.navigationModePager() + .asHotFlow { + val isTappingEnabled = it != 5 + binding.pagerPrefsGroup.tappingInverted.isVisible = isTappingEnabled + binding.pagerPrefsGroup.navigatePan.isVisible = isTappingEnabled + } + .launchIn((context as ReaderActivity).lifecycleScope) + // Makes so that landscape zoom gets hidden away when image scale type is not fit screen + binding.pagerPrefsGroup.scaleType.bindToPreference(readerPreferences.imageScaleType(), 1) + readerPreferences.imageScaleType() + .asHotFlow { binding.pagerPrefsGroup.landscapeZoom.isVisible = it == 1 } + .launchIn((context as ReaderActivity).lifecycleScope) + binding.pagerPrefsGroup.landscapeZoom.bindToPreference(readerPreferences.landscapeZoom()) + + binding.pagerPrefsGroup.zoomStart.bindToPreference(readerPreferences.zoomStart(), 1) + binding.pagerPrefsGroup.cropBorders.bindToPreference(readerPreferences.cropBorders()) + + binding.pagerPrefsGroup.dualPageSplit.bindToPreference(readerPreferences.dualPageSplitPaged()) + // Makes it so that dual page invert gets hidden away when dual page split is turned off + readerPreferences.dualPageSplitPaged() + .asHotFlow { binding.pagerPrefsGroup.dualPageInvert.isVisible = it } + .launchIn((context as ReaderActivity).lifecycleScope) + binding.pagerPrefsGroup.dualPageInvert.bindToPreference(readerPreferences.dualPageInvertPaged()) + } + + /** + * Init the preferences for the webtoon reader. + */ + private fun initWebtoonPreferences() { + binding.pagerPrefsGroup.root.isVisible = false + binding.webtoonPrefsGroup.root.isVisible = true + + binding.webtoonPrefsGroup.tappingInverted.bindToPreference(readerPreferences.webtoonNavInverted(), PreferenceValues.TappingInvertMode::class.java) + + binding.webtoonPrefsGroup.webtoonNav.bindToPreference(readerPreferences.navigationModeWebtoon()) + readerPreferences.navigationModeWebtoon() + .asHotFlow { binding.webtoonPrefsGroup.tappingInverted.isVisible = it != 5 } + .launchIn((context as ReaderActivity).lifecycleScope) + binding.webtoonPrefsGroup.cropBordersWebtoon.bindToPreference(readerPreferences.cropBordersWebtoon()) + binding.webtoonPrefsGroup.webtoonSidePadding.bindToIntPreference(readerPreferences.webtoonSidePadding(), R.array.webtoon_side_padding_values) + + binding.webtoonPrefsGroup.dualPageSplit.bindToPreference(readerPreferences.dualPageSplitWebtoon()) + // Makes it so that dual page invert gets hidden away when dual page split is turned off + readerPreferences.dualPageSplitWebtoon() + .asHotFlow { binding.webtoonPrefsGroup.dualPageInvert.isVisible = it } + .launchIn((context as ReaderActivity).lifecycleScope) + binding.webtoonPrefsGroup.dualPageInvert.bindToPreference(readerPreferences.dualPageInvertWebtoon()) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsSheet.kt new file mode 100644 index 000000000..89f1cebf2 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsSheet.kt @@ -0,0 +1,92 @@ +package eu.kanade.tachiyomi.ui.reader.setting + +import android.animation.ValueAnimator +import android.os.Bundle +import com.google.android.material.tabs.TabLayout +import com.hippo.ehviewer.R +import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import eu.kanade.tachiyomi.widget.listener.SimpleTabSelectedListener +import eu.kanade.tachiyomi.widget.sheet.TabbedBottomSheetDialog + +class ReaderSettingsSheet( + private val activity: ReaderActivity, + private val showColorFilterSettings: Boolean = false, +) : TabbedBottomSheetDialog(activity) { + + private lateinit var readingModeSettings: ReaderReadingModeSettings + private lateinit var generalSettings: ReaderGeneralSettings + private lateinit var colorFilterSettings: ReaderColorFilterSettings + + init { + initSettings() + } + + private fun initSettings() { + readingModeSettings = ReaderReadingModeSettings(activity) + generalSettings = ReaderGeneralSettings(activity) + colorFilterSettings = ReaderColorFilterSettings(activity) + } + + private val backgroundDimAnimator by lazy { + val sheetBackgroundDim = window?.attributes?.dimAmount ?: 0.25f + ValueAnimator.ofFloat(sheetBackgroundDim, 0f).also { valueAnimator -> + valueAnimator.duration = 250 + valueAnimator.addUpdateListener { + window?.setDimAmount(it.animatedValue as Float) + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + behavior.isFitToContents = false + behavior.halfExpandedRatio = 0.25f + + val filterTabIndex = getTabViews().indexOf(colorFilterSettings) + binding.tabs.addOnTabSelectedListener( + object : SimpleTabSelectedListener() { + override fun onTabSelected(tab: TabLayout.Tab?) { + val isFilterTab = tab?.position == filterTabIndex + + // Remove dimmed backdrop so color filter changes can be previewed + backgroundDimAnimator.run { + if (isFilterTab) { + if (animatedFraction < 1f) { + start() + } + } else if (animatedFraction > 0f) { + reverse() + } + } + + // Hide toolbars + if (activity.menuVisible != !isFilterTab) { + activity.setMenuVisibility(!isFilterTab) + } + } + }, + ) + + if (showColorFilterSettings) { + binding.tabs.getTabAt(filterTabIndex)?.select() + } + } + + override fun getTabViews() = listOf( + readingModeSettings, + generalSettings, + colorFilterSettings, + ) + + override fun getTabTitles() = listOf( + R.string.pref_category_reading_mode, + R.string.pref_category_general, + R.string.custom_filter, + ) + + override fun show() { + initSettings() + super.show() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReadingModeType.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReadingModeType.kt new file mode 100644 index 000000000..b7c166115 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReadingModeType.kt @@ -0,0 +1,45 @@ +package eu.kanade.tachiyomi.ui.reader.setting + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import com.hippo.ehviewer.R +import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer +import eu.kanade.tachiyomi.ui.reader.viewer.pager.L2RPagerViewer +import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer +import eu.kanade.tachiyomi.ui.reader.viewer.pager.VerticalPagerViewer +import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer + +enum class ReadingModeType(val prefValue: Int, @StringRes val stringRes: Int, @DrawableRes val iconRes: Int, val flagValue: Int) { + DEFAULT(0, R.string.label_default, R.drawable.ic_reader_default_24dp, 0x00000000), + LEFT_TO_RIGHT(1, R.string.left_to_right_viewer, R.drawable.ic_reader_ltr_24dp, 0x00000001), + RIGHT_TO_LEFT(2, R.string.right_to_left_viewer, R.drawable.ic_reader_rtl_24dp, 0x00000002), + VERTICAL(3, R.string.vertical_viewer, R.drawable.ic_reader_vertical_24dp, 0x00000003), + WEBTOON(4, R.string.webtoon_viewer, R.drawable.ic_reader_webtoon_24dp, 0x00000004), + CONTINUOUS_VERTICAL(5, R.string.vertical_plus_viewer, R.drawable.ic_reader_continuous_vertical_24dp, 0x00000005), + ; + + companion object { + const val MASK = 0x00000007 + + fun fromPreference(preference: Int?): ReadingModeType = values().find { it.flagValue == preference } ?: DEFAULT + + fun isPagerType(preference: Int): Boolean { + val mode = fromPreference(preference) + return mode == LEFT_TO_RIGHT || mode == RIGHT_TO_LEFT || mode == VERTICAL + } + + fun fromSpinner(position: Int?) = values().find { value -> value.prefValue == position } ?: DEFAULT + + fun toViewer(preference: Int?, activity: ReaderActivity): BaseViewer { + return when (fromPreference(preference)) { + LEFT_TO_RIGHT -> L2RPagerViewer(activity) + RIGHT_TO_LEFT -> R2LPagerViewer(activity) + VERTICAL -> VerticalPagerViewer(activity) + WEBTOON -> WebtoonViewer(activity) + CONTINUOUS_VERTICAL -> WebtoonViewer(activity, isContinuous = false) + DEFAULT -> L2RPagerViewer(activity) + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/BaseViewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/BaseViewer.kt new file mode 100644 index 000000000..ec40e3266 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/BaseViewer.kt @@ -0,0 +1,45 @@ +package eu.kanade.tachiyomi.ui.reader.viewer + +import android.view.KeyEvent +import android.view.MotionEvent +import android.view.View +import eu.kanade.tachiyomi.ui.reader.loader.PageLoader +import eu.kanade.tachiyomi.ui.reader.model.ReaderPage + +/** + * Interface for implementing a viewer. + */ +interface BaseViewer { + + /** + * Returns the view this viewer uses. + */ + fun getView(): View + + /** + * Destroys this viewer. Called when leaving the reader or swapping viewers. + */ + fun destroy() {} + + /** + * Tells this viewer to set the given [provider] as active. + */ + fun setGalleryProvider(provider: PageLoader) + + /** + * Tells this viewer to move to the given [page]. + */ + fun moveToPage(page: ReaderPage) + + /** + * Called from the containing activity when a key [event] is received. It should return true + * if the event was handled, false otherwise. + */ + fun handleKeyEvent(event: KeyEvent): Boolean + + /** + * Called from the containing activity when a generic motion [event] is received. It should + * return true if the event was handled, false otherwise. + */ + fun handleGenericMotionEvent(event: MotionEvent): Boolean +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/GestureDetectorWithLongTap.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/GestureDetectorWithLongTap.kt new file mode 100644 index 000000000..a85f8dc7d --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/GestureDetectorWithLongTap.kt @@ -0,0 +1,75 @@ +package eu.kanade.tachiyomi.ui.reader.viewer + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.ViewConfiguration +import kotlin.math.abs + +/** + * A custom gesture detector that also implements an on long tap confirmed, because the built-in + * one conflicts with the quick scale feature. + */ +open class GestureDetectorWithLongTap( + context: Context, + listener: Listener, +) : GestureDetector(context, listener) { + + private val handler = Handler(Looper.getMainLooper()) + private val slop = ViewConfiguration.get(context).scaledTouchSlop + private val longTapTime = ViewConfiguration.getLongPressTimeout().toLong() + private val doubleTapTime = ViewConfiguration.getDoubleTapTimeout().toLong() + + private var downX = 0f + private var downY = 0f + private var lastUp = 0L + private var lastDownEvent: MotionEvent? = null + + /** + * Runnable to execute when a long tap is confirmed. + */ + private val longTapFn = Runnable { listener.onLongTapConfirmed(lastDownEvent!!) } + + override fun onTouchEvent(ev: MotionEvent): Boolean { + when (ev.actionMasked) { + MotionEvent.ACTION_DOWN -> { + lastDownEvent?.recycle() + lastDownEvent = MotionEvent.obtain(ev) + + // This is the key difference with the built-in detector. We have to ignore the + // event if the last up and current down are too close in time (double tap). + if (ev.downTime - lastUp > doubleTapTime) { + downX = ev.rawX + downY = ev.rawY + handler.postDelayed(longTapFn, longTapTime) + } + } + MotionEvent.ACTION_MOVE -> { + if (abs(ev.rawX - downX) > slop || abs(ev.rawY - downY) > slop) { + handler.removeCallbacks(longTapFn) + } + } + MotionEvent.ACTION_UP -> { + lastUp = ev.eventTime + handler.removeCallbacks(longTapFn) + } + MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_POINTER_DOWN -> { + handler.removeCallbacks(longTapFn) + } + } + return super.onTouchEvent(ev) + } + + /** + * Custom listener to also include a long tap confirmed + */ + open class Listener : SimpleOnGestureListener() { + /** + * Notified when a long tap occurs with the initial on down [ev] that triggered it. + */ + open fun onLongTapConfirmed(ev: MotionEvent) { + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderButton.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderButton.kt new file mode 100644 index 000000000..7b56f7323 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderButton.kt @@ -0,0 +1,27 @@ +package eu.kanade.tachiyomi.ui.reader.viewer + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import com.google.android.material.button.MaterialButton +import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerViewer + +/** + * A button class to be used by child views of the pager viewer. All tap gestures are handled by + * the pager, but this class disables that behavior to allow clickable buttons. + */ +class ReaderButton @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, +) : MaterialButton(context, attrs) { + + var viewer: PagerViewer? = null + + override fun onTouchEvent(event: MotionEvent): Boolean { + viewer?.pager?.setGestureDetectorEnabled(false) + if (event.actionMasked == MotionEvent.ACTION_UP) { + viewer?.pager?.setGestureDetectorEnabled(true) + } + return super.onTouchEvent(event) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt new file mode 100644 index 000000000..1227ec901 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt @@ -0,0 +1,381 @@ +package eu.kanade.tachiyomi.ui.reader.viewer + +import android.content.Context +import android.graphics.PointF +import android.graphics.RectF +import android.graphics.drawable.Animatable +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.widget.FrameLayout +import androidx.annotation.AttrRes +import androidx.annotation.CallSuper +import androidx.annotation.StyleRes +import androidx.appcompat.widget.AppCompatImageView +import androidx.core.os.postDelayed +import androidx.core.view.isVisible +import coil.dispose +import coil.imageLoader +import coil.request.CachePolicy +import coil.request.ImageRequest +import com.davemorrissey.labs.subscaleview.ImageSource +import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView +import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.Companion.EASE_OUT_QUAD +import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.Companion.SCALE_TYPE_CENTER_INSIDE +import com.github.chrisbanes.photoview.PhotoView +import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonSubsamplingImageView +import eu.kanade.tachiyomi.util.system.GLUtil +import eu.kanade.tachiyomi.util.system.animatorDurationScale +import eu.kanade.tachiyomi.util.view.isVisibleOnScreen +import java.io.InputStream +import java.nio.ByteBuffer + +/** + * A wrapper view for showing page image. + * + * Animated image will be drawn by [PhotoView] while [SubsamplingScaleImageView] will take non-animated image. + * + * @param isWebtoon if true, [WebtoonSubsamplingImageView] will be used instead of [SubsamplingScaleImageView] + * and [AppCompatImageView] will be used instead of [PhotoView] + */ +open class ReaderPageImageView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + @AttrRes defStyleAttrs: Int = 0, + @StyleRes defStyleRes: Int = 0, + private val isWebtoon: Boolean = false, +) : FrameLayout(context, attrs, defStyleAttrs, defStyleRes) { + + private var pageView: View? = null + + private var config: Config? = null + + var onImageLoaded: (() -> Unit)? = null + var onImageLoadError: (() -> Unit)? = null + var onScaleChanged: ((newScale: Float) -> Unit)? = null + var onViewClicked: (() -> Unit)? = null + + /** + * For automatic background. Will be set as background color when [onImageLoaded] is called. + */ + var pageBackground: Drawable? = null + + @CallSuper + open fun onImageLoaded() { + onImageLoaded?.invoke() + background = pageBackground + } + + @CallSuper + open fun onImageLoadError() { + onImageLoadError?.invoke() + } + + @CallSuper + open fun onScaleChanged(newScale: Float) { + onScaleChanged?.invoke(newScale) + } + + @CallSuper + open fun onViewClicked() { + onViewClicked?.invoke() + } + + open fun onPageSelected(forward: Boolean) { + with(pageView as? SubsamplingScaleImageView) { + if (this == null) return + if (isReady) { + landscapeZoom(forward) + } else { + setOnImageEventListener( + object : SubsamplingScaleImageView.DefaultOnImageEventListener() { + override fun onReady() { + setupZoom(config) + landscapeZoom(forward) + this@ReaderPageImageView.onImageLoaded() + } + + override fun onImageLoadError(e: Exception) { + onImageLoadError() + } + }, + ) + } + } + } + + private fun SubsamplingScaleImageView.landscapeZoom(forward: Boolean) { + if (config != null && config!!.landscapeZoom && config!!.minimumScaleType == SCALE_TYPE_CENTER_INSIDE && sWidth > sHeight && scale == getMinScale()) { + handler?.postDelayed(500) { + val point = when (config!!.zoomStartPosition) { + ZoomStartPosition.LEFT -> if (forward) PointF(0F, 0F) else PointF(sWidth.toFloat(), 0F) + ZoomStartPosition.RIGHT -> if (forward) PointF(sWidth.toFloat(), 0F) else PointF(0F, 0F) + ZoomStartPosition.CENTER -> center.also { it?.y = 0F } + } + + val targetScale = height.toFloat() / sHeight.toFloat() + animateScaleAndCenter(targetScale, point)!! + .withDuration(500) + .withEasing(SubsamplingScaleImageView.EASE_IN_OUT_QUAD) + .withInterruptible(true) + .start() + } + } + } + + fun setImage(drawable: Drawable, config: Config) { + this.config = config + if (drawable is Animatable) { + prepareAnimatedImageView() + setAnimatedImage(drawable, config) + } else { + prepareNonAnimatedImageView() + setNonAnimatedImage(drawable, config) + } + } + + fun setImage(inputStream: InputStream, isAnimated: Boolean, config: Config) { + this.config = config + if (isAnimated) { + prepareAnimatedImageView() + setAnimatedImage(inputStream, config) + } else { + prepareNonAnimatedImageView() + setNonAnimatedImage(inputStream, config) + } + } + + fun recycle() = pageView?.let { + when (it) { + is SubsamplingScaleImageView -> it.recycle() + is AppCompatImageView -> it.dispose() + } + it.isVisible = false + } + + /** + * Check if the image can be panned to the left + */ + fun canPanLeft(): Boolean = canPan { it.left } + + /** + * Check if the image can be panned to the right + */ + fun canPanRight(): Boolean = canPan { it.right } + + /** + * Check whether the image can be panned. + * @param fn a function that returns the direction to check for + */ + private fun canPan(fn: (RectF) -> Float): Boolean { + (pageView as? SubsamplingScaleImageView)?.let { view -> + RectF().let { + view.getPanRemaining(it) + return fn(it) > 0 + } + } + return false + } + + /** + * Pans the image to the left by a screen's width worth. + */ + fun panLeft() { + pan { center, view -> center.also { it.x -= view.width / view.scale } } + } + + /** + * Pans the image to the right by a screen's width worth. + */ + fun panRight() { + pan { center, view -> center.also { it.x += view.width / view.scale } } + } + + /** + * Pans the image. + * @param fn a function that computes the new center of the image + */ + private fun pan(fn: (PointF, SubsamplingScaleImageView) -> PointF) { + (pageView as? SubsamplingScaleImageView)?.let { view -> + + val target = fn(view.center ?: return, view) + view.animateCenter(target)!! + .withEasing(EASE_OUT_QUAD) + .withDuration(250) + .withInterruptible(true) + .start() + } + } + + private fun prepareNonAnimatedImageView() { + if (pageView is SubsamplingScaleImageView) return + removeView(pageView) + + pageView = if (isWebtoon) { + WebtoonSubsamplingImageView(context) + } else { + SubsamplingScaleImageView(context) + }.apply { + setMaxTileSize(GLUtil.maxTextureSize) + setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER) + setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE) + setMinimumTileDpi(180) + setOnStateChangedListener( + object : SubsamplingScaleImageView.OnStateChangedListener { + override fun onScaleChanged(newScale: Float, origin: Int) { + this@ReaderPageImageView.onScaleChanged(newScale) + } + + override fun onCenterChanged(newCenter: PointF?, origin: Int) { + // Not used + } + }, + ) + setOnClickListener { this@ReaderPageImageView.onViewClicked() } + } + addView(pageView, MATCH_PARENT, MATCH_PARENT) + } + + private fun SubsamplingScaleImageView.setupZoom(config: Config?) { + // 5x zoom + maxScale = scale * MAX_ZOOM_SCALE + setDoubleTapZoomScale(scale * 2) + + when (config?.zoomStartPosition) { + ZoomStartPosition.LEFT -> setScaleAndCenter(scale, PointF(0F, 0F)) + ZoomStartPosition.RIGHT -> setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0F)) + ZoomStartPosition.CENTER -> setScaleAndCenter(scale, center.also { it?.y = 0F }) + null -> {} + } + } + + private fun setNonAnimatedImage( + image: Any, + config: Config, + ) = (pageView as? SubsamplingScaleImageView)?.apply { + setDoubleTapZoomDuration(config.zoomDuration.getSystemScaledDuration()) + setMinimumScaleType(config.minimumScaleType) + setMinimumDpi(1) // Just so that very small image will be fit for initial load + setCropBorders(config.cropBorders) + setOnImageEventListener( + object : SubsamplingScaleImageView.DefaultOnImageEventListener() { + override fun onReady() { + setupZoom(config) + if (isVisibleOnScreen()) landscapeZoom(true) + this@ReaderPageImageView.onImageLoaded() + } + + override fun onImageLoadError(e: Exception) { + this@ReaderPageImageView.onImageLoadError() + } + }, + ) + + when (image) { + is Drawable -> { + val bitmap = (image as BitmapDrawable).bitmap + setImage(ImageSource.bitmap(bitmap)) + } + is InputStream -> setImage(ImageSource.inputStream(image)) + else -> throw IllegalArgumentException("Not implemented for class ${image::class.simpleName}") + } + isVisible = true + } + + private fun prepareAnimatedImageView() { + if (pageView is AppCompatImageView) return + removeView(pageView) + + pageView = if (isWebtoon) { + AppCompatImageView(context) + } else { + PhotoView(context) + }.apply { + adjustViewBounds = true + + if (this is PhotoView) { + setScaleLevels(1F, 2F, MAX_ZOOM_SCALE) + // Force 2 scale levels on double tap + setOnDoubleTapListener( + object : GestureDetector.SimpleOnGestureListener() { + override fun onDoubleTap(e: MotionEvent): Boolean { + if (scale > 1F) { + setScale(1F, e.x, e.y, true) + } else { + setScale(2F, e.x, e.y, true) + } + return true + } + + override fun onSingleTapConfirmed(e: MotionEvent): Boolean { + this@ReaderPageImageView.onViewClicked() + return super.onSingleTapConfirmed(e) + } + }, + ) + setOnScaleChangeListener { _, _, _ -> + this@ReaderPageImageView.onScaleChanged(scale) + } + } + } + addView(pageView, MATCH_PARENT, MATCH_PARENT) + } + + private fun setAnimatedImage( + image: Any, + config: Config, + ) = (pageView as? AppCompatImageView)?.apply { + if (this is PhotoView) { + setZoomTransitionDuration(config.zoomDuration.getSystemScaledDuration()) + } + + val data = when (image) { + is Drawable -> image + is InputStream -> ByteBuffer.wrap(image.readBytes()) + else -> throw IllegalArgumentException("Not implemented for class ${image::class.simpleName}") + } + val request = ImageRequest.Builder(context) + .data(data) + .memoryCachePolicy(CachePolicy.DISABLED) + .diskCachePolicy(CachePolicy.DISABLED) + .target( + onSuccess = { result -> + setImageDrawable(result) + (result as? Animatable)?.start() + isVisible = true + this@ReaderPageImageView.onImageLoaded() + }, + onError = { + this@ReaderPageImageView.onImageLoadError() + }, + ) + .crossfade(false) + .build() + context.imageLoader.enqueue(request) + } + + private fun Int.getSystemScaledDuration(): Int { + return (this * context.animatorDurationScale).toInt().coerceAtLeast(1) + } + + /** + * All of the config except [zoomDuration] will only be used for non-animated image. + */ + data class Config( + val zoomDuration: Int, + val minimumScaleType: Int = SCALE_TYPE_CENTER_INSIDE, + val cropBorders: Boolean = false, + val zoomStartPosition: ZoomStartPosition = ZoomStartPosition.CENTER, + val landscapeZoom: Boolean = false, + ) + + enum class ZoomStartPosition { + LEFT, CENTER, RIGHT + } +} + +private const val MAX_ZOOM_SCALE = 5F diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressIndicator.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressIndicator.kt new file mode 100644 index 000000000..e565ee878 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressIndicator.kt @@ -0,0 +1,95 @@ +package eu.kanade.tachiyomi.ui.reader.viewer + +import android.content.Context +import android.util.AttributeSet +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.view.animation.Animation +import android.view.animation.LinearInterpolator +import android.view.animation.RotateAnimation +import android.widget.FrameLayout +import androidx.annotation.IntRange +import com.google.android.material.progressindicator.CircularProgressIndicator + +/** + * A wrapper for [CircularProgressIndicator] that always rotates while being determinate. + * + * By always rotating we give the feedback to the user that the application isn't 'stuck', + * and by making it determinate the user also approximately knows how much the operation will take. + */ +class ReaderProgressIndicator @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : FrameLayout(context, attrs, defStyleAttr) { + + private val indicator: CircularProgressIndicator + + private val rotateAnimation by lazy { + RotateAnimation( + 0F, + 360F, + Animation.RELATIVE_TO_SELF, + 0.5F, + Animation.RELATIVE_TO_SELF, + 0.5F, + ).apply { + interpolator = LinearInterpolator() + repeatCount = Animation.INFINITE + duration = 4000 + } + } + + init { + layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT) + indicator = CircularProgressIndicator(context) + indicator.max = 100 + indicator.isIndeterminate = true + addView(indicator) + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + updateRotateAnimation() + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + updateRotateAnimation() + } + + fun show() { + indicator.show() + updateRotateAnimation() + } + + fun hide() { + indicator.hide() + updateRotateAnimation() + } + + /** + * Sets the current indicator progress to the specified value. + * + * @param progress Indicator will be set indeterminate if this value is 0 + */ + fun setProgress(@IntRange(from = 0, to = 100) progress: Int, animated: Boolean = true) { + if (progress > 0) { + indicator.setProgressCompat(progress, animated) + } else if (!indicator.isIndeterminate) { + indicator.hide() + indicator.isIndeterminate = true + indicator.show() + } + updateRotateAnimation() + } + + private fun updateRotateAnimation() { + if (isAttachedToWindow && indicator.isShown && !indicator.isIndeterminate) { + if (animation == null) { + startAnimation(rotateAnimation) + } + } else { + clearAnimation() + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ViewerConfig.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ViewerConfig.kt new file mode 100644 index 000000000..cc2bd8297 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ViewerConfig.kt @@ -0,0 +1,89 @@ +package eu.kanade.tachiyomi.ui.reader.viewer + +import eu.kanade.tachiyomi.core.preference.Preference +import eu.kanade.tachiyomi.data.preference.PreferenceValues.TappingInvertMode +import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +/** + * Common configuration for all viewers. + */ +abstract class ViewerConfig(readerPreferences: ReaderPreferences, private val scope: CoroutineScope) { + + var imagePropertyChangedListener: (() -> Unit)? = null + + var navigationModeChangedListener: (() -> Unit)? = null + + var tappingInverted = TappingInvertMode.NONE + var longTapEnabled = true + var usePageTransitions = false + var doubleTapAnimDuration = 500 + var volumeKeysEnabled = false + var volumeKeysInverted = false + var trueColor = false + var alwaysShowChapterTransition = true + var navigationMode = 0 + protected set + + var forceNavigationOverlay = false + + var navigationOverlayOnStart = false + + var dualPageSplit = false + protected set + + var dualPageInvert = false + protected set + + abstract var navigator: ViewerNavigation + protected set + + init { + readerPreferences.readWithLongTap() + .register({ longTapEnabled = it }) + + readerPreferences.pageTransitions() + .register({ usePageTransitions = it }) + + readerPreferences.doubleTapAnimSpeed() + .register({ doubleTapAnimDuration = it }) + + readerPreferences.readWithVolumeKeys() + .register({ volumeKeysEnabled = it }) + + readerPreferences.readWithVolumeKeysInverted() + .register({ volumeKeysInverted = it }) + + readerPreferences.trueColor() + .register({ trueColor = it }, { imagePropertyChangedListener?.invoke() }) + + readerPreferences.alwaysShowChapterTransition() + .register({ alwaysShowChapterTransition = it }) + + forceNavigationOverlay = readerPreferences.showNavigationOverlayNewUser().get() + if (forceNavigationOverlay) { + readerPreferences.showNavigationOverlayNewUser().set(false) + } + + readerPreferences.showNavigationOverlayOnStart() + .register({ navigationOverlayOnStart = it }) + } + + protected abstract fun defaultNavigation(): ViewerNavigation + + abstract fun updateNavigation(navigationMode: Int) + + fun Preference.register( + valueAssignment: (T) -> Unit, + onChanged: (T) -> Unit = {}, + ) { + changes() + .onEach { valueAssignment(it) } + .distinctUntilChanged() + .onEach { onChanged(it) } + .launchIn(scope) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ViewerNavigation.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ViewerNavigation.kt new file mode 100644 index 000000000..ba5159f42 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ViewerNavigation.kt @@ -0,0 +1,49 @@ +package eu.kanade.tachiyomi.ui.reader.viewer + +import android.graphics.PointF +import android.graphics.RectF +import androidx.annotation.StringRes +import com.hippo.ehviewer.R +import eu.kanade.tachiyomi.data.preference.PreferenceValues +import eu.kanade.tachiyomi.util.lang.invert + +abstract class ViewerNavigation { + + sealed class NavigationRegion(@StringRes val nameRes: Int, val colorRes: Int) { + object MENU : NavigationRegion(R.string.action_menu, R.color.navigation_menu) + object PREV : NavigationRegion(R.string.nav_zone_prev, R.color.navigation_prev) + object NEXT : NavigationRegion(R.string.nav_zone_next, R.color.navigation_next) + object LEFT : NavigationRegion(R.string.nav_zone_left, R.color.navigation_left) + object RIGHT : NavigationRegion(R.string.nav_zone_right, R.color.navigation_right) + } + + data class Region( + val rectF: RectF, + val type: NavigationRegion, + ) { + fun invert(invertMode: PreferenceValues.TappingInvertMode): Region { + if (invertMode == PreferenceValues.TappingInvertMode.NONE) return this + return this.copy( + rectF = this.rectF.invert(invertMode), + ) + } + } + + private var constantMenuRegion: RectF = RectF(0f, 0f, 1f, 0.05f) + + abstract var regions: List + + var invertMode: PreferenceValues.TappingInvertMode = PreferenceValues.TappingInvertMode.NONE + + fun getAction(pos: PointF): NavigationRegion { + val x = pos.x + val y = pos.y + val region = regions.map { it.invert(invertMode) } + .find { it.rectF.contains(x, y) } + return when { + region != null -> region.type + constantMenuRegion.contains(x, y) -> NavigationRegion.MENU + else -> NavigationRegion.MENU + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/DisabledNavigation.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/DisabledNavigation.kt new file mode 100644 index 000000000..11a715faf --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/DisabledNavigation.kt @@ -0,0 +1,18 @@ +package eu.kanade.tachiyomi.ui.reader.viewer.navigation + +import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation + +/** + * Visualization of default state without any inversion + * +---+---+---+ + * | M | M | M | P: Previous + * +---+---+---+ + * | M | M | M | M: Menu + * +---+---+---+ + * | M | M | M | N: Next + * +---+---+---+ +*/ +class DisabledNavigation : ViewerNavigation() { + + override var regions: List = emptyList() +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/EdgeNavigation.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/EdgeNavigation.kt new file mode 100644 index 000000000..7440b1e35 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/EdgeNavigation.kt @@ -0,0 +1,32 @@ +package eu.kanade.tachiyomi.ui.reader.viewer.navigation + +import android.graphics.RectF +import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation + +/** + * Visualization of default state without any inversion + * +---+---+---+ + * | N | N | N | P: Previous + * +---+---+---+ + * | N | M | N | M: Menu + * +---+---+---+ + * | N | P | N | N: Next + * +---+---+---+ +*/ +class EdgeNavigation : ViewerNavigation() { + + override var regions: List = listOf( + Region( + rectF = RectF(0f, 0f, 0.33f, 1f), + type = NavigationRegion.NEXT, + ), + Region( + rectF = RectF(0.33f, 0.66f, 0.66f, 1f), + type = NavigationRegion.PREV, + ), + Region( + rectF = RectF(0.66f, 0f, 1f, 1f), + type = NavigationRegion.NEXT, + ), + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/KindlishNavigation.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/KindlishNavigation.kt new file mode 100644 index 000000000..21bf053eb --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/KindlishNavigation.kt @@ -0,0 +1,28 @@ +package eu.kanade.tachiyomi.ui.reader.viewer.navigation + +import android.graphics.RectF +import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation + +/** + * Visualization of default state without any inversion + * +---+---+---+ + * | M | M | M | P: Previous + * +---+---+---+ + * | P | N | N | M: Menu + * +---+---+---+ + * | P | N | N | N: Next + * +---+---+---+ +*/ +class KindlishNavigation : ViewerNavigation() { + + override var regions: List = listOf( + Region( + rectF = RectF(0.33f, 0.33f, 1f, 1f), + type = NavigationRegion.NEXT, + ), + Region( + rectF = RectF(0f, 0.33f, 0.33f, 1f), + type = NavigationRegion.PREV, + ), + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/LNavigation.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/LNavigation.kt new file mode 100644 index 000000000..8f94a6100 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/LNavigation.kt @@ -0,0 +1,36 @@ +package eu.kanade.tachiyomi.ui.reader.viewer.navigation + +import android.graphics.RectF +import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation + +/** + * Visualization of default state without any inversion + * +---+---+---+ + * | P | P | P | P: Previous + * +---+---+---+ + * | P | M | N | M: Menu + * +---+---+---+ + * | N | N | N | N: Next + * +---+---+---+ + */ +open class LNavigation : ViewerNavigation() { + + override var regions: List = listOf( + Region( + rectF = RectF(0f, 0.33f, 0.33f, 0.66f), + type = NavigationRegion.PREV, + ), + Region( + rectF = RectF(0f, 0f, 1f, 0.33f), + type = NavigationRegion.PREV, + ), + Region( + rectF = RectF(0.66f, 0.33f, 1f, 0.66f), + type = NavigationRegion.NEXT, + ), + Region( + rectF = RectF(0f, 0.66f, 1f, 1f), + type = NavigationRegion.NEXT, + ), + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/RightAndLeftNavigation.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/RightAndLeftNavigation.kt new file mode 100644 index 000000000..b660000f2 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/RightAndLeftNavigation.kt @@ -0,0 +1,28 @@ +package eu.kanade.tachiyomi.ui.reader.viewer.navigation + +import android.graphics.RectF +import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation + +/** + * Visualization of default state without any inversion + * +---+---+---+ + * | N | M | P | P: Move Right + * +---+---+---+ + * | N | M | P | M: Menu + * +---+---+---+ + * | N | M | P | N: Move Left + * +---+---+---+ + */ +class RightAndLeftNavigation : ViewerNavigation() { + + override var regions: List = listOf( + Region( + rectF = RectF(0f, 0f, 0.33f, 1f), + type = NavigationRegion.LEFT, + ), + Region( + rectF = RectF(0.66f, 0f, 1f, 1f), + type = NavigationRegion.RIGHT, + ), + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/Pager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/Pager.kt new file mode 100644 index 000000000..87c1e1678 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/Pager.kt @@ -0,0 +1,111 @@ +package eu.kanade.tachiyomi.ui.reader.viewer.pager + +import android.content.Context +import android.view.HapticFeedbackConstants +import android.view.KeyEvent +import android.view.MotionEvent +import androidx.viewpager.widget.DirectionalViewPager +import eu.kanade.tachiyomi.ui.reader.viewer.GestureDetectorWithLongTap + +/** + * Pager implementation that listens for tap and long tap and allows temporarily disabling touch + * events in order to work with child views that need to disable touch events on this parent. The + * pager can also be declared to be vertical by creating it with [isHorizontal] to false. + */ +open class Pager( + context: Context, + isHorizontal: Boolean = true, +) : DirectionalViewPager(context, isHorizontal) { + + /** + * Tap listener function to execute when a tap is detected. + */ + var tapListener: ((MotionEvent) -> Unit)? = null + + /** + * Long tap listener function to execute when a long tap is detected. + */ + var longTapListener: ((MotionEvent) -> Boolean)? = null + + /** + * Gesture listener that implements tap and long tap events. + */ + private val gestureListener = object : GestureDetectorWithLongTap.Listener() { + override fun onSingleTapConfirmed(ev: MotionEvent): Boolean { + tapListener?.invoke(ev) + return true + } + + override fun onLongTapConfirmed(ev: MotionEvent) { + val listener = longTapListener + if (listener != null && listener.invoke(ev)) { + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + } + } + } + + /** + * Gesture detector which handles motion events. + */ + private val gestureDetector = GestureDetectorWithLongTap(context, gestureListener) + + /** + * Whether the gesture detector is currently enabled. + */ + private var isGestureDetectorEnabled = true + + /** + * Dispatches a touch event. + */ + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { + val handled = super.dispatchTouchEvent(ev) + if (isGestureDetectorEnabled) { + gestureDetector.onTouchEvent(ev) + } + return handled + } + + /** + * Whether the given [ev] should be intercepted. Only used to prevent crashes when child + * views manipulate [requestDisallowInterceptTouchEvent]. + */ + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { + return try { + super.onInterceptTouchEvent(ev) + } catch (e: IllegalArgumentException) { + false + } + } + + /** + * Handles a touch event. Only used to prevent crashes when child views manipulate + * [requestDisallowInterceptTouchEvent]. + */ + override fun onTouchEvent(ev: MotionEvent): Boolean { + return try { + super.onTouchEvent(ev) + } catch (e: NullPointerException) { + false + } catch (e: IndexOutOfBoundsException) { + false + } catch (e: IllegalArgumentException) { + false + } + } + + /** + * Executes the given key event when this pager has focus. Just do nothing because the reader + * already dispatches key events to the viewer and has more control than this method. + */ + override fun executeKeyEvent(event: KeyEvent): Boolean { + // Disable viewpager's default key event handling + return false + } + + /** + * Enables or disables the gesture detector. + */ + fun setGestureDetectorEnabled(enabled: Boolean) { + isGestureDetectorEnabled = enabled + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt new file mode 100644 index 000000000..a995ed202 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt @@ -0,0 +1,139 @@ +package eu.kanade.tachiyomi.ui.reader.viewer.pager + +import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences +import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView +import eu.kanade.tachiyomi.ui.reader.viewer.ViewerConfig +import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation +import eu.kanade.tachiyomi.ui.reader.viewer.navigation.DisabledNavigation +import eu.kanade.tachiyomi.ui.reader.viewer.navigation.EdgeNavigation +import eu.kanade.tachiyomi.ui.reader.viewer.navigation.KindlishNavigation +import eu.kanade.tachiyomi.ui.reader.viewer.navigation.LNavigation +import eu.kanade.tachiyomi.ui.reader.viewer.navigation.RightAndLeftNavigation +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +/** + * Configuration used by pager viewers. + */ +class PagerConfig( + private val viewer: PagerViewer, + scope: CoroutineScope, + readerPreferences: ReaderPreferences = ReaderActivity.readerPreferences, +) : ViewerConfig(readerPreferences, scope) { + + var theme = readerPreferences.readerTheme().get() + private set + + var automaticBackground = false + private set + + var dualPageSplitChangedListener: ((Boolean) -> Unit)? = null + + var imageScaleType = 1 + private set + + var imageZoomType = ReaderPageImageView.ZoomStartPosition.LEFT + private set + + var imageCropBorders = false + private set + + var navigateToPan = false + private set + + var landscapeZoom = false + private set + + init { + readerPreferences.readerTheme() + .register( + { + theme = it + automaticBackground = it == 3 + }, + { imagePropertyChangedListener?.invoke() }, + ) + + readerPreferences.imageScaleType() + .register({ imageScaleType = it }, { imagePropertyChangedListener?.invoke() }) + + readerPreferences.zoomStart() + .register({ zoomTypeFromPreference(it) }, { imagePropertyChangedListener?.invoke() }) + + readerPreferences.cropBorders() + .register({ imageCropBorders = it }, { imagePropertyChangedListener?.invoke() }) + + readerPreferences.navigateToPan() + .register({ navigateToPan = it }) + + readerPreferences.landscapeZoom() + .register({ landscapeZoom = it }, { imagePropertyChangedListener?.invoke() }) + + readerPreferences.navigationModePager() + .register({ navigationMode = it }, { updateNavigation(navigationMode) }) + + readerPreferences.pagerNavInverted() + .register({ tappingInverted = it }, { navigator.invertMode = it }) + readerPreferences.pagerNavInverted().changes() + .drop(1) + .onEach { navigationModeChangedListener?.invoke() } + .launchIn(scope) + + readerPreferences.dualPageSplitPaged() + .register( + { dualPageSplit = it }, + { + imagePropertyChangedListener?.invoke() + dualPageSplitChangedListener?.invoke(it) + }, + ) + + readerPreferences.dualPageInvertPaged() + .register({ dualPageInvert = it }, { imagePropertyChangedListener?.invoke() }) + } + + private fun zoomTypeFromPreference(value: Int) { + imageZoomType = when (value) { + // Auto + 1 -> when (viewer) { + is L2RPagerViewer -> ReaderPageImageView.ZoomStartPosition.LEFT + is R2LPagerViewer -> ReaderPageImageView.ZoomStartPosition.RIGHT + else -> ReaderPageImageView.ZoomStartPosition.CENTER + } + // Left + 2 -> ReaderPageImageView.ZoomStartPosition.LEFT + // Right + 3 -> ReaderPageImageView.ZoomStartPosition.RIGHT + // Center + else -> ReaderPageImageView.ZoomStartPosition.CENTER + } + } + + override var navigator: ViewerNavigation = defaultNavigation() + set(value) { + field = value.also { it.invertMode = this.tappingInverted } + } + + override fun defaultNavigation(): ViewerNavigation { + return when (viewer) { + is VerticalPagerViewer -> LNavigation() + else -> RightAndLeftNavigation() + } + } + + override fun updateNavigation(navigationMode: Int) { + navigator = when (navigationMode) { + 0 -> defaultNavigation() + 1 -> LNavigation() + 2 -> KindlishNavigation() + 3 -> EdgeNavigation() + 4 -> RightAndLeftNavigation() + 5 -> DisabledNavigation() + else -> defaultNavigation() + } + navigationModeChangedListener?.invoke() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt new file mode 100644 index 000000000..275561e7a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt @@ -0,0 +1,202 @@ +package eu.kanade.tachiyomi.ui.reader.viewer.pager + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.drawable.Drawable +import android.view.Gravity +import android.view.LayoutInflater +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams +import com.hippo.ehviewer.databinding.ReaderErrorBinding +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.ui.reader.model.ReaderPage +import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView +import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator +import eu.kanade.tachiyomi.widget.ViewPagerAdapter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +/** + * View of the ViewPager that contains a page of a chapter. + */ +@SuppressLint("ViewConstructor") +class PagerPageHolder( + readerThemedContext: Context, + val viewer: PagerViewer, + val page: ReaderPage, +) : ReaderPageImageView(readerThemedContext), ViewPagerAdapter.PositionableView { + + /** + * Item that identifies this view. Needed by the adapter to not recreate views. + */ + override val item + get() = page + + /** + * Loading progress bar to indicate the current progress. + */ + private val progressIndicator: ReaderProgressIndicator = + ReaderProgressIndicator(readerThemedContext).apply { + updateLayoutParams { + gravity = Gravity.CENTER + } + } + + /** + * Error layout to show when the image fails to load. + */ + private var errorLayout: ReaderErrorBinding? = null + + private var scope = CoroutineScope(Dispatchers.IO) + + /** + * Subscription for status changes of the page. + */ + private var statusJob: Job? = null + + /** + * Subscription for progress changes of the page. + */ + private var progressJob: Job? = null + + init { + addView(progressIndicator) + progressJob = scope.launch(Dispatchers.Main) { + page.progress.collect { + progressIndicator.setProgress(it.toInt()) + } + } + statusJob = scope.launch(Dispatchers.Main) { + page.status.collectLatest { + processStatus(it) + } + } + } + + /** + * Called when this view is detached from the window. Unsubscribes any active subscription. + */ + @SuppressLint("ClickableViewAccessibility") + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + unsubscribeProgress() + unsubscribeStatus() + } + + /** + * Called when the status of the page changes. + * + * @param status the new status of the page. + */ + private fun processStatus(status: Page.State) { + when (status) { + Page.State.QUEUE -> setQueued() + Page.State.LOAD_PAGE -> setLoading() + Page.State.DOWNLOAD_IMAGE -> { + setDownloading() + } + + Page.State.READY -> { + page.image?.mObtainedDrawable?.let { setImage(it) } + unsubscribeProgress() + } + + Page.State.ERROR -> { + setError() + unsubscribeProgress() + } + } + } + + /** + * Unsubscribes from the status subscription. + */ + private fun unsubscribeStatus() { + statusJob?.cancel() + } + + /** + * Unsubscribes from the progress subscription. + */ + private fun unsubscribeProgress() { + progressJob?.cancel() + } + + /** + * Called when the page is queued. + */ + private fun setQueued() { + progressIndicator.show() + errorLayout?.root?.isVisible = false + } + + /** + * Called when the page is loading. + */ + private fun setLoading() { + progressIndicator.show() + errorLayout?.root?.isVisible = false + } + + /** + * Called when the page is downloading. + */ + private fun setDownloading() { + progressIndicator.show() + errorLayout?.root?.isVisible = false + } + + /** + * Called when the page is ready. + */ + private fun setImage(drawable: Drawable) { + progressIndicator.setProgress(0) + errorLayout?.root?.isVisible = false + setImage(drawable, Config(10)) + } + + /** + * Called when the page has an error. + */ + private fun setError() { + progressIndicator.hide() + showErrorLayout() + } + + override fun onImageLoaded() { + super.onImageLoaded() + progressIndicator.hide() + } + + /** + * Called when an image fails to decode. + */ + override fun onImageLoadError() { + super.onImageLoadError() + progressIndicator.hide() + showErrorLayout() + } + + /** + * Called when an image is zoomed in/out. + */ + override fun onScaleChanged(newScale: Float) { + super.onScaleChanged(newScale) + viewer.activity.hideMenu() + } + + private fun showErrorLayout(): ReaderErrorBinding { + if (errorLayout == null) { + errorLayout = ReaderErrorBinding.inflate(LayoutInflater.from(context), this, true) + errorLayout?.actionRetry?.viewer = viewer + errorLayout?.actionRetry?.setOnClickListener { + TODO() + } + } + errorLayout?.root?.isVisible = true + return errorLayout!! + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt new file mode 100644 index 000000000..d32b6e437 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt @@ -0,0 +1,362 @@ +package eu.kanade.tachiyomi.ui.reader.viewer.pager + +import android.graphics.PointF +import android.view.InputDevice +import android.view.KeyEvent +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup.LayoutParams +import androidx.core.view.children +import androidx.core.view.isGone +import androidx.core.view.isVisible +import androidx.viewpager.widget.ViewPager +import com.hippo.ehviewer.R +import eu.kanade.tachiyomi.ui.reader.loader.PageLoader +import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import eu.kanade.tachiyomi.ui.reader.model.ReaderPage +import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer +import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion +import eu.kanade.tachiyomi.util.system.logcat +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel + +/** + * Implementation of a [BaseViewer] to display pages with a [ViewPager]. + */ +@Suppress("LeakingThis") +abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer { + + private val scope = MainScope() + + /** + * View pager used by this viewer. It's abstract to implement L2R, R2L and vertical pagers on + * top of this class. + */ + val pager = createPager() + + /** + * Configuration used by the pager, like allow taps, scale mode on images, page transitions... + */ + val config = PagerConfig(this, scope) + + /** + * Adapter of the pager. + */ + private val adapter = PagerViewerAdapter(this) + + /** + * Currently active item. It can be a chapter page or a chapter transition. + */ + private var currentPage: Any? = null + + /** + * Viewer chapters to set when the pager enters idle mode. Otherwise, if the view was settling + * or dragging, there'd be a noticeable and annoying jump. + */ + private var awaitingIdleViewerChapters: PageLoader? = null + + /** + * Whether the view pager is currently in idle mode. It sets the awaiting chapters if setting + * this field to true. + */ + private var isIdle = true + set(value) { + field = value + if (value) { + awaitingIdleViewerChapters?.let { viewerChapters -> + setChaptersInternal(viewerChapters) + awaitingIdleViewerChapters = null + } + } + } + + init { + pager.isVisible = false // Don't layout the pager yet + pager.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + pager.isFocusable = false + pager.offscreenPageLimit = 1 + pager.id = R.id.reader_pager + pager.adapter = adapter + pager.addOnPageChangeListener( + object : ViewPager.SimpleOnPageChangeListener() { + override fun onPageSelected(position: Int) { + if (activity.isScrollingThroughPages.not()) { + activity.hideMenu() + } + + onPageChange(position) + } + + override fun onPageScrollStateChanged(state: Int) { + isIdle = state == ViewPager.SCROLL_STATE_IDLE + } + }, + ) + pager.tapListener = { event -> + val pos = PointF(event.rawX / pager.width, event.rawY / pager.height) + val navigator = config.navigator + + when (navigator.getAction(pos)) { + NavigationRegion.MENU -> activity.toggleMenu() + NavigationRegion.NEXT -> moveToNext() + NavigationRegion.PREV -> moveToPrevious() + NavigationRegion.RIGHT -> moveRight() + NavigationRegion.LEFT -> moveLeft() + } + } + pager.longTapListener = f@{ + if (activity.menuVisible || config.longTapEnabled) { + val item = adapter.items.getOrNull(pager.currentItem) + if (item is ReaderPage) { + activity.onPageLongTap(item) + return@f true + } + } + false + } + + config.imagePropertyChangedListener = { + refreshAdapter() + } + + config.navigationModeChangedListener = { + val showOnStart = config.navigationOverlayOnStart || config.forceNavigationOverlay + activity.binding.navigationOverlay.setNavigation(config.navigator, showOnStart) + } + } + + override fun destroy() { + super.destroy() + scope.cancel() + } + + /** + * Creates a new ViewPager. + */ + abstract fun createPager(): Pager + + /** + * Returns the view this viewer uses. + */ + override fun getView(): View { + return pager + } + + /** + * Returns the PagerPageHolder for the provided page + */ + private fun getPageHolder(page: ReaderPage): PagerPageHolder? = + pager.children + .filterIsInstance(PagerPageHolder::class.java) + .firstOrNull { it.item == page } + + /** + * Called when a new page (either a [ReaderPage] or [ChapterTransition]) is marked as active + */ + private fun onPageChange(position: Int) { + val page = adapter.items.getOrNull(position) + if (page != null && currentPage != page) { + val forward = when { + currentPage is ReaderPage && page is ReaderPage -> { + // if both pages have the same number, it's a split page with an InsertPage + if (page.number == (currentPage as ReaderPage).number) { + // the InsertPage is always the second in the reading direction + false + } else { + page.number > (currentPage as ReaderPage).number + } + } + else -> true + } + currentPage = page + when (page) { + is ReaderPage -> onReaderPageSelected(page, forward) + } + } + } + + /** + * Called when a [ReaderPage] is marked as active. It notifies the + * activity of the change and requests the preload of the next chapter if this is the last page. + */ + private fun onReaderPageSelected(page: ReaderPage, forward: Boolean) { + activity.onPageSelected(page) + + // Notify holder of page change + getPageHolder(page)?.onPageSelected(forward) + } + + /** + * Tells this viewer to set the given [provider] as active. If the pager is currently idle, + * it sets the chapters immediately, otherwise they are saved and set when it becomes idle. + */ + override fun setGalleryProvider(provider: PageLoader) { + if (isIdle) { + setChaptersInternal(provider) + } else { + awaitingIdleViewerChapters = provider + } + } + + /** + * Sets the active [chapters] on this pager. + */ + private fun setChaptersInternal(chapters: PageLoader) { + logcat { "setChaptersInternal" } + adapter.setChapters(chapters) + + // Layout the pager once a chapter is being set + if (pager.isGone) { + logcat { "Pager first layout" } + pager.isVisible = true + } + } + + /** + * Tells this viewer to move to the given [page]. + */ + override fun moveToPage(page: ReaderPage) { + logcat { "moveToPage ${page.number}" } + val position = adapter.items.indexOf(page) + if (position != -1) { + val currentPosition = pager.currentItem + pager.setCurrentItem(position, true) + // manually call onPageChange since ViewPager listener is not triggered in this case + if (currentPosition == position) { + onPageChange(position) + } + } else { + logcat { "Page $page not found in adapter" } + } + } + + /** + * Moves to the next page. + */ + open fun moveToNext() { + moveRight() + } + + /** + * Moves to the previous page. + */ + open fun moveToPrevious() { + moveLeft() + } + + /** + * Moves to the page at the right. + */ + protected open fun moveRight() { + if (pager.currentItem != adapter.count - 1) { + val holder = (currentPage as? ReaderPage)?.let { getPageHolder(it) } + if (holder != null && config.navigateToPan && holder.canPanRight()) { + holder.panRight() + } else { + pager.setCurrentItem(pager.currentItem + 1, config.usePageTransitions) + } + } + } + + /** + * Moves to the page at the left. + */ + protected open fun moveLeft() { + if (pager.currentItem != 0) { + val holder = (currentPage as? ReaderPage)?.let { getPageHolder(it) } + if (holder != null && config.navigateToPan && holder.canPanLeft()) { + holder.panLeft() + } else { + pager.setCurrentItem(pager.currentItem - 1, config.usePageTransitions) + } + } + } + + /** + * Moves to the page at the top (or previous). + */ + protected open fun moveUp() { + moveToPrevious() + } + + /** + * Moves to the page at the bottom (or next). + */ + protected open fun moveDown() { + moveToNext() + } + + /** + * Resets the adapter in order to recreate all the views. Used when a image configuration is + * changed. + */ + private fun refreshAdapter() { + val currentItem = pager.currentItem + adapter.refresh() + pager.adapter = adapter + pager.setCurrentItem(currentItem, false) + } + + /** + * Called from the containing activity when a key [event] is received. It should return true + * if the event was handled, false otherwise. + */ + override fun handleKeyEvent(event: KeyEvent): Boolean { + val isUp = event.action == KeyEvent.ACTION_UP + val ctrlPressed = event.metaState.and(KeyEvent.META_CTRL_ON) > 0 + + when (event.keyCode) { + KeyEvent.KEYCODE_VOLUME_DOWN -> { + if (!config.volumeKeysEnabled || activity.menuVisible) { + return false + } else if (isUp) { + if (!config.volumeKeysInverted) moveDown() else moveUp() + } + } + KeyEvent.KEYCODE_VOLUME_UP -> { + if (!config.volumeKeysEnabled || activity.menuVisible) { + return false + } else if (isUp) { + if (!config.volumeKeysInverted) moveUp() else moveDown() + } + } + KeyEvent.KEYCODE_DPAD_RIGHT -> { + if (isUp) { + if (ctrlPressed) moveToNext() else moveRight() + } + } + KeyEvent.KEYCODE_DPAD_LEFT -> { + if (isUp) { + if (ctrlPressed) moveToPrevious() else moveLeft() + } + } + KeyEvent.KEYCODE_DPAD_DOWN -> if (isUp) moveDown() + KeyEvent.KEYCODE_DPAD_UP -> if (isUp) moveUp() + KeyEvent.KEYCODE_PAGE_DOWN -> if (isUp) moveDown() + KeyEvent.KEYCODE_PAGE_UP -> if (isUp) moveUp() + KeyEvent.KEYCODE_MENU -> if (isUp) activity.toggleMenu() + else -> return false + } + return true + } + + /** + * Called from the containing activity when a generic motion [event] is received. It should + * return true if the event was handled, false otherwise. + */ + override fun handleGenericMotionEvent(event: MotionEvent): Boolean { + if (event.source and InputDevice.SOURCE_CLASS_POINTER != 0) { + when (event.action) { + MotionEvent.ACTION_SCROLL -> { + if (event.getAxisValue(MotionEvent.AXIS_VSCROLL) < 0.0f) { + moveDown() + } else { + moveUp() + } + return true + } + } + } + return false + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt new file mode 100644 index 000000000..089228424 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt @@ -0,0 +1,82 @@ +package eu.kanade.tachiyomi.ui.reader.viewer.pager + +import android.view.View +import android.view.ViewGroup +import eu.kanade.tachiyomi.ui.reader.loader.PageLoader +import eu.kanade.tachiyomi.ui.reader.model.ReaderPage +import eu.kanade.tachiyomi.util.system.createReaderThemeContext +import eu.kanade.tachiyomi.util.system.logcat +import eu.kanade.tachiyomi.widget.ViewPagerAdapter + +/** + * Pager adapter used by this [viewer] to where [ViewerChapters] updates are posted. + */ +class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() { + + /** + * List of currently set items. + */ + var items: MutableList = mutableListOf() + private set + + var currentChapter: PageLoader? = null + + /** + * Context that has been wrapped to use the correct theme values based on the + * current app theme and reader background color + */ + private var readerThemedContext = viewer.activity.createReaderThemeContext() + + /** + * Updates this adapter with the given [chapters]. It handles setting a few pages of the + * next/previous chapter to allow seamless transitions and inverting the pages if the viewer + * has R2L direction. + */ + fun setChapters(chapters: PageLoader) { + items = chapters.mPages.toMutableList() + currentChapter = chapters + + if (viewer is R2LPagerViewer) { + items.reverse() + } + + notifyDataSetChanged() + } + + /** + * Returns the amount of items of the adapter. + */ + override fun getCount(): Int { + return items.size + } + + /** + * Creates a new view for the item at the given [position]. + */ + override fun createView(container: ViewGroup, position: Int): View { + currentChapter?.request(position) + return when (val item = items[position]) { + is ReaderPage -> PagerPageHolder(readerThemedContext, viewer, item) + else -> throw NotImplementedError("Holder for ${item.javaClass} not implemented") + } + } + + /** + * Returns the current position of the given [view] on the adapter. + */ + override fun getItemPosition(view: Any): Int { + if (view is PositionableView) { + val position = items.indexOf(view.item) + if (position != -1) { + return position + } else { + logcat { "Position for ${view.item} not found" } + } + } + return POSITION_NONE + } + + fun refresh() { + readerThemedContext = viewer.activity.createReaderThemeContext() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewers.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewers.kt new file mode 100644 index 000000000..a79f85814 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewers.kt @@ -0,0 +1,53 @@ +package eu.kanade.tachiyomi.ui.reader.viewer.pager + +import eu.kanade.tachiyomi.ui.reader.ReaderActivity + +/** + * Implementation of a left to right PagerViewer. + */ +class L2RPagerViewer(activity: ReaderActivity) : PagerViewer(activity) { + /** + * Creates a new left to right pager. + */ + override fun createPager(): Pager { + return Pager(activity) + } +} + +/** + * Implementation of a right to left PagerViewer. + */ +class R2LPagerViewer(activity: ReaderActivity) : PagerViewer(activity) { + /** + * Creates a new right to left pager. + */ + override fun createPager(): Pager { + return Pager(activity) + } + + /** + * Moves to the next page. On a R2L pager the next page is the one at the left. + */ + override fun moveToNext() { + moveLeft() + } + + /** + * Moves to the previous page. On a R2L pager the previous page is the one at the right. + */ + override fun moveToPrevious() { + moveRight() + } +} + +/** + * Implementation of a vertical (top to bottom) PagerViewer. + */ +class VerticalPagerViewer(activity: ReaderActivity) : PagerViewer(activity) { + /** + * Creates a new vertical pager. + */ + override fun createPager(): Pager { + return Pager(activity, isHorizontal = false) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt new file mode 100644 index 000000000..60a2ababe --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt @@ -0,0 +1,96 @@ +package eu.kanade.tachiyomi.ui.reader.viewer.webtoon + +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import eu.kanade.tachiyomi.ui.reader.loader.PageLoader +import eu.kanade.tachiyomi.ui.reader.model.ReaderPage +import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView +import eu.kanade.tachiyomi.util.system.createReaderThemeContext + +/** + * RecyclerView Adapter used by this [viewer] to where [ViewerChapters] updates are posted. + */ +class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter() { + + /** + * List of currently set items. + */ + var items: List = emptyList() + private set + + var currentChapter: PageLoader? = null + + /** + * Context that has been wrapped to use the correct theme values based on the + * current app theme and reader background color + */ + private var readerThemedContext = viewer.activity.createReaderThemeContext() + + /** + * Updates this adapter with the given [chapter]. It handles setting a few pages of the + * next/previous chapter to allow seamless transitions. + */ + fun setChapters(chapter: PageLoader) { + currentChapter = chapter + items = chapter.mPages + } + + fun refresh() { + readerThemedContext = viewer.activity.createReaderThemeContext() + } + + /** + * Returns the amount of items of the adapter. + */ + override fun getItemCount(): Int { + return items.size + } + + /** + * Returns the view type for the item at the given [position]. + */ + override fun getItemViewType(position: Int): Int { + return when (val item = items[position]) { + is ReaderPage -> PAGE_VIEW + else -> error("Unknown view type for ${item.javaClass}") + } + } + + /** + * Creates a new view holder for an item with the given [viewType]. + */ + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + PAGE_VIEW -> { + val view = ReaderPageImageView(readerThemedContext, isWebtoon = true) + WebtoonPageHolder(view, viewer) + } + else -> error("Unknown view type") + } + } + + /** + * Binds an existing view [holder] with the item at the given [position]. + */ + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val item = items[position] + currentChapter!!.request(position) + when (holder) { + is WebtoonPageHolder -> holder.bind(item as ReaderPage) + } + } + + /** + * Recycles an existing view [holder] before adding it to the view pool. + */ + override fun onViewRecycled(holder: RecyclerView.ViewHolder) { + when (holder) { + is WebtoonPageHolder -> holder.recycle() + } + } +} + +/** + * View holder type of a chapter page view. + */ +private const val PAGE_VIEW = 0 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonBaseHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonBaseHolder.kt new file mode 100644 index 000000000..cbded3e7a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonBaseHolder.kt @@ -0,0 +1,29 @@ +package eu.kanade.tachiyomi.ui.reader.viewer.webtoon + +import android.content.Context +import android.view.View +import android.view.ViewGroup.LayoutParams +import androidx.recyclerview.widget.RecyclerView + +abstract class WebtoonBaseHolder( + view: View, + protected val viewer: WebtoonViewer, +) : RecyclerView.ViewHolder(view) { + + /** + * Context getter because it's used often. + */ + val context: Context get() = itemView.context + + /** + * Called when the view is recycled and being added to the view pool. + */ + open fun recycle() {} + + /** + * Extension method to set layout params to wrap content on this view. + */ + protected fun View.wrapContent() { + layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonConfig.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonConfig.kt new file mode 100644 index 000000000..922997438 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonConfig.kt @@ -0,0 +1,92 @@ +package eu.kanade.tachiyomi.ui.reader.viewer.webtoon + +import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences +import eu.kanade.tachiyomi.ui.reader.viewer.ViewerConfig +import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation +import eu.kanade.tachiyomi.ui.reader.viewer.navigation.DisabledNavigation +import eu.kanade.tachiyomi.ui.reader.viewer.navigation.EdgeNavigation +import eu.kanade.tachiyomi.ui.reader.viewer.navigation.KindlishNavigation +import eu.kanade.tachiyomi.ui.reader.viewer.navigation.LNavigation +import eu.kanade.tachiyomi.ui.reader.viewer.navigation.RightAndLeftNavigation +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +/** + * Configuration used by webtoon viewers. + */ +class WebtoonConfig( + scope: CoroutineScope, + readerPreferences: ReaderPreferences = ReaderActivity.readerPreferences, +) : ViewerConfig(readerPreferences, scope) { + + var themeChangedListener: (() -> Unit)? = null + + var imageCropBorders = false + private set + + var sidePadding = 0 + private set + + var longStripSplit = false + private set + + var longStripSplitChangedListener: ((Boolean) -> Unit)? = null + + val theme = readerPreferences.readerTheme().get() + + init { + readerPreferences.cropBordersWebtoon() + .register({ imageCropBorders = it }, { imagePropertyChangedListener?.invoke() }) + + readerPreferences.webtoonSidePadding() + .register({ sidePadding = it }, { imagePropertyChangedListener?.invoke() }) + + readerPreferences.navigationModeWebtoon() + .register({ navigationMode = it }, { updateNavigation(it) }) + + readerPreferences.webtoonNavInverted() + .register({ tappingInverted = it }, { navigator.invertMode = it }) + readerPreferences.webtoonNavInverted().changes() + .drop(1) + .onEach { navigationModeChangedListener?.invoke() } + .launchIn(scope) + + readerPreferences.dualPageSplitWebtoon() + .register({ dualPageSplit = it }, { imagePropertyChangedListener?.invoke() }) + + readerPreferences.dualPageInvertWebtoon() + .register({ dualPageInvert = it }, { imagePropertyChangedListener?.invoke() }) + + readerPreferences.readerTheme().changes() + .drop(1) + .distinctUntilChanged() + .onEach { themeChangedListener?.invoke() } + .launchIn(scope) + } + + override var navigator: ViewerNavigation = defaultNavigation() + set(value) { + field = value.also { it.invertMode = tappingInverted } + } + + override fun defaultNavigation(): ViewerNavigation { + return LNavigation() + } + + override fun updateNavigation(navigationMode: Int) { + this.navigator = when (navigationMode) { + 0 -> defaultNavigation() + 1 -> LNavigation() + 2 -> KindlishNavigation() + 3 -> EdgeNavigation() + 4 -> RightAndLeftNavigation() + 5 -> DisabledNavigation() + else -> defaultNavigation() + } + navigationModeChangedListener?.invoke() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonFrame.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonFrame.kt new file mode 100644 index 000000000..323cf0f63 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonFrame.kt @@ -0,0 +1,79 @@ +package eu.kanade.tachiyomi.ui.reader.viewer.webtoon + +import android.content.Context +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.ScaleGestureDetector +import android.widget.FrameLayout + +/** + * Frame layout which contains a [WebtoonRecyclerView]. It's needed to handle touch events, + * because the recyclerview is scaled and its touch events are translated, which breaks the + * detectors. + * + * TODO consider integrating this class into [WebtoonViewer]. + */ +class WebtoonFrame(context: Context) : FrameLayout(context) { + + /** + * Scale detector, either with pinch or quick scale. + */ + private val scaleDetector = ScaleGestureDetector(context, ScaleListener()) + + /** + * Fling detector. + */ + private val flingDetector = GestureDetector(context, FlingListener()) + + /** + * Recycler view added in this frame. + */ + private val recycler: WebtoonRecyclerView? + get() = getChildAt(0) as? WebtoonRecyclerView + + /** + * Dispatches a touch event to the detectors. + */ + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { + scaleDetector.onTouchEvent(ev) + flingDetector.onTouchEvent(ev) + return super.dispatchTouchEvent(ev) + } + + /** + * Scale listener used to delegate events to the recycler view. + */ + inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() { + override fun onScaleBegin(detector: ScaleGestureDetector): Boolean { + recycler?.onScaleBegin() + return true + } + + override fun onScale(detector: ScaleGestureDetector): Boolean { + recycler?.onScale(detector.scaleFactor) + return true + } + + override fun onScaleEnd(detector: ScaleGestureDetector) { + recycler?.onScaleEnd() + } + } + + /** + * Fling listener used to delegate events to the recycler view. + */ + inner class FlingListener : GestureDetector.SimpleOnGestureListener() { + override fun onDown(e: MotionEvent): Boolean { + return true + } + + override fun onFling( + e1: MotionEvent, + e2: MotionEvent, + velocityX: Float, + velocityY: Float, + ): Boolean { + return recycler?.zoomFling(velocityX.toInt(), velocityY.toInt()) ?: false + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonLayoutManager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonLayoutManager.kt new file mode 100644 index 000000000..74ced080c --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonLayoutManager.kt @@ -0,0 +1,55 @@ +@file:Suppress("PackageDirectoryMismatch") + +package androidx.recyclerview.widget + +import androidx.recyclerview.widget.RecyclerView.NO_POSITION +import eu.kanade.tachiyomi.ui.reader.ReaderActivity + +/** + * Layout manager used by the webtoon viewer. Item prefetch is disabled because the extra layout + * space feature is used which allows setting the image even if the holder is not visible, + * avoiding (in most cases) black views when they are visible. + * + * This layout manager uses the same package name as the support library in order to use a package + * protected method. + */ +class WebtoonLayoutManager(activity: ReaderActivity) : LinearLayoutManager(activity) { + + /** + * Extra layout space is set to half the screen height. + */ + private val extraLayoutSpace = activity.resources.displayMetrics.heightPixels / 2 + + init { + isItemPrefetchEnabled = false + } + + /** + * Returns the custom extra layout space. + */ + override fun getExtraLayoutSpace(state: RecyclerView.State): Int { + return extraLayoutSpace + } + + /** + * Returns the position of the last item whose end side is visible on screen. + */ + fun findLastEndVisibleItemPosition(): Int { + ensureLayoutState() + @ViewBoundsCheck.ViewBounds val preferredBoundsFlag = + (ViewBoundsCheck.FLAG_CVE_LT_PVE or ViewBoundsCheck.FLAG_CVE_EQ_PVE) + + val fromIndex = childCount - 1 + val toIndex = -1 + + val child = if (mOrientation == HORIZONTAL) { + mHorizontalBoundCheck + .findOneViewWithinBoundFlags(fromIndex, toIndex, preferredBoundsFlag, 0) + } else { + mVerticalBoundCheck + .findOneViewWithinBoundFlags(fromIndex, toIndex, preferredBoundsFlag, 0) + } + + return if (child == null) NO_POSITION else getPosition(child) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt new file mode 100644 index 000000000..6f3def857 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt @@ -0,0 +1,244 @@ +package eu.kanade.tachiyomi.ui.reader.viewer.webtoon + +import android.content.res.Resources +import android.graphics.drawable.Drawable +import android.view.Gravity +import android.view.LayoutInflater +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.widget.FrameLayout +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams +import androidx.core.view.updateMargins +import com.hippo.ehviewer.databinding.ReaderErrorBinding +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.ui.reader.model.ReaderPage +import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView +import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator +import eu.kanade.tachiyomi.util.system.dpToPx +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +/** + * Holder of the webtoon reader for a single page of a chapter. + * + * @param frame the root view for this holder. + * @param viewer the webtoon viewer. + * @constructor creates a new webtoon holder. + */ +class WebtoonPageHolder( + private val frame: ReaderPageImageView, + viewer: WebtoonViewer, +) : WebtoonBaseHolder(frame, viewer) { + + /** + * Loading progress bar to indicate the current progress. + */ + private val progressIndicator = createProgressIndicator() + + /** + * Progress bar container. Needed to keep a minimum height size of the holder, otherwise the + * adapter would create more views to fill the screen, which is not wanted. + */ + private lateinit var progressContainer: ViewGroup + + /** + * Error layout to show when the image fails to load. + */ + private var errorLayout: ReaderErrorBinding? = null + + /** + * Getter to retrieve the height of the recycler view. + */ + private val parentHeight + get() = viewer.recycler.height + + /** + * Page of a chapter. + */ + private var page: ReaderPage? = null + + private var progressJob: Job? = null + + private var statusJob: Job? = null + + private var scope = CoroutineScope(Dispatchers.IO) + + init { + refreshLayoutParams() + frame.onImageLoaded = { onImageDecoded() } + frame.onImageLoadError = { onImageDecodeError() } + frame.onScaleChanged = { viewer.activity.hideMenu() } + } + + /** + * Binds the given [page] with this view holder, subscribing to its state. + */ + fun bind(page: ReaderPage) { + this.page = page + progressJob = scope.launch(Dispatchers.Main) { + page.progress.collect { + progressIndicator.setProgress(it.toInt()) + } + } + statusJob = scope.launch(Dispatchers.Main) { + page.status.collectLatest { + processStatus(it) + } + } + refreshLayoutParams() + } + + private fun refreshLayoutParams() { + frame.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).apply { + if (!viewer.isContinuous) { + bottomMargin = 15.dpToPx + } + + val margin = Resources.getSystem().displayMetrics.widthPixels * (viewer.config.sidePadding / 100f) + marginEnd = margin.toInt() + marginStart = margin.toInt() + } + } + + /** + * Called when the view is recycled and added to the view pool. + */ + override fun recycle() { + statusJob?.cancel() + progressJob?.cancel() + removeErrorLayout() + frame.recycle() + page?.image?.recycle() + progressIndicator.setProgress(0, animated = false) + } + + /** + * Called when the status of the page changes. + * + * @param status the new status of the page. + */ + private fun processStatus(status: Page.State) { + when (status) { + Page.State.QUEUE -> setQueued() + Page.State.LOAD_PAGE -> setLoading() + Page.State.DOWNLOAD_IMAGE -> { + setDownloading() + } + + Page.State.READY -> { + page?.image?.mObtainedDrawable?.let { setImage(it) } + progressJob?.cancel() + } + + Page.State.ERROR -> { + setError() + progressJob?.cancel() + } + } + } + + /** + * Called when the page is queued. + */ + private fun setQueued() { + progressContainer.isVisible = true + progressIndicator.show() + removeErrorLayout() + } + + /** + * Called when the page is loading. + */ + private fun setLoading() { + progressContainer.isVisible = true + progressIndicator.show() + removeErrorLayout() + } + + /** + * Called when the page is downloading + */ + private fun setDownloading() { + progressContainer.isVisible = true + progressIndicator.show() + removeErrorLayout() + } + + /** + * Called when the page is ready. + */ + private fun setImage(drawable: Drawable) { + progressIndicator.setProgress(0) + removeErrorLayout() + frame.setImage(drawable, ReaderPageImageView.Config(10)) + } + + /** + * Called when the page has an error. + */ + private fun setError() { + progressContainer.isVisible = false + initErrorLayout() + } + + /** + * Called when the image is decoded and going to be displayed. + */ + private fun onImageDecoded() { + progressContainer.isVisible = false + } + + /** + * Called when the image fails to decode. + */ + private fun onImageDecodeError() { + progressContainer.isVisible = false + initErrorLayout() + } + + /** + * Creates a new progress bar. + */ + private fun createProgressIndicator(): ReaderProgressIndicator { + progressContainer = FrameLayout(context) + frame.addView(progressContainer, MATCH_PARENT, parentHeight) + + val progress = ReaderProgressIndicator(context).apply { + updateLayoutParams { + gravity = Gravity.CENTER_HORIZONTAL + updateMargins(top = parentHeight / 4) + } + } + progressContainer.addView(progress) + return progress + } + + /** + * Initializes a button to retry pages. + */ + private fun initErrorLayout(): ReaderErrorBinding { + if (errorLayout == null) { + errorLayout = ReaderErrorBinding.inflate(LayoutInflater.from(context), frame, true) + errorLayout?.root?.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, (parentHeight * 0.8).toInt()) + errorLayout?.actionRetry?.setOnClickListener { + TODO() + } + } + return errorLayout!! + } + + /** + * Removes the decode error layout from the holder, if found. + */ + private fun removeErrorLayout() { + errorLayout?.let { + frame.removeView(it.root) + errorLayout = null + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonRecyclerView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonRecyclerView.kt new file mode 100644 index 000000000..5e81ec7a9 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonRecyclerView.kt @@ -0,0 +1,327 @@ +package eu.kanade.tachiyomi.ui.reader.viewer.webtoon + +import android.animation.AnimatorSet +import android.animation.ValueAnimator +import android.content.Context +import android.util.AttributeSet +import android.view.HapticFeedbackConstants +import android.view.MotionEvent +import android.view.ViewConfiguration +import android.view.animation.DecelerateInterpolator +import androidx.core.animation.doOnEnd +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import eu.kanade.tachiyomi.ui.reader.viewer.GestureDetectorWithLongTap +import kotlin.math.abs + +/** + * Implementation of a [RecyclerView] used by the webtoon reader. + */ +class WebtoonRecyclerView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0, +) : RecyclerView(context, attrs, defStyle) { + + private var isZooming = false + private var atLastPosition = false + private var atFirstPosition = false + private var halfWidth = 0 + private var halfHeight = 0 + private var originalHeight = 0 + private var heightSet = false + private var firstVisibleItemPosition = 0 + private var lastVisibleItemPosition = 0 + private var currentScale = DEFAULT_RATE + + private val listener = GestureListener() + private val detector = Detector() + + var tapListener: ((MotionEvent) -> Unit)? = null + var longTapListener: ((MotionEvent) -> Boolean)? = null + + init { + isVerticalScrollBarEnabled = false + isHorizontalScrollBarEnabled = false + } + + override fun onMeasure(widthSpec: Int, heightSpec: Int) { + halfWidth = MeasureSpec.getSize(widthSpec) / 2 + halfHeight = MeasureSpec.getSize(heightSpec) / 2 + if (!heightSet) { + originalHeight = MeasureSpec.getSize(heightSpec) + heightSet = true + } + super.onMeasure(widthSpec, heightSpec) + } + + override fun onTouchEvent(e: MotionEvent): Boolean { + detector.onTouchEvent(e) + return super.onTouchEvent(e) + } + + override fun onScrolled(dx: Int, dy: Int) { + super.onScrolled(dx, dy) + val layoutManager = layoutManager + lastVisibleItemPosition = + (layoutManager as LinearLayoutManager).findLastVisibleItemPosition() + firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() + } + + override fun onScrollStateChanged(state: Int) { + super.onScrollStateChanged(state) + val layoutManager = layoutManager + val visibleItemCount = layoutManager?.childCount ?: 0 + val totalItemCount = layoutManager?.itemCount ?: 0 + atLastPosition = visibleItemCount > 0 && lastVisibleItemPosition == totalItemCount - 1 + atFirstPosition = firstVisibleItemPosition == 0 + } + + private fun getPositionX(positionX: Float): Float { + if (currentScale < 1) { + return 0f + } + val maxPositionX = halfWidth * (currentScale - 1) + return positionX.coerceIn(-maxPositionX, maxPositionX) + } + + private fun getPositionY(positionY: Float): Float { + if (currentScale < 1) { + return (originalHeight / 2 - halfHeight).toFloat() + } + val maxPositionY = halfHeight * (currentScale - 1) + return positionY.coerceIn(-maxPositionY, maxPositionY) + } + + private fun zoom( + fromRate: Float, + toRate: Float, + fromX: Float, + toX: Float, + fromY: Float, + toY: Float, + ) { + isZooming = true + val animatorSet = AnimatorSet() + val translationXAnimator = ValueAnimator.ofFloat(fromX, toX) + translationXAnimator.addUpdateListener { animation -> x = animation.animatedValue as Float } + + val translationYAnimator = ValueAnimator.ofFloat(fromY, toY) + translationYAnimator.addUpdateListener { animation -> y = animation.animatedValue as Float } + + val scaleAnimator = ValueAnimator.ofFloat(fromRate, toRate) + scaleAnimator.addUpdateListener { animation -> + setScaleRate(animation.animatedValue as Float) + } + animatorSet.playTogether(translationXAnimator, translationYAnimator, scaleAnimator) + animatorSet.duration = ANIMATOR_DURATION_TIME.toLong() + animatorSet.interpolator = DecelerateInterpolator() + animatorSet.start() + animatorSet.doOnEnd { + isZooming = false + currentScale = toRate + } + } + + fun zoomFling(velocityX: Int, velocityY: Int): Boolean { + if (currentScale <= 1f) return false + + val distanceTimeFactor = 0.4f + var newX: Float? = null + var newY: Float? = null + + if (velocityX != 0) { + val dx = (distanceTimeFactor * velocityX / 2) + newX = getPositionX(x + dx) + } + if (velocityY != 0 && (atFirstPosition || atLastPosition)) { + val dy = (distanceTimeFactor * velocityY / 2) + newY = getPositionY(y + dy) + } + + animate() + .apply { + newX?.let { x(it) } + newY?.let { y(it) } + } + .setInterpolator(DecelerateInterpolator()) + .setDuration(400) + .start() + + return true + } + + private fun zoomScrollBy(dx: Int, dy: Int) { + if (dx != 0) { + x = getPositionX(x + dx) + } + if (dy != 0) { + y = getPositionY(y + dy) + } + } + + private fun setScaleRate(rate: Float) { + scaleX = rate + scaleY = rate + } + + fun onScale(scaleFactor: Float) { + currentScale *= scaleFactor + currentScale = currentScale.coerceIn( + MIN_RATE, + MAX_SCALE_RATE, + ) + + setScaleRate(currentScale) + + layoutParams.height = if (currentScale < 1) { (originalHeight / currentScale).toInt() } else { originalHeight } + halfHeight = layoutParams.height / 2 + + if (currentScale != DEFAULT_RATE) { + x = getPositionX(x) + y = getPositionY(y) + } else { + x = 0f + y = 0f + } + + requestLayout() + } + + fun onScaleBegin() { + if (detector.isDoubleTapping) { + detector.isQuickScaling = true + } + } + + fun onScaleEnd() { + if (scaleX < MIN_RATE) { + zoom(currentScale, MIN_RATE, x, 0f, y, 0f) + } + } + + inner class GestureListener : GestureDetectorWithLongTap.Listener() { + + override fun onSingleTapConfirmed(ev: MotionEvent): Boolean { + tapListener?.invoke(ev) + return false + } + + override fun onDoubleTap(ev: MotionEvent): Boolean { + detector.isDoubleTapping = true + return false + } + + fun onDoubleTapConfirmed(ev: MotionEvent) { + if (!isZooming) { + if (scaleX != DEFAULT_RATE) { + zoom(currentScale, DEFAULT_RATE, x, 0f, y, 0f) + } else { + val toScale = 2f + val toX = (halfWidth - ev.x) * (toScale - 1) + val toY = (halfHeight - ev.y) * (toScale - 1) + zoom(DEFAULT_RATE, toScale, 0f, toX, 0f, toY) + } + } + } + + override fun onLongTapConfirmed(ev: MotionEvent) { + val listener = longTapListener + if (listener != null && listener.invoke(ev)) { + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + } + } + } + + inner class Detector : GestureDetectorWithLongTap(context, listener) { + + private var scrollPointerId = 0 + private var downX = 0 + private var downY = 0 + private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop + private var isZoomDragging = false + var isDoubleTapping = false + var isQuickScaling = false + + override fun onTouchEvent(ev: MotionEvent): Boolean { + val action = ev.actionMasked + val actionIndex = ev.actionIndex + + when (action) { + MotionEvent.ACTION_DOWN -> { + scrollPointerId = ev.getPointerId(0) + downX = (ev.x + 0.5f).toInt() + downY = (ev.y + 0.5f).toInt() + } + MotionEvent.ACTION_POINTER_DOWN -> { + scrollPointerId = ev.getPointerId(actionIndex) + downX = (ev.getX(actionIndex) + 0.5f).toInt() + downY = (ev.getY(actionIndex) + 0.5f).toInt() + } + MotionEvent.ACTION_MOVE -> { + if (isDoubleTapping && isQuickScaling) { + return true + } + + val index = ev.findPointerIndex(scrollPointerId) + if (index < 0) { + return false + } + + val x = (ev.getX(index) + 0.5f).toInt() + val y = (ev.getY(index) + 0.5f).toInt() + var dx = x - downX + var dy = if (atFirstPosition || atLastPosition) y - downY else 0 + + if (!isZoomDragging && currentScale > 1f) { + var startScroll = false + + if (abs(dx) > touchSlop) { + if (dx < 0) { + dx += touchSlop + } else { + dx -= touchSlop + } + startScroll = true + } + if (abs(dy) > touchSlop) { + if (dy < 0) { + dy += touchSlop + } else { + dy -= touchSlop + } + startScroll = true + } + + if (startScroll) { + isZoomDragging = true + } + } + + if (isZoomDragging) { + zoomScrollBy(dx, dy) + } + } + MotionEvent.ACTION_UP -> { + if (isDoubleTapping && !isQuickScaling) { + listener.onDoubleTapConfirmed(ev) + } + isZoomDragging = false + isDoubleTapping = false + isQuickScaling = false + } + MotionEvent.ACTION_CANCEL -> { + isZoomDragging = false + isDoubleTapping = false + isQuickScaling = false + } + } + return super.onTouchEvent(ev) + } + } +} + +private const val ANIMATOR_DURATION_TIME = 200 +private const val MIN_RATE = 0.5f +private const val DEFAULT_RATE = 1f +private const val MAX_SCALE_RATE = 3f diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonSubsamplingImageView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonSubsamplingImageView.kt new file mode 100644 index 000000000..328d6725a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonSubsamplingImageView.kt @@ -0,0 +1,20 @@ +package eu.kanade.tachiyomi.ui.reader.viewer.webtoon + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView + +/** + * Implementation of subsampling scale image view that ignores all touch events, because the + * webtoon viewer handles all the gestures. + */ +class WebtoonSubsamplingImageView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, +) : SubsamplingScaleImageView(context, attrs) { + + override fun onTouchEvent(event: MotionEvent): Boolean { + return false + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt new file mode 100644 index 000000000..0e65d0018 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt @@ -0,0 +1,270 @@ +package eu.kanade.tachiyomi.ui.reader.viewer.webtoon + +import android.annotation.SuppressLint +import android.graphics.PointF +import android.view.KeyEvent +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import androidx.core.app.ActivityCompat +import androidx.core.view.isGone +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.WebtoonLayoutManager +import eu.kanade.tachiyomi.ui.reader.loader.PageLoader +import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import eu.kanade.tachiyomi.ui.reader.model.ReaderPage +import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer +import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion +import eu.kanade.tachiyomi.util.system.logcat +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +import kotlin.math.max +import kotlin.math.min + +/** + * Implementation of a [BaseViewer] to display pages with a [RecyclerView]. + */ +class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = true) : BaseViewer { + + private val scope = MainScope() + + /** + * Recycler view used by this viewer. + */ + val recycler = WebtoonRecyclerView(activity) + + /** + * Frame containing the recycler view. + */ + private val frame = WebtoonFrame(activity) + + /** + * Layout manager of the recycler view. + */ + private val layoutManager = WebtoonLayoutManager(activity) + + /** + * Configuration used by this viewer, like allow taps, or crop image borders. + */ + val config = WebtoonConfig(scope) + + /** + * Adapter of the recycler view. + */ + private val adapter = WebtoonAdapter(this) + + /** + * Distance to scroll when the user taps on one side of the recycler view. + */ + private var scrollDistance = activity.resources.displayMetrics.heightPixels * 3 / 4 + + /** + * Currently active item. It can be a chapter page or a chapter transition. + */ + private var currentPage: Any? = null + + + private val threshold: Int = 13 + + init { + recycler.isVisible = false // Don't let the recycler layout yet + recycler.layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT) + recycler.isFocusable = false + recycler.itemAnimator = null + recycler.layoutManager = layoutManager + recycler.adapter = adapter + recycler.addOnScrollListener( + object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + onScrolled() + + if ((dy > threshold || dy < -threshold) && activity.menuVisible) { + activity.hideMenu() + } + } + }, + ) + recycler.tapListener = { event -> + val pos = PointF(event.rawX / recycler.width, event.rawY / recycler.height) + val navigator = config.navigator + + when (navigator.getAction(pos)) { + NavigationRegion.MENU -> activity.toggleMenu() + NavigationRegion.NEXT, NavigationRegion.RIGHT -> scrollDown() + NavigationRegion.PREV, NavigationRegion.LEFT -> scrollUp() + } + } + recycler.longTapListener = f@{ event -> + if (activity.menuVisible || config.longTapEnabled) { + val child = recycler.findChildViewUnder(event.x, event.y) + if (child != null) { + val position = recycler.getChildAdapterPosition(child) + val item = adapter.items.getOrNull(position) + if (item is ReaderPage) { + activity.onPageLongTap(item) + return@f true + } + } + } + false + } + + config.imagePropertyChangedListener = { + refreshAdapter() + } + + config.themeChangedListener = { + ActivityCompat.recreate(activity) + } + + config.navigationModeChangedListener = { + val showOnStart = config.navigationOverlayOnStart || config.forceNavigationOverlay + activity.binding.navigationOverlay.setNavigation(config.navigator, showOnStart) + } + + frame.layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT) + frame.addView(recycler) + } + + /** + * Returns the view this viewer uses. + */ + override fun getView(): View { + return frame + } + + /** + * Destroys this viewer. Called when leaving the reader or swapping viewers. + */ + override fun destroy() { + super.destroy() + scope.cancel() + } + + /** + * Called from the RecyclerView listener when a [page] is marked as active. It notifies the + * activity of the change and requests the preload of the next chapter if this is the last page. + */ + private fun onPageSelected(page: ReaderPage) { + activity.onPageSelected(page) + } + + /** + * Tells this viewer to set the given [provider] as active. + */ + override fun setGalleryProvider(provider: PageLoader) { + adapter.setChapters(provider) + + if (recycler.isGone) { + logcat { "Recycler first layout" } + recycler.isVisible = true + } + } + + /** + * Tells this viewer to move to the given [page]. + */ + override fun moveToPage(page: ReaderPage) { + logcat { "moveToPage" } + val position = adapter.items.indexOf(page) + if (position != -1) { + layoutManager.scrollToPositionWithOffset(position, 0) + if (layoutManager.findLastEndVisibleItemPosition() == -1) { + onScrolled(pos = position) + } + } else { + logcat { "Page $page not found in adapter" } + } + } + + fun onScrolled(pos: Int? = null) { + val position = pos ?: layoutManager.findLastEndVisibleItemPosition() + val item = adapter.items.getOrNull(position) + if (item != null && currentPage != item) { + currentPage = item + when (item) { + is ReaderPage -> onPageSelected(item) + } + } + } + + /** + * Scrolls up by [scrollDistance]. + */ + private fun scrollUp() { + if (config.usePageTransitions) { + recycler.smoothScrollBy(0, -scrollDistance) + } else { + recycler.scrollBy(0, -scrollDistance) + } + } + + /** + * Scrolls down by [scrollDistance]. + */ + private fun scrollDown() { + if (config.usePageTransitions) { + recycler.smoothScrollBy(0, scrollDistance) + } else { + recycler.scrollBy(0, scrollDistance) + } + } + + /** + * Called from the containing activity when a key [event] is received. It should return true + * if the event was handled, false otherwise. + */ + override fun handleKeyEvent(event: KeyEvent): Boolean { + val isUp = event.action == KeyEvent.ACTION_UP + + when (event.keyCode) { + KeyEvent.KEYCODE_VOLUME_DOWN -> { + if (!config.volumeKeysEnabled || activity.menuVisible) { + return false + } else if (isUp) { + if (!config.volumeKeysInverted) scrollDown() else scrollUp() + } + } + KeyEvent.KEYCODE_VOLUME_UP -> { + if (!config.volumeKeysEnabled || activity.menuVisible) { + return false + } else if (isUp) { + if (!config.volumeKeysInverted) scrollUp() else scrollDown() + } + } + KeyEvent.KEYCODE_MENU -> if (isUp) activity.toggleMenu() + + KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.KEYCODE_DPAD_UP, + KeyEvent.KEYCODE_PAGE_UP, + -> if (isUp) scrollUp() + + KeyEvent.KEYCODE_DPAD_RIGHT, + KeyEvent.KEYCODE_DPAD_DOWN, + KeyEvent.KEYCODE_PAGE_DOWN, + -> if (isUp) scrollDown() + else -> return false + } + return true + } + + /** + * Called from the containing activity when a generic motion [event] is received. It should + * return true if the event was handled, false otherwise. + */ + override fun handleGenericMotionEvent(event: MotionEvent): Boolean { + return false + } + + /** + * Notifies adapter of changes around the current page to trigger a relayout in the recycler. + * Used when an image configuration is changed. + */ + @SuppressLint("NotifyDataSetChanged") + private fun refreshAdapter() { + adapter.refresh() + adapter.notifyDataSetChanged() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/lang/RectFExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/lang/RectFExtensions.kt new file mode 100644 index 000000000..d1ad0b5b9 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/lang/RectFExtensions.kt @@ -0,0 +1,15 @@ +package eu.kanade.tachiyomi.util.lang + +import android.graphics.RectF +import eu.kanade.tachiyomi.data.preference.PreferenceValues + +fun RectF.invert(invertMode: PreferenceValues.TappingInvertMode): RectF { + val horizontal = invertMode.shouldInvertHorizontal + val vertical = invertMode.shouldInvertVertical + return when { + horizontal && vertical -> RectF(1f - this.right, 1f - this.bottom, 1f - this.left, 1f - this.top) + vertical -> RectF(this.left, 1f - this.bottom, this.right, 1f - this.top) + horizontal -> RectF(1f - this.right, this.top, 1f - this.left, this.bottom) + else -> this + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/preference/PreferenceExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/preference/PreferenceExtensions.kt new file mode 100644 index 000000000..213c94f27 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/preference/PreferenceExtensions.kt @@ -0,0 +1,33 @@ +package eu.kanade.tachiyomi.util.preference + +import android.widget.CompoundButton +import eu.kanade.tachiyomi.core.preference.Preference +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.onEach + +/** + * Binds a checkbox or switch view with a boolean preference. + */ +fun CompoundButton.bindToPreference(pref: Preference) { + isChecked = pref.get() + setOnCheckedChangeListener { _, isChecked -> pref.set(isChecked) } +} + +fun Preference.asHotFlow(block: (T) -> Unit): Flow { + block(get()) + return changes() + .onEach { block(it) } +} + +operator fun Preference>.plusAssign(item: T) { + set(get() + item) +} + +operator fun Preference>.minusAssign(item: T) { + set(get() - item) +} + +fun Preference.toggle(): Boolean { + set(!get()) + return get() +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ActivityExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ActivityExtensions.kt new file mode 100644 index 000000000..bbe0cb134 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ActivityExtensions.kt @@ -0,0 +1,14 @@ +package eu.kanade.tachiyomi.util.system + +import android.app.Activity +import android.os.Build + +/** + * Checks whether if the device has a display cutout (i.e. notch, camera cutout, etc.). + * + * Only works in Android 9+. + */ +fun Activity.hasDisplayCutout(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && + window.decorView.rootWindowInsets?.displayCutout != null +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/AnimationExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/AnimationExtensions.kt new file mode 100644 index 000000000..3136e38c5 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/AnimationExtensions.kt @@ -0,0 +1,22 @@ +package eu.kanade.tachiyomi.util.system + +import android.content.Context +import android.view.ViewPropertyAnimator +import android.view.animation.Animation +import androidx.constraintlayout.motion.widget.MotionScene.Transition + +/** Scale the duration of this [Animation] by [Context.animatorDurationScale] */ +fun Animation.applySystemAnimatorScale(context: Context) { + this.duration = (this.duration * context.animatorDurationScale).toLong() +} + +/** Scale the duration of this [Transition] by [Context.animatorDurationScale] */ +fun Transition.applySystemAnimatorScale(context: Context) { + // End layout of cover expanding animation tends to break when the transition is less than ~25ms + this.duration = (this.duration * context.animatorDurationScale).toInt().coerceAtLeast(25) +} + +/** Scale the duration of this [ViewPropertyAnimator] by [Context.animatorDurationScale] */ +fun ViewPropertyAnimator.applySystemAnimatorScale(context: Context): ViewPropertyAnimator = apply { + this.duration = (this.duration * context.animatorDurationScale).toLong() +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt new file mode 100644 index 000000000..a1b8f6313 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt @@ -0,0 +1,258 @@ +package eu.kanade.tachiyomi.util.system + +import android.app.ActivityManager +import android.app.NotificationManager +import android.content.Context +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.content.res.Resources +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.net.Uri +import android.net.wifi.WifiManager +import android.os.Build +import android.os.PowerManager +import android.provider.Settings +import android.util.TypedValue +import android.view.Display +import android.view.View +import android.view.WindowManager +import androidx.annotation.AttrRes +import androidx.annotation.ColorInt +import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService +import androidx.core.graphics.alpha +import androidx.core.graphics.blue +import androidx.core.graphics.green +import androidx.core.graphics.red +import com.hippo.unifile.UniFile +import java.io.File +import kotlin.math.max +import kotlin.math.roundToInt + +/** + * Checks if the give permission is granted. + * + * @param permission the permission to check. + * @return true if it has permissions. + */ +fun Context.hasPermission(permission: String) = ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED + +/** + * Returns the color for the given attribute. + * + * @param resource the attribute. + * @param alphaFactor the alpha number [0,1]. + */ +@ColorInt fun Context.getResourceColor(@AttrRes resource: Int, alphaFactor: Float = 1f): Int { + val typedArray = obtainStyledAttributes(intArrayOf(resource)) + val color = typedArray.getColor(0, 0) + typedArray.recycle() + + if (alphaFactor < 1f) { + val alpha = (color.alpha * alphaFactor).roundToInt() + return Color.argb(alpha, color.red, color.green, color.blue) + } + + return color +} + +@ColorInt fun Context.getThemeColor(attr: Int): Int { + val tv = TypedValue() + return if (this.theme.resolveAttribute(attr, tv, true)) { + if (tv.resourceId != 0) { + getColor(tv.resourceId) + } else { + tv.data + } + } else { + 0 + } +} + +val getDisplayMaxHeightInPx: Int + get() = Resources.getSystem().displayMetrics.let { max(it.heightPixels, it.widthPixels) } + +/** + * Converts to px. + */ +val Int.dpToPx: Int + get() = (this * Resources.getSystem().displayMetrics.density).toInt() + +/** + * Converts to px and takes into account LTR/RTL layout. + */ +val Float.dpToPxEnd: Float + get() = ( + this * Resources.getSystem().displayMetrics.density * + if (Resources.getSystem().isLTR) 1 else -1 + ) + +val Resources.isLTR + get() = configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR + +val Context.notificationManager: NotificationManager + get() = getSystemService()!! + +val Context.connectivityManager: ConnectivityManager + get() = getSystemService()!! + +val Context.wifiManager: WifiManager + get() = getSystemService()!! + +val Context.powerManager: PowerManager + get() = getSystemService()!! + +val Context.displayCompat: Display? + get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + display + } else { + @Suppress("DEPRECATION") + getSystemService()?.defaultDisplay + } + +/** Gets the duration multiplier for general animations on the device + * @see Settings.Global.ANIMATOR_DURATION_SCALE + */ +val Context.animatorDurationScale: Float + get() = Settings.Global.getFloat(this.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f) + +/** + * Convenience method to acquire a partial wake lock. + */ +fun Context.acquireWakeLock(tag: String): PowerManager.WakeLock { + val wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag:WakeLock") + wakeLock.acquire() + return wakeLock +} + +/** + * Returns true if the given service class is running. + */ +fun Context.isServiceRunning(serviceClass: Class<*>): Boolean { + val className = serviceClass.name + val manager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + @Suppress("DEPRECATION") + return manager.getRunningServices(Integer.MAX_VALUE) + .any { className == it.service.className } +} + +fun Context.createFileInCacheDir(name: String): File { + val file = File(externalCacheDir, name) + if (file.exists()) { + file.delete() + } + file.createNewFile() + return file +} + +private const val TABLET_UI_REQUIRED_SCREEN_WIDTH_DP = 720 + +// some tablets have screen width like 711dp = 1600px / 2.25 +private const val TABLET_UI_MIN_SCREEN_WIDTH_PORTRAIT_DP = 700 + +// make sure icons on the nav rail fit +private const val TABLET_UI_MIN_SCREEN_WIDTH_LANDSCAPE_DP = 600 + +fun Context.isTabletUi(): Boolean { + return resources.configuration.isTabletUi() +} + +fun Configuration.isTabletUi(): Boolean { + return smallestScreenWidthDp >= TABLET_UI_REQUIRED_SCREEN_WIDTH_DP +} + +fun Configuration.isAutoTabletUiAvailable(): Boolean { + return smallestScreenWidthDp >= TABLET_UI_MIN_SCREEN_WIDTH_LANDSCAPE_DP +} + +/** + * Returns true if current context is in night mode + */ +fun Context.isNightMode(): Boolean { + return resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES +} + +fun Context.isOnline(): Boolean { + val activeNetwork = connectivityManager.activeNetwork ?: return false + val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false + val maxTransport = when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 -> NetworkCapabilities.TRANSPORT_LOWPAN + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> NetworkCapabilities.TRANSPORT_WIFI_AWARE + else -> NetworkCapabilities.TRANSPORT_VPN + } + return (NetworkCapabilities.TRANSPORT_CELLULAR..maxTransport).any(networkCapabilities::hasTransport) +} + +/** + * Returns true if device is connected to Wifi. + */ +fun Context.isConnectedToWifi(): Boolean { + if (!wifiManager.isWifiEnabled) return false + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val activeNetwork = connectivityManager.activeNetwork ?: return false + val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false + + networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) && + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + } else { + @Suppress("DEPRECATION") + wifiManager.connectionInfo.bssid != null + } +} + +/** + * Gets document size of provided [Uri] + * + * @return document size of [uri] or null if size can't be obtained + */ +fun Context.getUriSize(uri: Uri): Long? { + return UniFile.fromUri(this, uri).length().takeIf { it >= 0 } +} + +/** + * Returns true if [packageName] is installed. + */ +fun Context.isPackageInstalled(packageName: String): Boolean { + return try { + packageManager.getApplicationInfo(packageName, 0) + true + } catch (e: PackageManager.NameNotFoundException) { + false + } +} + +fun Context.getApplicationIcon(pkgName: String): Drawable? { + return try { + packageManager.getApplicationIcon(pkgName) + } catch (e: PackageManager.NameNotFoundException) { + null + } +} + +/** + * Gets system's config_navBarNeedsScrim boolean flag added in Android 10, defaults to true. + */ +fun Context.isNavigationBarNeedsScrim(): Boolean { + return Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || + InternalResourceHelper.getBoolean(this, "config_navBarNeedsScrim", true) +} + +/** + * Creates night mode Context depending on reader theme/background + * + * Context wrapping method obtained from AppCompatDelegateImpl + * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java;l=348;drc=e28752c96fc3fb4d3354781469a1af3dbded4898 + */ +fun Context.createReaderThemeContext(): Context { + return this + TODO() +} + +fun logcat(block: () -> String) { + return + TODO() +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/GLUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/GLUtil.kt new file mode 100644 index 000000000..59876e002 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/GLUtil.kt @@ -0,0 +1,47 @@ +package eu.kanade.tachiyomi.util.system + +import javax.microedition.khronos.egl.EGL10 +import javax.microedition.khronos.egl.EGLConfig +import javax.microedition.khronos.egl.EGLContext +import kotlin.math.max + +object GLUtil { + val maxTextureSize: Int by lazy { + // Get EGL Display + val egl = EGLContext.getEGL() as EGL10 + val display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY) + + // Initialise + val version = IntArray(2) + egl.eglInitialize(display, version) + + // Query total number of configurations + val totalConfigurations = IntArray(1) + egl.eglGetConfigs(display, null, 0, totalConfigurations) + + // Query actual list configurations + val configurationsList = arrayOfNulls(totalConfigurations[0]) + egl.eglGetConfigs(display, configurationsList, totalConfigurations[0], totalConfigurations) + + val textureSize = IntArray(1) + var maximumTextureSize = 0 + + // Iterate through all the configurations to located the maximum texture size + for (i in 0 until totalConfigurations[0]) { + // Only need to check for width since opengl textures are always squared + egl.eglGetConfigAttrib(display, configurationsList[i], EGL10.EGL_MAX_PBUFFER_WIDTH, textureSize) + + // Keep track of the maximum texture size + if (maximumTextureSize < textureSize[0]) maximumTextureSize = textureSize[0] + } + + // Release + egl.eglTerminate(display) + + // Return largest texture size found, or default + max(maximumTextureSize, IMAGE_MAX_BITMAP_DIMENSION) + } +} + +// Safe minimum default size +private const val IMAGE_MAX_BITMAP_DIMENSION = 2048 diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt new file mode 100644 index 000000000..6e71ed2f0 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt @@ -0,0 +1,248 @@ +package eu.kanade.tachiyomi.util.system + +import android.content.Context +import android.content.res.Configuration +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.BitmapRegionDecoder +import android.graphics.Color +import android.graphics.Rect +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.os.Build +import android.webkit.MimeTypeMap +import androidx.annotation.ColorInt +import androidx.core.graphics.alpha +import androidx.core.graphics.applyCanvas +import androidx.core.graphics.blue +import androidx.core.graphics.createBitmap +import androidx.core.graphics.get +import androidx.core.graphics.green +import androidx.core.graphics.red +import com.hippo.unifile.UniFile +import java.io.BufferedInputStream +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.net.URLConnection +import kotlin.math.abs +import kotlin.math.min + +object ImageUtil { + fun getExtensionFromMimeType(mime: String?): String { + return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime) + ?: SUPPLEMENTARY_MIMETYPE_MAPPING[mime] + ?: "jpg" + } + + enum class ImageType(val mime: String, val extension: String) { + AVIF("image/avif", "avif"), + GIF("image/gif", "gif"), + HEIF("image/heif", "heif"), + JPEG("image/jpeg", "jpg"), + JXL("image/jxl", "jxl"), + PNG("image/png", "png"), + WEBP("image/webp", "webp"), + } + + /** + * Check whether the image is wide (which we consider a double-page spread). + * + * @return true if the width is greater than the height + */ + fun isWideImage(imageStream: BufferedInputStream): Boolean { + val options = extractImageOptions(imageStream) + return options.outWidth > options.outHeight + } + + /** + * Extract the 'side' part from imageStream and return it as InputStream. + */ + fun splitInHalf(imageStream: InputStream, side: Side): InputStream { + val imageBytes = imageStream.readBytes() + + val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) + val height = imageBitmap.height + val width = imageBitmap.width + + val singlePage = Rect(0, 0, width / 2, height) + + val half = createBitmap(width / 2, height) + val part = when (side) { + Side.RIGHT -> Rect(width - width / 2, 0, width, height) + Side.LEFT -> Rect(0, 0, width / 2, height) + } + half.applyCanvas { + drawBitmap(imageBitmap, part, singlePage, null) + } + val output = ByteArrayOutputStream() + half.compress(Bitmap.CompressFormat.JPEG, 100, output) + + return ByteArrayInputStream(output.toByteArray()) + } + + /** + * Split the image into left and right parts, then merge them into a new image. + */ + fun splitAndMerge(imageStream: InputStream, upperSide: Side): InputStream { + val imageBytes = imageStream.readBytes() + + val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) + val height = imageBitmap.height + val width = imageBitmap.width + + val result = createBitmap(width / 2, height * 2) + result.applyCanvas { + // right -> upper + val rightPart = when (upperSide) { + Side.RIGHT -> Rect(width - width / 2, 0, width, height) + Side.LEFT -> Rect(0, 0, width / 2, height) + } + val upperPart = Rect(0, 0, width / 2, height) + drawBitmap(imageBitmap, rightPart, upperPart, null) + // left -> bottom + val leftPart = when (upperSide) { + Side.LEFT -> Rect(width - width / 2, 0, width, height) + Side.RIGHT -> Rect(0, 0, width / 2, height) + } + val bottomPart = Rect(0, height, width / 2, height * 2) + drawBitmap(imageBitmap, leftPart, bottomPart, null) + } + + val output = ByteArrayOutputStream() + result.compress(Bitmap.CompressFormat.JPEG, 100, output) + return ByteArrayInputStream(output.toByteArray()) + } + + enum class Side { + RIGHT, + LEFT, + } + + /** + * Check whether the image is considered a tall image. + * + * @return true if the height:width ratio is greater than 3. + */ + private fun isTallImage(imageStream: InputStream): Boolean { + val options = extractImageOptions(imageStream, resetAfterExtraction = false) + return (options.outHeight / options.outWidth) > 3 + } + + private fun splitImageName(filenamePrefix: String, index: Int) = "${filenamePrefix}__${"%03d".format(index + 1)}.jpg" + + /** + * Split the imageStream according to the provided splitData + */ + fun splitStrip(splitData: SplitData, streamFn: () -> InputStream): InputStream { + val bitmapRegionDecoder = getBitmapRegionDecoder(streamFn()) + ?: throw Exception("Failed to create new instance of BitmapRegionDecoder") + + logcat { + "WebtoonSplit #${splitData.index} with topOffset=${splitData.topOffset} " + + "splitHeight=${splitData.splitHeight} bottomOffset=${splitData.bottomOffset}" + } + + val region = Rect(0, splitData.topOffset, splitData.splitWidth, splitData.bottomOffset) + + try { + val splitBitmap = bitmapRegionDecoder.decodeRegion(region, null) + val outputStream = ByteArrayOutputStream() + splitBitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) + return ByteArrayInputStream(outputStream.toByteArray()) + } catch (e: Throwable) { + throw e + } finally { + bitmapRegionDecoder.recycle() + } + } + + fun getSplitDataForStream(imageStream: InputStream): List { + return extractImageOptions(imageStream).splitData + } + + private val BitmapFactory.Options.splitData + get(): List { + val imageHeight = outHeight + val imageWidth = outWidth + + // -1 so it doesn't try to split when imageHeight = optimalImageHeight + val partCount = (imageHeight - 1) / optimalImageHeight + 1 + val optimalSplitHeight = imageHeight / partCount + + logcat { + "Generating SplitData for image (height: $imageHeight): " + + "$partCount parts @ ${optimalSplitHeight}px height per part" + } + + return mutableListOf().apply { + val range = 0 until partCount + for (index in range) { + // Only continue if the list is empty or there is image remaining + if (isNotEmpty() && imageHeight <= last().bottomOffset) break + + val topOffset = index * optimalSplitHeight + var splitHeight = min(optimalSplitHeight, imageHeight - topOffset) + + if (index == range.last) { + val remainingHeight = imageHeight - (topOffset + splitHeight) + splitHeight += remainingHeight + } + + add(SplitData(index, topOffset, splitHeight, imageWidth)) + } + } + } + + data class SplitData( + val index: Int, + val topOffset: Int, + val splitHeight: Int, + val splitWidth: Int, + ) { + val bottomOffset = topOffset + splitHeight + } + + private fun @receiver:ColorInt Int.isDark(): Boolean = + red < 40 && blue < 40 && green < 40 && alpha > 200 + + private fun @receiver:ColorInt Int.isCloseTo(other: Int): Boolean = + abs(red - other.red) < 30 && abs(green - other.green) < 30 && abs(blue - other.blue) < 30 + + private fun @receiver:ColorInt Int.isWhite(): Boolean = + red + blue + green > 740 + + /** + * Used to check an image's dimensions without loading it in the memory. + */ + private fun extractImageOptions( + imageStream: InputStream, + resetAfterExtraction: Boolean = true, + ): BitmapFactory.Options { + imageStream.mark(imageStream.available() + 1) + + val imageBytes = imageStream.readBytes() + val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options) + if (resetAfterExtraction) imageStream.reset() + return options + } + + private fun getBitmapRegionDecoder(imageStream: InputStream): BitmapRegionDecoder? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + BitmapRegionDecoder.newInstance(imageStream) + } else { + @Suppress("DEPRECATION") + BitmapRegionDecoder.newInstance(imageStream, false) + } + } + + private val optimalImageHeight = getDisplayMaxHeightInPx * 2 + + // Android doesn't include some mappings + private val SUPPLEMENTARY_MIMETYPE_MAPPING = mapOf( + // https://issuetracker.google.com/issues/182703810 + "image/jxl" to "jxl", + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/InternalResourceHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/InternalResourceHelper.kt new file mode 100644 index 000000000..12529a19c --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/InternalResourceHelper.kt @@ -0,0 +1,28 @@ +package eu.kanade.tachiyomi.util.system + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.Resources + +object InternalResourceHelper { + + fun getBoolean(context: Context, resName: String, defaultValue: Boolean): Boolean { + val id = getResourceId(resName, "bool") + return if (id != 0) { + context.createPackageContext("android", 0).resources.getBoolean(id) + } else { + defaultValue + } + } + + /** + * Get resource id from system resources + * @param resName resource name to get + * @param type resource type of [resName] to get + * @return 0 if not available + */ + @SuppressLint("DiscouragedApi") + private fun getResourceId(resName: String, type: String): Int { + return Resources.getSystem().getIdentifier(resName, type, "android") + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt new file mode 100644 index 000000000..412f6a13e --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt @@ -0,0 +1,132 @@ +@file:Suppress("NOTHING_TO_INLINE") + +package eu.kanade.tachiyomi.util.view + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.Resources +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.view.Gravity +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.annotation.MenuRes +import androidx.annotation.StringRes +import androidx.appcompat.content.res.AppCompatResources +import androidx.appcompat.view.menu.MenuBuilder +import androidx.appcompat.widget.PopupMenu +import androidx.appcompat.widget.TooltipCompat +import androidx.core.view.forEach +import com.google.android.material.shape.MaterialShapeDrawable +import com.hippo.ehviewer.R +import eu.kanade.tachiyomi.util.system.getResourceColor + +/** + * Adds a tooltip shown on long press. + * + * @param stringRes String resource for tooltip. + */ +inline fun View.setTooltip(@StringRes stringRes: Int) { + setTooltip(context.getString(stringRes)) +} + +/** + * Adds a tooltip shown on long press. + * + * @param text Text for tooltip. + */ +inline fun View.setTooltip(text: String) { + TooltipCompat.setTooltipText(this, text) +} + +/** + * Shows a popup menu on top of this view. + * + * @param menuRes menu items to inflate the menu with. + * @param initMenu function to execute when the menu after is inflated. + * @param onMenuItemClick function to execute when a menu item is clicked. + */ +inline fun View.popupMenu( + @MenuRes menuRes: Int, + noinline initMenu: (Menu.() -> Unit)? = null, + noinline onMenuItemClick: MenuItem.() -> Unit, +): PopupMenu { + val popup = PopupMenu(context, this, Gravity.NO_GRAVITY, androidx.appcompat.R.attr.actionOverflowMenuStyle, 0) + popup.menuInflater.inflate(menuRes, popup.menu) + + if (initMenu != null) { + popup.menu.initMenu() + } + popup.setOnMenuItemClickListener { + it.onMenuItemClick() + true + } + + popup.show() + return popup +} + +/** + * Shows a popup menu on top of this view. + * + * @param items menu item names to inflate the menu with. List of itemId to stringRes pairs. + * @param selectedItemId optionally show a checkmark beside an item with this itemId. + * @param onMenuItemClick function to execute when a menu item is clicked. + */ +@SuppressLint("RestrictedApi") +inline fun View.popupMenu( + items: List>, + selectedItemId: Int? = null, + noinline onMenuItemClick: MenuItem.() -> Unit, +): PopupMenu { + val popup = PopupMenu(context, this, Gravity.NO_GRAVITY, androidx.appcompat.R.attr.actionOverflowMenuStyle, 0) + items.forEach { (id, stringRes) -> + popup.menu.add(0, id, 0, stringRes) + } + + if (selectedItemId != null) { + (popup.menu as? MenuBuilder)?.setOptionalIconsVisible(true) + val emptyIcon = AppCompatResources.getDrawable(context, R.drawable.ic_blank_24dp) + popup.menu.forEach { item -> + item.icon = when (item.itemId) { + selectedItemId -> AppCompatResources.getDrawable(context, R.drawable.ic_check_24dp)?.mutate()?.apply { + setTint(context.getResourceColor(android.R.attr.textColorPrimary)) + } + else -> emptyIcon + } + } + } + + popup.setOnMenuItemClickListener { + it.onMenuItemClick() + true + } + + popup.show() + return popup +} + +/** + * Returns a deep copy of the provided [Drawable] + */ +inline fun T.copy(context: Context): T? { + return (constantState?.newDrawable()?.mutate() as? T).apply { + if (this is MaterialShapeDrawable) { + initializeElevationOverlay(context) + } + } +} + +fun View?.isVisibleOnScreen(): Boolean { + if (this == null) { + return false + } + if (!this.isShown) { + return false + } + val actualPosition = Rect() + this.getGlobalVisibleRect(actualPosition) + val screen = Rect(0, 0, Resources.getSystem().displayMetrics.widthPixels, Resources.getSystem().displayMetrics.heightPixels) + return actualPosition.intersect(screen) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/WindowExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/WindowExtensions.kt new file mode 100644 index 000000000..fc5a2fc1f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/view/WindowExtensions.kt @@ -0,0 +1,35 @@ +package eu.kanade.tachiyomi.util.view + +import android.content.Context +import android.graphics.Color +import android.view.Window +import android.view.WindowManager +import com.google.android.material.elevation.ElevationOverlayProvider +import eu.kanade.tachiyomi.util.system.getResourceColor +import eu.kanade.tachiyomi.util.system.isNavigationBarNeedsScrim + +/** + * Sets navigation bar color to transparent if system's config_navBarNeedsScrim is false, + * otherwise it will use the theme navigationBarColor with 70% opacity. + * + * @see isNavigationBarNeedsScrim + */ +fun Window.setNavigationBarTransparentCompat(context: Context, elevation: Float = 0F) { + navigationBarColor = if (context.isNavigationBarNeedsScrim()) { + // Set navbar scrim 70% of navigationBarColor + ElevationOverlayProvider(context).compositeOverlayIfNeeded( + context.getResourceColor(android.R.attr.navigationBarColor, 0.7F), + elevation, + ) + } else { + Color.TRANSPARENT + } +} + +fun Window.setSecureScreen(enabled: Boolean) { + if (enabled) { + setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) + } else { + clearFlags(WindowManager.LayoutParams.FLAG_SECURE) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/MaterialSpinnerView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/MaterialSpinnerView.kt new file mode 100644 index 000000000..bdd1c3d02 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/MaterialSpinnerView.kt @@ -0,0 +1,167 @@ +package eu.kanade.tachiyomi.widget + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.view.Gravity +import android.view.LayoutInflater +import android.view.MenuItem +import android.widget.FrameLayout +import androidx.annotation.ArrayRes +import androidx.appcompat.content.res.AppCompatResources +import androidx.appcompat.view.menu.MenuBuilder +import androidx.appcompat.widget.PopupMenu +import androidx.core.content.withStyledAttributes +import androidx.core.view.forEach +import androidx.core.view.get +import androidx.core.view.size +import com.hippo.ehviewer.R +import eu.kanade.tachiyomi.core.preference.Preference +import com.hippo.ehviewer.databinding.PrefSpinnerBinding +import eu.kanade.tachiyomi.util.system.getResourceColor + +class MaterialSpinnerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + FrameLayout(context, attrs) { + + private var entries = emptyList() + private var selectedPosition = 0 + private var popup: PopupMenu? = null + + var onItemSelectedListener: ((Int) -> Unit)? = null + set(value) { + field = value + if (value != null) { + popup = makeSettingsPopup() + setOnTouchListener(popup?.dragToOpenListener) + setOnClickListener { + popup?.show() + } + } + } + + private val emptyIcon by lazy { + AppCompatResources.getDrawable(context, R.drawable.ic_blank_24dp) + } + private val checkmarkIcon by lazy { + AppCompatResources.getDrawable(context, R.drawable.ic_check_24dp)?.mutate()?.apply { + setTint(context.getResourceColor(android.R.attr.textColorPrimary)) + } + } + + private val binding = PrefSpinnerBinding.inflate(LayoutInflater.from(context), this, false) + + init { + addView(binding.root) + + context.withStyledAttributes(set = attrs, attrs = R.styleable.MaterialSpinnerView) { + val title = getString(R.styleable.MaterialSpinnerView_title).orEmpty() + binding.title.text = title + + val viewEntries = ( + getTextArray(R.styleable.MaterialSpinnerView_android_entries) + ?: emptyArray() + ).map { it.toString() } + entries = viewEntries + binding.details.text = viewEntries.firstOrNull().orEmpty() + } + } + + fun setSelection(selection: Int) { + if (selectedPosition < (popup?.menu?.size ?: 0)) { + popup?.menu?.getItem(selectedPosition)?.let { + it.icon = emptyIcon + } + } + selectedPosition = selection + popup?.menu?.getItem(selectedPosition)?.let { + it.icon = checkmarkIcon + } + binding.details.text = entries.getOrNull(selection).orEmpty() + } + + fun bindToPreference(pref: Preference, offset: Int = 0, block: ((Int) -> Unit)? = null) { + setSelection(pref.get() - offset) + + popup = makeSettingsPopup(pref, offset, block) + setOnTouchListener(popup?.dragToOpenListener) + setOnClickListener { + popup?.show() + } + } + + fun > bindToPreference(pref: Preference, clazz: Class) { + val enumConstants = clazz.enumConstants + enumConstants?.indexOf(pref.get())?.let { setSelection(it) } + + popup = makeSettingsPopup(pref, clazz) + setOnTouchListener(popup?.dragToOpenListener) + setOnClickListener { + popup?.show() + } + } + + fun bindToIntPreference(pref: Preference, @ArrayRes intValuesResource: Int, block: ((Int) -> Unit)? = null) { + val intValues = resources.getStringArray(intValuesResource).map { it.toIntOrNull() } + setSelection(intValues.indexOf(pref.get())) + + popup = makeSettingsPopup(pref, intValues, block) + setOnTouchListener(popup?.dragToOpenListener) + setOnClickListener { + popup?.show() + } + } + + private fun > makeSettingsPopup(preference: Preference, clazz: Class): PopupMenu { + return createPopupMenu { pos -> + onItemSelectedListener?.invoke(pos) + + val enumConstants = clazz.enumConstants + enumConstants?.get(pos)?.let { enumValue -> preference.set(enumValue) } + } + } + + private fun makeSettingsPopup(preference: Preference, intValues: List, block: ((Int) -> Unit)? = null): PopupMenu { + return createPopupMenu { pos -> + preference.set(intValues[pos] ?: 0) + block?.invoke(pos) + } + } + + private fun makeSettingsPopup(preference: Preference, offset: Int = 0, block: ((Int) -> Unit)? = null): PopupMenu { + return createPopupMenu { pos -> + preference.set(pos + offset) + block?.invoke(pos) + } + } + + private fun makeSettingsPopup(): PopupMenu { + return createPopupMenu { pos -> + onItemSelectedListener?.invoke(pos) + } + } + + private fun menuClicked(menuItem: MenuItem): Int { + val pos = menuItem.itemId + setSelection(pos) + return pos + } + + @SuppressLint("RestrictedApi") + fun createPopupMenu(onItemClick: (Int) -> Unit): PopupMenu { + val popup = PopupMenu(context, this, Gravity.END, androidx.appcompat.R.attr.actionOverflowMenuStyle, 0) + entries.forEachIndexed { index, entry -> + popup.menu.add(0, index, 0, entry) + } + (popup.menu as? MenuBuilder)?.setOptionalIconsVisible(true) + popup.menu.forEach { + it.icon = emptyIcon + } + popup.menu[selectedPosition].icon = checkmarkIcon + popup.setOnMenuItemClickListener { menuItem -> + val pos = menuClicked(menuItem) + onItemClick(pos) + true + } + return popup + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/OutlineSpan.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/OutlineSpan.kt new file mode 100644 index 000000000..e5125a28c --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/OutlineSpan.kt @@ -0,0 +1,56 @@ +package eu.kanade.tachiyomi.widget + +import android.graphics.Canvas +import android.graphics.Paint +import android.text.style.ReplacementSpan +import androidx.annotation.ColorInt +import androidx.annotation.Dimension + +/** + * Source: https://github.com/santaevpavel + * + * A class that draws the outlines of a text when given a stroke color and stroke width. + */ +class OutlineSpan( + @ColorInt private val strokeColor: Int, + @Dimension private val strokeWidth: Float, +) : ReplacementSpan() { + + override fun getSize( + paint: Paint, + text: CharSequence, + start: Int, + end: Int, + fm: Paint.FontMetricsInt?, + ): Int { + return paint.measureText(text.toString().substring(start until end)).toInt() + } + + override fun draw( + canvas: Canvas, + text: CharSequence, + start: Int, + end: Int, + x: Float, + top: Int, + y: Int, + bottom: Int, + paint: Paint, + ) { + val originTextColor = paint.color + + paint.apply { + color = strokeColor + style = Paint.Style.STROKE + this.strokeWidth = this@OutlineSpan.strokeWidth + } + canvas.drawText(text, start, end, x, y.toFloat(), paint) + + paint.apply { + color = originTextColor + style = Paint.Style.FILL + } + + canvas.drawText(text, start, end, x, y.toFloat(), paint) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/ViewPagerAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/ViewPagerAdapter.kt new file mode 100644 index 000000000..4e35f4c86 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/ViewPagerAdapter.kt @@ -0,0 +1,33 @@ +package eu.kanade.tachiyomi.widget + +import android.view.View +import android.view.ViewGroup +import androidx.viewpager.widget.PagerAdapter + +abstract class ViewPagerAdapter : PagerAdapter() { + + protected abstract fun createView(container: ViewGroup, position: Int): View + + protected open fun destroyView(container: ViewGroup, position: Int, view: View) { + } + + override fun instantiateItem(container: ViewGroup, position: Int): Any { + val view = createView(container, position) + container.addView(view) + return view + } + + override fun destroyItem(container: ViewGroup, position: Int, obj: Any) { + val view = obj as View + destroyView(container, position, view) + container.removeView(view) + } + + override fun isViewFromObject(view: View, obj: Any): Boolean { + return view === obj + } + + interface PositionableView { + val item: Any + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/listener/SimpleAnimationListener.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/listener/SimpleAnimationListener.kt new file mode 100644 index 000000000..d06ef6443 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/listener/SimpleAnimationListener.kt @@ -0,0 +1,11 @@ +package eu.kanade.tachiyomi.widget.listener + +import android.view.animation.Animation + +open class SimpleAnimationListener : Animation.AnimationListener { + override fun onAnimationRepeat(animation: Animation) {} + + override fun onAnimationEnd(animation: Animation) {} + + override fun onAnimationStart(animation: Animation) {} +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/listener/SimpleTabSelectedListener.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/listener/SimpleTabSelectedListener.kt new file mode 100644 index 000000000..68a226466 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/listener/SimpleTabSelectedListener.kt @@ -0,0 +1,14 @@ +package eu.kanade.tachiyomi.widget.listener + +import com.google.android.material.tabs.TabLayout + +open class SimpleTabSelectedListener : TabLayout.OnTabSelectedListener { + override fun onTabSelected(tab: TabLayout.Tab?) { + } + + override fun onTabUnselected(tab: TabLayout.Tab?) { + } + + override fun onTabReselected(tab: TabLayout.Tab?) { + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/sheet/BaseBottomSheetDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/sheet/BaseBottomSheetDialog.kt new file mode 100644 index 000000000..af64c97f1 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/sheet/BaseBottomSheetDialog.kt @@ -0,0 +1,52 @@ +package eu.kanade.tachiyomi.widget.sheet + +import android.content.Context +import android.os.Bundle +import android.util.DisplayMetrics +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.hippo.ehviewer.R +import eu.kanade.tachiyomi.util.system.displayCompat +import eu.kanade.tachiyomi.util.system.isNightMode +import eu.kanade.tachiyomi.util.view.setNavigationBarTransparentCompat + +abstract class BaseBottomSheetDialog(context: Context) : BottomSheetDialog(context) { + + abstract fun createView(inflater: LayoutInflater): View + + @Suppress("DEPRECATION") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val rootView = createView(layoutInflater) + setContentView(rootView) + + // Enforce max width for tablets + val width = context.resources.getDimensionPixelSize(R.dimen.bottom_sheet_width) + if (width > 0) { + behavior.maxWidth = width + } + + // Set peek height to 50% display height + context.displayCompat?.let { + val metrics = DisplayMetrics() + it.getRealMetrics(metrics) + behavior.peekHeight = metrics.heightPixels / 2 + } + + // Set navbar color to transparent for edge-to-edge bottom sheet if we can use light navigation bar + // TODO Replace deprecated systemUiVisibility when material-components uses new API to modify status bar icons + // window?.setNavigationBarTransparentCompat(context, behavior.getElevation()) + window?.setNavigationBarTransparentCompat(context, 0f) + val bottomSheet = rootView.parent as ViewGroup + var flags = bottomSheet.systemUiVisibility + flags = if (context.isNightMode()) { + flags and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv() + } else { + flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + } + bottomSheet.systemUiVisibility = flags + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/sheet/BottomSheetViewPager.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/sheet/BottomSheetViewPager.kt new file mode 100644 index 000000000..91b710757 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/sheet/BottomSheetViewPager.kt @@ -0,0 +1,56 @@ +package eu.kanade.tachiyomi.widget.sheet +import android.content.Context +import android.util.AttributeSet +import android.view.View +import androidx.viewpager.widget.ViewPager +import java.lang.reflect.Field + +/** + * From https://github.com/kafumi/android-bottomsheet-viewpager + */ +class BottomSheetViewPager @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, +) : ViewPager(context, attrs) { + + private val positionField: Field = LayoutParams::class.java.getDeclaredField("position").also { + it.isAccessible = true + } + + override fun getChildAt(index: Int): View { + val currentView = getCurrentView() ?: return super.getChildAt(index) + return if (index == 0) { + currentView + } else { + var view = super.getChildAt(index) + if (view == currentView) { + view = super.getChildAt(0) + } + return view + } + } + + private fun getCurrentView(): View? { + for (i in 0 until childCount) { + val child = super.getChildAt(i) + val lp = child.layoutParams as? LayoutParams + if (lp != null) { + val position = positionField.getInt(lp) + if (!lp.isDecor && currentItem == position) { + return child + } + } + } + return null + } + + init { + addOnPageChangeListener( + object : SimpleOnPageChangeListener() { + override fun onPageSelected(position: Int) { + requestLayout() + } + }, + ) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/sheet/TabbedBottomSheetDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/sheet/TabbedBottomSheetDialog.kt new file mode 100644 index 000000000..be2935c62 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/sheet/TabbedBottomSheetDialog.kt @@ -0,0 +1,43 @@ +package eu.kanade.tachiyomi.widget.sheet + +import android.app.Activity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.hippo.ehviewer.databinding.CommonTabbedSheetBinding +import eu.kanade.tachiyomi.widget.ViewPagerAdapter + +abstract class TabbedBottomSheetDialog(private val activity: Activity) : BaseBottomSheetDialog(activity) { + + lateinit var binding: CommonTabbedSheetBinding + + override fun createView(inflater: LayoutInflater): View { + binding = CommonTabbedSheetBinding.inflate(activity.layoutInflater) + + val adapter = LibrarySettingsSheetAdapter() + binding.pager.offscreenPageLimit = 2 + binding.pager.adapter = adapter + binding.tabs.setupWithViewPager(binding.pager) + + return binding.root + } + + abstract fun getTabViews(): List + + abstract fun getTabTitles(): List + + private inner class LibrarySettingsSheetAdapter : ViewPagerAdapter() { + + override fun createView(container: ViewGroup, position: Int): View { + return getTabViews()[position] + } + + override fun getCount(): Int { + return getTabViews().size + } + + override fun getPageTitle(position: Int): CharSequence { + return activity.resources!!.getString(getTabTitles()[position]) + } + } +} diff --git a/app/src/main/res/anim/enter_from_bottom.xml b/app/src/main/res/anim/enter_from_bottom.xml new file mode 100644 index 000000000..c9bd492a5 --- /dev/null +++ b/app/src/main/res/anim/enter_from_bottom.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/enter_from_top.xml b/app/src/main/res/anim/enter_from_top.xml new file mode 100644 index 000000000..ec0431ba0 --- /dev/null +++ b/app/src/main/res/anim/enter_from_top.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/exit_to_bottom.xml b/app/src/main/res/anim/exit_to_bottom.xml new file mode 100644 index 000000000..d5d94c795 --- /dev/null +++ b/app/src/main/res/anim/exit_to_bottom.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/exit_to_top.xml b/app/src/main/res/anim/exit_to_top.xml new file mode 100644 index 000000000..3a6f3e9db --- /dev/null +++ b/app/src/main/res/anim/exit_to_top.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_blank_24dp.xml b/app/src/main/res/drawable/ic_blank_24dp.xml new file mode 100644 index 000000000..940db069f --- /dev/null +++ b/app/src/main/res/drawable/ic_blank_24dp.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/src/main/res/drawable/ic_brightness_5_24dp.xml b/app/src/main/res/drawable/ic_brightness_5_24dp.xml new file mode 100644 index 000000000..ae338ca19 --- /dev/null +++ b/app/src/main/res/drawable/ic_brightness_5_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_check_24dp.xml b/app/src/main/res/drawable/ic_check_24dp.xml new file mode 100644 index 000000000..b268959e3 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_crop_24dp.xml b/app/src/main/res/drawable/ic_crop_24dp.xml new file mode 100644 index 000000000..f802c216e --- /dev/null +++ b/app/src/main/res/drawable/ic_crop_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_crop_off_24dp.xml b/app/src/main/res/drawable/ic_crop_off_24dp.xml new file mode 100644 index 000000000..5d234a0b0 --- /dev/null +++ b/app/src/main/res/drawable/ic_crop_off_24dp.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_expand_more_24dp.xml b/app/src/main/res/drawable/ic_expand_more_24dp.xml new file mode 100644 index 000000000..5f5ba4e44 --- /dev/null +++ b/app/src/main/res/drawable/ic_expand_more_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_overflow_24dp.xml b/app/src/main/res/drawable/ic_overflow_24dp.xml new file mode 100644 index 000000000..2ae4b83fb --- /dev/null +++ b/app/src/main/res/drawable/ic_overflow_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_reader_continuous_vertical_24dp.xml b/app/src/main/res/drawable/ic_reader_continuous_vertical_24dp.xml new file mode 100644 index 000000000..7997bba7b --- /dev/null +++ b/app/src/main/res/drawable/ic_reader_continuous_vertical_24dp.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_reader_default_24dp.xml b/app/src/main/res/drawable/ic_reader_default_24dp.xml new file mode 100644 index 000000000..3ec2d7598 --- /dev/null +++ b/app/src/main/res/drawable/ic_reader_default_24dp.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_reader_ltr_24dp.xml b/app/src/main/res/drawable/ic_reader_ltr_24dp.xml new file mode 100644 index 000000000..2c44474e9 --- /dev/null +++ b/app/src/main/res/drawable/ic_reader_ltr_24dp.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_reader_rtl_24dp.xml b/app/src/main/res/drawable/ic_reader_rtl_24dp.xml new file mode 100644 index 000000000..bce2932fc --- /dev/null +++ b/app/src/main/res/drawable/ic_reader_rtl_24dp.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_reader_vertical_24dp.xml b/app/src/main/res/drawable/ic_reader_vertical_24dp.xml new file mode 100644 index 000000000..def93b2c2 --- /dev/null +++ b/app/src/main/res/drawable/ic_reader_vertical_24dp.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_reader_webtoon_24dp.xml b/app/src/main/res/drawable/ic_reader_webtoon_24dp.xml new file mode 100644 index 000000000..b7683822d --- /dev/null +++ b/app/src/main/res/drawable/ic_reader_webtoon_24dp.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_save_24dp.xml b/app/src/main/res/drawable/ic_save_24dp.xml new file mode 100644 index 000000000..4435923ad --- /dev/null +++ b/app/src/main/res/drawable/ic_save_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_screen_lock_landscape_24dp.xml b/app/src/main/res/drawable/ic_screen_lock_landscape_24dp.xml new file mode 100644 index 000000000..f78d9a5d4 --- /dev/null +++ b/app/src/main/res/drawable/ic_screen_lock_landscape_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_screen_lock_portrait_24dp.xml b/app/src/main/res/drawable/ic_screen_lock_portrait_24dp.xml new file mode 100644 index 000000000..718ca70ba --- /dev/null +++ b/app/src/main/res/drawable/ic_screen_lock_portrait_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_screen_rotation_24dp.xml b/app/src/main/res/drawable/ic_screen_rotation_24dp.xml new file mode 100644 index 000000000..3243d5a75 --- /dev/null +++ b/app/src/main/res/drawable/ic_screen_rotation_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_24dp.xml b/app/src/main/res/drawable/ic_settings_24dp.xml new file mode 100644 index 000000000..df54c6b13 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_share_24dp.xml b/app/src/main/res/drawable/ic_share_24dp.xml new file mode 100644 index 000000000..394e3effe --- /dev/null +++ b/app/src/main/res/drawable/ic_share_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_stay_current_landscape_24dp.xml b/app/src/main/res/drawable/ic_stay_current_landscape_24dp.xml new file mode 100644 index 000000000..ac185cda4 --- /dev/null +++ b/app/src/main/res/drawable/ic_stay_current_landscape_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_stay_current_portrait_24dp.xml b/app/src/main/res/drawable/ic_stay_current_portrait_24dp.xml new file mode 100644 index 000000000..23e9f4f26 --- /dev/null +++ b/app/src/main/res/drawable/ic_stay_current_portrait_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/transparent_tabs_background.xml b/app/src/main/res/drawable/transparent_tabs_background.xml new file mode 100644 index 000000000..a503ef698 --- /dev/null +++ b/app/src/main/res/drawable/transparent_tabs_background.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_gallery.xml b/app/src/main/res/layout/activity_gallery.xml deleted file mode 100644 index cdbe51148..000000000 --- a/app/src/main/res/layout/activity_gallery.xml +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/common_tabbed_sheet.xml b/app/src/main/res/layout/common_tabbed_sheet.xml new file mode 100644 index 000000000..9b6aae965 --- /dev/null +++ b/app/src/main/res/layout/common_tabbed_sheet.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_gallery_menu.xml b/app/src/main/res/layout/dialog_gallery_menu.xml deleted file mode 100644 index 7d2d451ed..000000000 --- a/app/src/main/res/layout/dialog_gallery_menu.xml +++ /dev/null @@ -1,202 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/pref_spinner.xml b/app/src/main/res/layout/pref_spinner.xml new file mode 100644 index 000000000..c7335fa1f --- /dev/null +++ b/app/src/main/res/layout/pref_spinner.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/reader_activity.xml b/app/src/main/res/layout/reader_activity.xml new file mode 100644 index 000000000..bca67da4c --- /dev/null +++ b/app/src/main/res/layout/reader_activity.xml @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/reader_color_filter_settings.xml b/app/src/main/res/layout/reader_color_filter_settings.xml new file mode 100644 index 000000000..f851e8cad --- /dev/null +++ b/app/src/main/res/layout/reader_color_filter_settings.xml @@ -0,0 +1,269 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/reader_error.xml b/app/src/main/res/layout/reader_error.xml new file mode 100644 index 000000000..27e7c0df6 --- /dev/null +++ b/app/src/main/res/layout/reader_error.xml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/app/src/main/res/layout/reader_general_settings.xml b/app/src/main/res/layout/reader_general_settings.xml new file mode 100644 index 000000000..1d9fe94a3 --- /dev/null +++ b/app/src/main/res/layout/reader_general_settings.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/reader_page_sheet.xml b/app/src/main/res/layout/reader_page_sheet.xml new file mode 100644 index 000000000..d05820108 --- /dev/null +++ b/app/src/main/res/layout/reader_page_sheet.xml @@ -0,0 +1,38 @@ + + + + + + + + diff --git a/app/src/main/res/layout/reader_pager_settings.xml b/app/src/main/res/layout/reader_pager_settings.xml new file mode 100644 index 000000000..3da1b46be --- /dev/null +++ b/app/src/main/res/layout/reader_pager_settings.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/reader_reading_mode_settings.xml b/app/src/main/res/layout/reader_reading_mode_settings.xml new file mode 100644 index 000000000..eeb733d47 --- /dev/null +++ b/app/src/main/res/layout/reader_reading_mode_settings.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/reader_webtoon_settings.xml b/app/src/main/res/layout/reader_webtoon_settings.xml new file mode 100644 index 000000000..ff0ddb766 --- /dev/null +++ b/app/src/main/res/layout/reader_webtoon_settings.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-sw720dp/dimens.xml b/app/src/main/res/values-sw720dp/dimens.xml new file mode 100644 index 000000000..0ec53bce7 --- /dev/null +++ b/app/src/main/res/values-sw720dp/dimens.xml @@ -0,0 +1,4 @@ + + 480dp + 24dp + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 92102e72e..ff8120f40 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -309,4 +309,110 @@ 1 2 + + + + + + + + + + + + @string/label_default + @string/left_to_right_viewer + @string/right_to_left_viewer + @string/vertical_viewer + @string/webtoon_viewer + @string/vertical_plus_viewer + + + + @string/black_background + @string/gray_background + @string/white_background + @string/automatic_background + + + + 1 + 2 + 0 + 3 + + + + @string/label_default + @string/filter_mode_multiply + @string/filter_mode_screen + + + + @string/label_default + @string/rotation_free + @string/rotation_portrait + @string/rotation_landscape + @string/rotation_force_portrait + @string/rotation_force_landscape + + + + @string/label_default + @string/l_nav + @string/kindlish_nav + @string/edge_nav + @string/right_and_left_nav + @string/disabled_nav + + + + @string/tapping_inverted_none + @string/tapping_inverted_horizontal + @string/tapping_inverted_vertical + @string/tapping_inverted_both + + + + @string/label_default + @string/l_nav + @string/kindlish_nav + @string/edge_nav + @string/right_and_left_nav + @string/disabled_nav + + + + @string/scale_type_fit_screen + @string/scale_type_stretch + @string/scale_type_fit_width + @string/scale_type_fit_height + @string/scale_type_original_size + @string/scale_type_smart_fit + + + + @string/zoom_start_automatic + @string/zoom_start_left + @string/zoom_start_right + @string/zoom_start_center + + + + @string/webtoon_side_padding_0 + @string/webtoon_side_padding_5 + @string/webtoon_side_padding_10 + @string/webtoon_side_padding_15 + @string/webtoon_side_padding_20 + @string/webtoon_side_padding_25 + + + + 0 + 5 + 10 + 15 + 20 + 25 + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 67f72cd24..e0e16601a 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -135,4 +135,10 @@ + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index cbf754037..f8df8163b 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -21,4 +21,13 @@ #1f000000 #20000000 #20ffffff + + + #CB84E296 + #CCFF7733 + #CC95818D + #CCA6CFD5 + #CC7D1128 + + #202125 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index edc0cdd1c..46eb9395b 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -123,4 +123,7 @@ 56sp 24sp + 0dp + 16dp + diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml index 7bcca4526..155242912 100644 --- a/app/src/main/res/values/ids.xml +++ b/app/src/main/res/values/ids.xml @@ -30,4 +30,6 @@ + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 06befc6ee..802329b11 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -696,4 +696,124 @@ Password can\'t be empty Thumb size in thumb mode Account + + + Settings + Default rotation type + Crop borders + Reading mode + Menu + Prev + Next + Left + Right + The image couldn\'t be loaded + Retry + Default + Free + Portrait + Reverse portrait + Landscape + Locked portrait + Locked landscape + Left to right + Right to left + Vertical + Webtoon + Continuous vertical + Paged + Fullscreen + Show tap zones overlay + Briefly show when reader is opened + Split wide pages + Invert split page placement + If the placement of the split wide pages don\'t match reading direction + Split tall images (BETA) + Show content in cutout area + Animate page transitions + Double tap animation speed + Show page number + Show reading mode + Briefly show current mode when reader is opened + 32-bit color + Reduces banding, but may impact performance + Custom brightness + Grayscale + Inverted + Custom color filter + Color filter blend mode + Overlay + Multiply + Screen + Dodge / Lighten + Burn / Darken + Keep screen on + Skip chapters marked read + Skip filtered chapters + Navigation + Volume keys + Invert volume keys + Invert tap zones + None + Horizontal + Vertical + Both + Actions + Show on long tap + Save pages into separate folders + Creates folders according to entries\' title + Background color + White + Gray + Black + Automatic + Default reading mode + L shaped + Kindle-ish + Edge + Right and Left + Disabled + Tap zones + Scale type + Fit screen + Stretch + Fit width + Fit height + Original size + Smart fit + Pan wide images when tapping + Zoom landscape image + Zoom start position + Automatic + Left + Right + Center + No animation + Normal + Fast + Rotation type + R + G + B + A + Always show chapter transition + Reading mode + Reading + Side padding + None + 5% + 10% + 15% + 20% + 25% + Sensitivity for hiding menu on scroll + Highest + High + Low + Lowest + General + Custom filter + Share + Save + For this series diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 7c73b8439..169bbe2a2 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -63,4 +63,9 @@ + + +