From 6f89adc7ebefa76367d065bab6152f6771995dd8 Mon Sep 17 00:00:00 2001 From: Artem Umerov Date: Wed, 28 Aug 2024 21:35:54 +0300 Subject: [PATCH] 70 70 --- app_fenrir/build.gradle | 4 - .../kotlin/dev/ragnarok/fenrir/Constants.kt | 6 +- .../dev/ragnarok/fenrir/FcmListenerService.kt | 15 +- .../kotlin/dev/ragnarok/fenrir/Includes.kt | 6 - .../ragnarok/fenrir/activity/MainActivity.kt | 2 +- .../fenrir/api/AbsVKApiInterceptor.kt | 2 +- .../kotlin/dev/ragnarok/fenrir/api/Auth.kt | 1 + .../fenrir/api/NeedValidationException.kt | 1 + .../dev/ragnarok/fenrir/api/impl/AuthApi.kt | 49 +- .../fenrir/api/interfaces/IAuthApi.kt | 13 +- .../ragnarok/fenrir/api/model/AnonymToken.kt | 19 + .../fenrir/api/model/LoginResponse.kt | 3 + .../ragnarok/fenrir/api/model/RefreshToken.kt | 3 + .../fenrir/api/services/IAuthService.kt | 27 + .../dialog/directauth/DirectAuthPresenter.kt | 3 + .../fenrir/domain/impl/WallsRepository.kt | 4 + .../fenrir/domain/mappers/Dto2Model.kt | 3 + .../fenrir/fragment/PreferencesFragment.kt | 2 +- .../fragment/accounts/AccountsPresenter.kt | 7 +- .../fenrir/longpoll/NotificationHelper.kt | 4 - .../model/catalog_v2_audio/CatalogV2List.kt | 14 +- .../ragnarok/fenrir/push/IDeviceIdProvider.kt | 5 - .../fenrir/push/IPushRegistrationResolver.kt | 5 +- .../fenrir/push/PushRegistrationResolver.kt | 35 +- .../fenrir/settings/AccountsSettings.kt | 2 +- .../fenrir/settings/backup/SettingsBackup.kt | 1 + .../kotlin/dev/ragnarok/fenrir/util/Utils.kt | 48 +- app_filegallery/build.gradle | 4 - build.gradle | 23 +- gradle/wrapper/gradle-wrapper.properties | 2 +- libfenrir/ffmpeg.sh | 2 +- .../animation/libyuv/include/libyuv/convert.h | 29 + .../animation/libyuv/include/libyuv/cpu_id.h | 4 +- .../libyuv/include/libyuv/planar_functions.h | 4 + .../libyuv/include/libyuv/rotate_row.h | 6 +- .../jni/animation/libyuv/include/libyuv/row.h | 19 +- .../libyuv/include/libyuv/scale_row.h | 4 + .../animation/libyuv/include/libyuv/version.h | 2 +- .../jni/animation/libyuv/source/convert.cc | 215 +++- .../libyuv/source/planar_functions.cc | 8 + .../jni/animation/libyuv/source/row_any.cc | 13 +- .../jni/animation/libyuv/source/row_gcc.cc | 44 +- .../jni/animation/libyuv/source/row_neon.cc | 35 +- .../jni/animation/libyuv/source/row_neon64.cc | 47 +- .../jni/animation/libyuv/source/row_rvv.cc | 48 +- .../jni/animation/libyuv/source/row_sve.cc | 20 +- .../jni/animation/libyuv/source/scale_neon.cc | 86 +- .../animation/libyuv/source/scale_neon64.cc | 52 - .../jni/animation/libyuv/source/scale_uv.cc | 6 +- .../main/jni/animation/rlottie/inc/rlottie.h | 8 +- .../jni/animation/rlottie/rlottie-master.zip | Bin 4303418 -> 4303852 bytes .../rlottie/src/lottie/lottiefiltermodel.h | 19 + .../rlottie/src/lottie/lottieitem.cpp | 28 +- .../animation/rlottie/src/lottie/lottieitem.h | 8 +- .../rlottie/src/lottie/lottiemodel.h | 14 + .../rlottie/src/lottie/rapidjson/reader.h | 4 +- .../main/jni/animation/thorvg/inc/thorvg.h | 124 +-- .../animation/thorvg/src/common/tvgArray.h | 2 +- .../thorvg/src/common/tvgCompressor.cpp | 2 + .../animation/thorvg/src/common/tvgInlist.h | 2 +- .../animation/thorvg/src/common/tvgMath.cpp | 39 +- .../jni/animation/thorvg/src/common/tvgMath.h | 4 +- .../src/renderer/sw_engine/tvgSwCommon.h | 4 +- .../src/renderer/sw_engine/tvgSwImage.cpp | 54 +- .../src/renderer/sw_engine/tvgSwRaster.cpp | 22 +- .../renderer/sw_engine/tvgSwRasterTexmap.h | 108 +- .../src/renderer/sw_engine/tvgSwRenderer.cpp | 86 +- .../src/renderer/sw_engine/tvgSwRenderer.h | 3 +- .../src/renderer/sw_engine/tvgSwRle.cpp | 79 -- .../thorvg/src/renderer/tvgAccessor.cpp | 41 +- .../thorvg/src/renderer/tvgLoadModule.h | 3 +- .../thorvg/src/renderer/tvgPaint.cpp | 42 +- .../animation/thorvg/src/renderer/tvgPaint.h | 25 +- .../thorvg/src/renderer/tvgPicture.cpp | 34 +- .../thorvg/src/renderer/tvgPicture.h | 61 +- .../animation/thorvg/src/renderer/tvgRender.h | 22 +- .../animation/thorvg/src/renderer/tvgScene.h | 18 +- .../thorvg/src/renderer/tvgShape.cpp | 2 +- .../animation/thorvg/src/renderer/tvgShape.h | 16 + .../animation/thorvg/src/renderer/tvgText.cpp | 13 +- .../animation/thorvg/src/renderer/tvgText.h | 63 +- .../main/jni/audio/taglib/toolkit/taglib.h | 2 +- material/build.gradle | 2 + .../material/appbar/res/values/tokens.xml | 2 +- .../material/badge/res/values/tokens.xml | 2 +- .../bottomappbar/res/values/tokens.xml | 2 +- .../bottomnavigation/res/values/tokens.xml | 2 +- .../bottomsheet/res/values/tokens.xml | 2 +- .../material/button/MaterialButton.java | 170 +++- .../material/button/MaterialButtonGroup.java | 763 ++++++++++++++ .../button/MaterialButtonToggleGroup.java | 487 +-------- .../button/res-public/values/public.xml | 9 +- .../material/button/res/values/attrs.xml | 30 +- .../button/res/values/icon_btn_tokens.xml | 2 +- .../material/button/res/values/styles.xml | 16 +- .../material/button/res/values/tokens.xml | 2 +- .../m3_button_group_child_size_change.xml} | 16 +- .../material/card/res/values/tokens.xml | 2 +- .../material/checkbox/res/values/tokens.xml | 2 +- .../material/chip/res/values/tokens.xml | 2 +- .../m3_ref_palette_dynamic_neutral12.xml | 2 +- .../m3_ref_palette_dynamic_neutral17.xml | 2 +- .../m3_ref_palette_dynamic_neutral22.xml | 2 +- .../m3_ref_palette_dynamic_neutral24.xml | 2 +- .../m3_ref_palette_dynamic_neutral4.xml | 2 +- .../m3_ref_palette_dynamic_neutral6.xml | 2 +- .../m3_ref_palette_dynamic_neutral87.xml | 2 +- .../m3_ref_palette_dynamic_neutral92.xml | 2 +- .../m3_ref_palette_dynamic_neutral94.xml | 2 +- .../m3_ref_palette_dynamic_neutral96.xml | 2 +- .../m3_ref_palette_dynamic_neutral98.xml | 2 +- .../material/color/res/values-v31/tokens.xml | 8 +- .../material/color/res/values-v34/tokens.xml | 2 +- .../material/color/res/values-v35/tokens.xml | 6 +- .../material/color/res/values/tokens.xml | 10 +- .../material/datepicker/res/values/tokens.xml | 2 +- .../{values-land => values-h480dp}/dimens.xml | 4 +- .../material/dialog/res/values/dimens.xml | 9 +- .../dialog/res/values/themes_base.xml | 44 +- .../material/dialog/res/values/tokens.xml | 2 +- .../material/divider/res/values/tokens.xml | 2 +- .../material/elevation/res/values/tokens.xml | 2 +- .../ExtendedFloatingActionButton.java | 5 + .../FloatingActionButton.java | 7 + .../res/values/efab_tokens.xml | 2 +- .../res/values/fab_tokens.xml | 2 +- .../loadingindicator/LoadingIndicator.java | 314 ++++++ .../LoadingIndicatorAnimatorDelegate.java | 210 ++++ .../LoadingIndicatorDrawable.java | 227 +++++ .../LoadingIndicatorDrawingDelegate.java | 165 ++++ .../LoadingIndicatorSpec.java | 119 +++ .../res-public/values/public.xml | 25 + .../loadingindicator/res/values/attrs.xml | 27 + .../loadingindicator/res/values/dimens.xml | 20 + .../loadingindicator/res/values/strings.xml | 22 + .../loadingindicator/res/values/styles.xml | 31 + .../materialswitch/res/values/tokens.xml | 2 +- .../material/menu/res/values/tokens.xml | 7 +- .../material/motion/res/values/tokens.xml | 2 +- .../navigation/NavigationBarMenu.java | 4 - .../navigation/NavigationBarMenuView.java | 57 +- .../navigation/NavigationBarView.java | 7 +- .../material/navigation/NavigationView.java | 42 +- .../navigation/res-public/values/public.xml | 2 + .../material/navigation/res/values/attrs.xml | 6 + .../material/navigation/res/values/tokens.xml | 2 +- .../navigationrail/res/values/tokens.xml | 2 +- .../res-public/values/public.xml | 2 - .../progressindicator/res/values/attrs.xml | 4 +- .../progressindicator/res/values/tokens.xml | 2 +- .../radiobutton/res/values/tokens.xml | 2 +- .../resources/res-public/values/public.xml | 4 + .../resources/res/values-v21/tokens.xml | 2 +- .../resources/res/values-v28/tokens.xml | 2 +- .../material/resources/res/values/attrs.xml | 4 + .../material/resources/res/values/tokens.xml | 2 +- .../material/search/res/values/tokens.xml | 2 +- .../material/shape/MaterialShapeDrawable.java | 6 +- .../material/shape/MaterialShapes.java | 935 ++++++++++++++++++ .../material/shape/StateListSizeChange.java | 20 +- .../material/shape/res/values/tokens.xml | 2 +- .../material/sidesheet/res/values/tokens.xml | 2 +- .../android/material/slider/BaseSlider.java | 5 + .../m3_slider_active_tick_marks_color.xml | 8 +- .../m3_slider_inactive_tick_marks_color.xml | 8 +- .../material/slider/res/values/tokens.xml | 28 +- .../snackbar/BaseTransientBottomBar.java | 6 +- .../material/snackbar/res/values/tokens.xml | 2 +- .../material/tabs/res/values/tokens.xml | 2 +- .../material/textfield/res/values/styles.xml | 8 +- .../res/values/tokens_dropdown_menu.xml | 2 +- .../textfield/res/values/tokens_textfield.xml | 2 +- .../material/theme/res/values/themes_base.xml | 44 +- .../material/timepicker/res/values/tokens.xml | 2 +- .../material/tooltip/res/values/tokens.xml | 2 +- .../typography/res/values-v21/styles.xml | 42 - .../typography/res/values-v21/tokens.xml | 185 ---- .../material/typography/res/values/attrs.xml | 30 + .../material/typography/res/values/styles.xml | 169 +++- .../material/typography/res/values/tokens.xml | 42 +- recyclerview/build.gradle | 4 +- 181 files changed, 4664 insertions(+), 1762 deletions(-) create mode 100644 app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/model/AnonymToken.kt delete mode 100644 app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/push/IDeviceIdProvider.kt create mode 100644 material/java/com/google/android/material/button/MaterialButtonGroup.java rename material/java/com/google/android/material/{dialog/res/values-small/dimens.xml => button/res/xml/m3_button_group_child_size_change.xml} (62%) rename material/java/com/google/android/material/dialog/res/{values-land => values-h480dp}/dimens.xml (86%) create mode 100644 material/java/com/google/android/material/loadingindicator/LoadingIndicator.java create mode 100644 material/java/com/google/android/material/loadingindicator/LoadingIndicatorAnimatorDelegate.java create mode 100644 material/java/com/google/android/material/loadingindicator/LoadingIndicatorDrawable.java create mode 100644 material/java/com/google/android/material/loadingindicator/LoadingIndicatorDrawingDelegate.java create mode 100644 material/java/com/google/android/material/loadingindicator/LoadingIndicatorSpec.java create mode 100644 material/java/com/google/android/material/loadingindicator/res-public/values/public.xml create mode 100644 material/java/com/google/android/material/loadingindicator/res/values/attrs.xml create mode 100644 material/java/com/google/android/material/loadingindicator/res/values/dimens.xml create mode 100644 material/java/com/google/android/material/loadingindicator/res/values/strings.xml create mode 100644 material/java/com/google/android/material/loadingindicator/res/values/styles.xml create mode 100644 material/java/com/google/android/material/shape/MaterialShapes.java delete mode 100644 material/java/com/google/android/material/typography/res/values-v21/styles.xml delete mode 100644 material/java/com/google/android/material/typography/res/values-v21/tokens.xml diff --git a/app_fenrir/build.gradle b/app_fenrir/build.gradle index 3d8eb6d98..c97fda905 100644 --- a/app_fenrir/build.gradle +++ b/app_fenrir/build.gradle @@ -79,14 +79,10 @@ android { minifyEnabled = true shrinkResources = true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - - signingConfig signingConfigs.fenrir } debug { minifyEnabled = false shrinkResources = false - - signingConfig signingConfigs.fenrir } } diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/Constants.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/Constants.kt index 4f00509f3..dda26d504 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/Constants.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/Constants.kt @@ -18,8 +18,8 @@ object Constants { const val FILE_PROVIDER_AUTHORITY: String = "${BuildConfig.APPLICATION_ID}.file_provider" const val VK_ANDROID_APP_VERSION_NAME = "8.15" const val VK_ANDROID_APP_VERSION_CODE = "15271" - const val KATE_APP_VERSION_NAME = "114 lite" - const val KATE_APP_VERSION_CODE = "560" + const val KATE_APP_VERSION_NAME = "118 lite" + const val KATE_APP_VERSION_CODE = "566" const val IOS_APP_VERSION_CODE = "3893" @@ -51,4 +51,6 @@ object Constants { const val LONGPOLL_TIMEOUT = 45L const val LONGPOLL_WAIT = 25L const val PICASSO_TIMEOUT = 15L + + val CATALOG_V2_IGNORE_SECTIONS = arrayOf("podcasts", "radiostations") } diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/FcmListenerService.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/FcmListenerService.kt index 00479c861..2eb31d713 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/FcmListenerService.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/FcmListenerService.kt @@ -34,7 +34,7 @@ class FcmListenerService : FirebaseMessagingService() { override fun onNewToken(s: String) { super.onNewToken(s) pushRegistrationResolver - .resolvePushRegistration() + .resolvePushRegistration(Settings.get().accounts().current, applicationContext) .hiddenIO() } @@ -52,7 +52,7 @@ class FcmListenerService : FirebaseMessagingService() { return } val registrationResolver = pushRegistrationResolver - if (!registrationResolver.canReceivePushNotification()) { + if (!registrationResolver.canReceivePushNotification(accountId)) { Logger.d(TAG, "Invalid push registration on VK") return } @@ -115,22 +115,13 @@ class FcmListenerService : FirebaseMessagingService() { PushType.BIRTHDAY -> BirthdayFCMMessage.fromRemoteMessage(message) ?.notify(context, accountId) - PushType.VALIDATE_DEVICE -> NotificationHelper.showSimpleNotification( + PushType.SHOW_MESSAGE, PushType.VALIDATE_DEVICE -> NotificationHelper.showSimpleNotification( context, message.data["body"], message.data["title"], - null, message.data["url"] ) - PushType.SHOW_MESSAGE -> NotificationHelper.showSimpleNotification( - context, - message.data["body"], - message.data["title"], - null, - null - ) - PushType.MENTION -> MentionMessage.fromRemoteMessage(message) .notify(context, accountId) diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/Includes.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/Includes.kt index a7732df4b..bcb404720 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/Includes.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/Includes.kt @@ -20,7 +20,6 @@ import dev.ragnarok.fenrir.media.story.AppStoryPlayerFactory import dev.ragnarok.fenrir.media.story.IStoryPlayerFactory import dev.ragnarok.fenrir.media.voice.IVoicePlayerFactory import dev.ragnarok.fenrir.media.voice.VoicePlayerFactory -import dev.ragnarok.fenrir.push.IDeviceIdProvider import dev.ragnarok.fenrir.push.IPushRegistrationResolver import dev.ragnarok.fenrir.push.PushRegistrationResolver import dev.ragnarok.fenrir.settings.IProxySettings @@ -29,7 +28,6 @@ import dev.ragnarok.fenrir.settings.ProxySettingsImpl import dev.ragnarok.fenrir.settings.SettingsImpl import dev.ragnarok.fenrir.upload.IUploadManager import dev.ragnarok.fenrir.upload.UploadManagerImpl -import dev.ragnarok.fenrir.util.Utils object Includes { val proxySettings: IProxySettings by lazy { @@ -50,10 +48,6 @@ object Includes { val pushRegistrationResolver: IPushRegistrationResolver by lazy { PushRegistrationResolver( - object : IDeviceIdProvider { - override val deviceId: String - get() = Utils.getDeviceId(provideApplicationContext()) - }, settings, networkInterfaces ) diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/MainActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/MainActivity.kt index 701a0f67f..de1d8bade 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/MainActivity.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/MainActivity.kt @@ -621,7 +621,7 @@ open class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks, OnSect return } mCompositeJob.add( - pushRegistrationResolver.resolvePushRegistration() + pushRegistrationResolver.resolvePushRegistration(mAccountId, this) .hiddenIO() ) diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/AbsVKApiInterceptor.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/AbsVKApiInterceptor.kt index 32533cdc4..a92fd2a32 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/AbsVKApiInterceptor.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/AbsVKApiInterceptor.kt @@ -77,7 +77,7 @@ abstract class AbsVKApiInterceptor(private val version: String) : if (!hasDeviceId) { formBuilder.add( "device_id", - Utils.getDeviceId(provideApplicationContext()) + Utils.getDeviceId(type, provideApplicationContext()) ) } return chain.proceed( diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/Auth.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/Auth.kt index 9200a5eee..42dbc9740 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/Auth.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/Auth.kt @@ -29,6 +29,7 @@ object Auth { Constants.DEVICE_COUNTRY_CODE, "utf-8" ) + "&device_id=" + URLEncoder.encode( Utils.getDeviceId( + Constants.DEFAULT_ACCOUNT_TYPE, provideApplicationContext() ), "utf-8" )) diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/NeedValidationException.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/NeedValidationException.kt index 633246bbd..df01a1c31 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/NeedValidationException.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/NeedValidationException.kt @@ -1,6 +1,7 @@ package dev.ragnarok.fenrir.api class NeedValidationException( + val phone: String?, val validationType: String?, val validationURL: String?, val sid: String?, diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/impl/AuthApi.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/impl/AuthApi.kt index 77c370a0f..8799eaeb5 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/impl/AuthApi.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/impl/AuthApi.kt @@ -1,5 +1,6 @@ package dev.ragnarok.fenrir.api.impl +import dev.ragnarok.fenrir.Constants import dev.ragnarok.fenrir.Constants.DEVICE_COUNTRY_CODE import dev.ragnarok.fenrir.Includes.provideApplicationContext import dev.ragnarok.fenrir.api.ApiException @@ -8,6 +9,7 @@ import dev.ragnarok.fenrir.api.CaptchaNeedException import dev.ragnarok.fenrir.api.IDirectLoginServiceProvider import dev.ragnarok.fenrir.api.NeedValidationException import dev.ragnarok.fenrir.api.interfaces.IAuthApi +import dev.ragnarok.fenrir.api.model.AnonymToken import dev.ragnarok.fenrir.api.model.LoginResponse import dev.ragnarok.fenrir.api.model.VKApiValidationResponse import dev.ragnarok.fenrir.api.model.response.BaseResponse @@ -50,6 +52,7 @@ class AuthApi(private val service: IDirectLoginServiceProvider) : IAuthApi { captchaKey, if (forceSms) 1 else null, getDeviceId( + Constants.DEFAULT_ACCOUNT_TYPE, provideApplicationContext() ), if (libverify_support) 1 else null, @@ -66,6 +69,7 @@ class AuthApi(private val service: IDirectLoginServiceProvider) : IAuthApi { "need_validation".equals(response.error, ignoreCase = true) -> { throw NeedValidationException( + username, response.validationType, response.redirect_uri, response.validation_sid, @@ -87,19 +91,31 @@ class AuthApi(private val service: IDirectLoginServiceProvider) : IAuthApi { } override fun validatePhone( + phone: String?, apiId: Int, clientId: Int, clientSecret: String?, sid: String?, v: String?, - libverify_support: Boolean + libverify_support: Boolean, + allow_callreset: Boolean ): Flow { return service.provideAuthService() .flatMapConcat { it.validatePhone( - apiId, clientId, clientSecret, sid, v, getDeviceId( + phone, + apiId, + clientId, + clientSecret, + sid, + v, + getDeviceId( + Constants.DEFAULT_ACCOUNT_TYPE, provideApplicationContext() - ), if (libverify_support) 1 else null, DEVICE_COUNTRY_CODE + ), + if (libverify_support) 1 else null, + if (allow_callreset) 1 else null, + DEVICE_COUNTRY_CODE ) .map(extractResponseWithErrorHandling()) } @@ -140,6 +156,33 @@ class AuthApi(private val service: IDirectLoginServiceProvider) : IAuthApi { } } + override fun get_anonym_token( + apiId: Int, + clientId: Int, + clientSecret: String?, + v: String?, + device_id: String? + ): Flow { + return service.provideAuthService() + .flatMapConcat { + it.get_anonym_token( + apiId, + clientId, + clientSecret, + v, + device_id, + DEVICE_COUNTRY_CODE + ) + .map { s -> + if (s.error != null) { + throw AuthException(s.error.orEmpty(), s.errorDescription) + } else { + s + } + } + } + } + companion object { fun extractResponseWithErrorHandling(): (BaseResponse) -> T = { err -> err.error?.let { throw ApiException(it) } diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/interfaces/IAuthApi.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/interfaces/IAuthApi.kt index 9d2d69ff4..e9bbe8c57 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/interfaces/IAuthApi.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/interfaces/IAuthApi.kt @@ -1,5 +1,6 @@ package dev.ragnarok.fenrir.api.interfaces +import dev.ragnarok.fenrir.api.model.AnonymToken import dev.ragnarok.fenrir.api.model.LoginResponse import dev.ragnarok.fenrir.api.model.VKApiValidationResponse import dev.ragnarok.fenrir.api.model.response.VKUrlResponse @@ -23,12 +24,14 @@ interface IAuthApi { ): Flow fun validatePhone( + phone: String?, apiId: Int, clientId: Int, clientSecret: String?, sid: String?, v: String?, - libverify_support: Boolean + libverify_support: Boolean, + allow_callreset: Boolean ): Flow fun authByExchangeToken( @@ -42,4 +45,12 @@ interface IAuthApi { gaid: String?, v: String? ): Flow + + fun get_anonym_token( + apiId: Int, + clientId: Int, + clientSecret: String?, + v: String?, + device_id: String? + ): Flow } \ No newline at end of file diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/model/AnonymToken.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/model/AnonymToken.kt new file mode 100644 index 000000000..b5c1bc0f2 --- /dev/null +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/model/AnonymToken.kt @@ -0,0 +1,19 @@ +package dev.ragnarok.fenrir.api.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +class AnonymToken { + @SerialName("token") + var token: String? = null + + @SerialName("expired_at") + var expired_at: Long = 0 + + @SerialName("error") + var error: String? = null + + @SerialName("error_description") + var errorDescription: String? = null +} \ No newline at end of file diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/model/LoginResponse.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/model/LoginResponse.kt index 3dced12e6..5290ded5e 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/model/LoginResponse.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/model/LoginResponse.kt @@ -36,4 +36,7 @@ class LoginResponse { @SerialName("validation_sid") var validation_sid: String? = null + + @SerialName("expired_at") + var expired_at: Long = 0 } \ No newline at end of file diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/model/RefreshToken.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/model/RefreshToken.kt index 473cad6ce..768e88e75 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/model/RefreshToken.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/model/RefreshToken.kt @@ -7,4 +7,7 @@ import kotlinx.serialization.Serializable class RefreshToken { @SerialName("token") var token: String? = null + + @SerialName("expired_at") + var expired_at: Long = 0 } \ No newline at end of file diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/services/IAuthService.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/services/IAuthService.kt index 55c5480b3..f39b65614 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/services/IAuthService.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/services/IAuthService.kt @@ -1,5 +1,6 @@ package dev.ragnarok.fenrir.api.services +import dev.ragnarok.fenrir.api.model.AnonymToken import dev.ragnarok.fenrir.api.model.LoginResponse import dev.ragnarok.fenrir.api.model.VKApiValidationResponse import dev.ragnarok.fenrir.api.model.response.BaseResponse @@ -82,6 +83,7 @@ class IAuthService : IServiceRest() { } fun validatePhone( + phone: String?, apiId: Int, clientId: Int, clientSecret: String?, @@ -89,12 +91,15 @@ class IAuthService : IServiceRest() { v: String?, device_id: String?, libverify_support: Int?, + allow_callreset: Int?, lang: String? ): Flow> { return rest.request( "auth.validatePhone", form( "libverify_support" to libverify_support, + "allow_callreset" to allow_callreset, + "phone" to phone, "api_id" to apiId, "client_id" to clientId, "client_secret" to clientSecret, @@ -108,6 +113,28 @@ class IAuthService : IServiceRest() { ) } + fun get_anonym_token( + apiId: Int, + clientId: Int, + clientSecret: String?, + v: String?, + device_id: String?, + lang: String? + ): Flow { + return rest.request( + "get_anonym_token", + form( + "api_id" to apiId, + "client_id" to clientId, + "client_secret" to clientSecret, + "v" to v, + "device_id" to device_id, + "lang" to lang, + "https" to 1 + ), AnonymToken.serializer() + ) + } + /* fun refreshTokens( clientId: Int, diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/dialog/directauth/DirectAuthPresenter.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/dialog/directauth/DirectAuthPresenter.kt index 00538ac61..3e701b50f 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/dialog/directauth/DirectAuthPresenter.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/dialog/directauth/DirectAuthPresenter.kt @@ -90,6 +90,7 @@ class DirectAuthPresenter(savedInstanceState: Bundle?) : } else { val type = t.validationType val sid = t.sid + val phone = t.phone when { "2fa_sms".equals(type, ignoreCase = true) || "2fa_libverify".equals( type, @@ -114,11 +115,13 @@ class DirectAuthPresenter(savedInstanceState: Bundle?) : if (!sid.isNullOrEmpty() && requireSmsCode) { appendJob(networker.vkAuth() .validatePhone( + phone, Constants.API_ID, Constants.API_ID, Constants.SECRET, sid, Constants.AUTH_API_VERSION, + true, true ) .delayedFlow(1000) diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/domain/impl/WallsRepository.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/domain/impl/WallsRepository.kt index cfc1befcb..0b4235853 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/domain/impl/WallsRepository.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/domain/impl/WallsRepository.kt @@ -177,7 +177,11 @@ class WallsRepository( val owners = transformOwners(response.profiles, response.groups) val dtos = listEmptyIfNull(response.posts) val ids = VKOwnIds() + for (dto in dtos) { + if (dto.owner_id == 0L) { + continue + } ids.append(dto) } ownersRepository diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/domain/mappers/Dto2Model.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/domain/mappers/Dto2Model.kt index 1ac0ba408..b13d7eb0d 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/domain/mappers/Dto2Model.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/domain/mappers/Dto2Model.kt @@ -1328,6 +1328,9 @@ object Dto2Model { fun transformPosts(dtos: Collection, bundle: IOwnersBundle): List { val posts: MutableList = ArrayList(safeCountOf(dtos)) for (dto in dtos) { + if (dto.owner_id == 0L) { + continue + } posts.add(transform(dto, bundle)) } return posts diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/fragment/PreferencesFragment.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/fragment/PreferencesFragment.kt index 8518f5be9..73465e557 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/fragment/PreferencesFragment.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/fragment/PreferencesFragment.kt @@ -2111,7 +2111,7 @@ class PreferencesFragment : AbsPreferencesFragment(), PreferencesAdapter.OnScree view.findViewById(R.id.item_user_agent) .setText(UserAgentTool.USER_AGENT_CURRENT_ACCOUNT) view.findViewById(R.id.item_device_id) - .setText(Utils.getDeviceId(requireActivity())) + .setText(Utils.getDeviceId(Settings.get().accounts().currentType, requireActivity())) view.findViewById(R.id.item_fcm_token).setText(pushToken()) view.findViewById(R.id.item_access_token) .setText(Settings.get().accounts().currentAccessToken) diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/fragment/accounts/AccountsPresenter.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/fragment/accounts/AccountsPresenter.kt index 224ed9f11..337435d75 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/fragment/accounts/AccountsPresenter.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/fragment/accounts/AccountsPresenter.kt @@ -350,14 +350,15 @@ class AccountsPresenter(savedInstanceState: Bundle?) : try { val root = JsonObjectBuilder() val owner = Users?.getById(user_id) + val type = Settings.get().accounts().getType(user_id) root.put("api_ver", Constants.API_VERSION) root.put("user_name", owner?.fullName) root.put("user_id", user_id) - root.put("type", Settings.get().accounts().getType(user_id)) + root.put("type", type) root.put("domain", owner?.domain) root.put("exchange_token", token) root.put("avatar", owner?.maxSquareAvatar) - root.put("device_id", Utils.getDeviceId(context)) + root.put("device_id", Utils.getDeviceId(type, context)) root.put("sak_version", "1.102") root.put("last_access_token", Settings.get().accounts().getAccessToken(user_id)) val login = Settings.get().accounts().getLogin(user_id) @@ -672,7 +673,7 @@ class AccountsPresenter(savedInstanceState: Bundle?) : .add("lang", Constants.DEVICE_COUNTRY_CODE) .add("https", "1") .add( - "device_id", Utils.getDeviceId(Includes.provideApplicationContext()) + "device_id", Utils.getDeviceId(type, Includes.provideApplicationContext()) ) return Includes.networkInterfaces.getVkRestProvider().provideRawHttpClient(type, null) .flatMapConcat { client -> diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/longpoll/NotificationHelper.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/longpoll/NotificationHelper.kt index d5afe7687..32ba2f453 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/longpoll/NotificationHelper.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/longpoll/NotificationHelper.kt @@ -512,7 +512,6 @@ object NotificationHelper { context: Context, body: String?, pTitle: String?, - type: String?, url: String? ) { var title = pTitle @@ -526,9 +525,6 @@ object NotificationHelper { nManager.createNotificationChannel(getChatMessageChannel(context)) nManager.createNotificationChannel(getGroupChatMessageChannel(context)) } - if (!type.isNullOrEmpty()) { - title += ", Type: $type" - } val builder = NotificationCompat.Builder(context, chatMessageChannelId) .setSmallIcon(R.drawable.client_round) .setContentText(text) diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/model/catalog_v2_audio/CatalogV2List.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/model/catalog_v2_audio/CatalogV2List.kt index 5dc101216..50f851dae 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/model/catalog_v2_audio/CatalogV2List.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/model/catalog_v2_audio/CatalogV2List.kt @@ -2,6 +2,7 @@ package dev.ragnarok.fenrir.model.catalog_v2_audio import android.os.Parcel import android.os.Parcelable +import dev.ragnarok.fenrir.Constants import dev.ragnarok.fenrir.api.model.catalog_v2_audio.VKApiCatalogV2ListResponse import dev.ragnarok.fenrir.model.catalog_v2_audio.CatalogV2SortListCategory.Companion.TYPE_CATALOG import dev.ragnarok.fenrir.orZero @@ -21,11 +22,16 @@ class CatalogV2List : Parcelable { default_section = object_api.catalog?.default_section sections = ArrayList(object_api.catalog?.sections?.size.orZero()) for (i in object_api.catalog?.sections.orEmpty()) { - if (i.url?.contains("?section=podcasts") == true - ) { - continue + var need = true + for (s in Constants.CATALOG_V2_IGNORE_SECTIONS) { + if (i.url?.contains("?section=$s") == true) { + need = false + break + } + } + if (need) { + sections?.add(CatalogV2ListItem(i)) } - sections?.add(CatalogV2ListItem(i)) } } diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/push/IDeviceIdProvider.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/push/IDeviceIdProvider.kt deleted file mode 100644 index 8b70a67b7..000000000 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/push/IDeviceIdProvider.kt +++ /dev/null @@ -1,5 +0,0 @@ -package dev.ragnarok.fenrir.push - -interface IDeviceIdProvider { - val deviceId: String -} \ No newline at end of file diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/push/IPushRegistrationResolver.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/push/IPushRegistrationResolver.kt index 10d90b158..be29ee3c6 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/push/IPushRegistrationResolver.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/push/IPushRegistrationResolver.kt @@ -1,8 +1,9 @@ package dev.ragnarok.fenrir.push +import android.content.Context import kotlinx.coroutines.flow.Flow interface IPushRegistrationResolver { - fun canReceivePushNotification(): Boolean - fun resolvePushRegistration(): Flow + fun canReceivePushNotification(accountId: Long): Boolean + fun resolvePushRegistration(accountId: Long, context: Context): Flow } \ No newline at end of file diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/push/PushRegistrationResolver.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/push/PushRegistrationResolver.kt index 9ab2566e2..22e0f0b14 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/push/PushRegistrationResolver.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/push/PushRegistrationResolver.kt @@ -1,5 +1,6 @@ package dev.ragnarok.fenrir.push +import android.content.Context import android.os.Build import dev.ragnarok.fenrir.AccountType import dev.ragnarok.fenrir.Constants @@ -8,8 +9,10 @@ import dev.ragnarok.fenrir.api.ApiException import dev.ragnarok.fenrir.api.interfaces.INetworker import dev.ragnarok.fenrir.service.ApiErrorCodes import dev.ragnarok.fenrir.settings.ISettings +import dev.ragnarok.fenrir.settings.Settings import dev.ragnarok.fenrir.settings.VKPushRegistration import dev.ragnarok.fenrir.util.Logger.d +import dev.ragnarok.fenrir.util.Utils import dev.ragnarok.fenrir.util.Utils.deviceName import dev.ragnarok.fenrir.util.Utils.getCauseIfRuntime import dev.ragnarok.fenrir.util.coroutines.CoroutinesUtils.andThen @@ -22,12 +25,10 @@ import kotlinx.coroutines.flow.map import java.util.Locale class PushRegistrationResolver( - private val deviceIdProvider: IDeviceIdProvider, private val settings: ISettings, private val networker: INetworker ) : IPushRegistrationResolver { - override fun canReceivePushNotification(): Boolean { - val accountId = settings.accounts().current + override fun canReceivePushNotification(accountId: Long): Boolean { if (accountId == ISettings.IAccountsSettings.INVALID_ID) { return false } @@ -40,21 +41,22 @@ class PushRegistrationResolver( return can } - override fun resolvePushRegistration(): Flow { - return info - .flatMapConcat { data -> + override fun resolvePushRegistration(accountId: Long, context: Context): Flow { + return FCMToken.fcmToken + .flatMapConcat { fcmToken -> val available = settings.pushSettings().registrations - val accountId = settings.accounts().current if (accountId == ISettings.IAccountsSettings.INVALID_ID && available.isEmpty() || accountId <= 0 || settings.accounts() .getType(accountId) != Constants.DEFAULT_ACCOUNT_TYPE ) { emptyTaskFlow() } else { + val deviceId = + Utils.getDeviceId(Settings.get().accounts().getType(accountId), context) val needUnregister: MutableSet = HashSet(0) var hasOk = false var hasRemove = false for (registered in available) { - val reason = analyzeRegistration(registered, data, accountId) + val reason = analyzeRegistration(registered, deviceId, fcmToken, accountId) d(TAG, "Reason: $reason") when (reason) { Reason.UNREGISTER_AND_REMOVE -> needUnregister.add(registered) @@ -77,9 +79,9 @@ class PushRegistrationResolver( val current = VKPushRegistration().set( accountId, - data.deviceId, + deviceId, vkToken, - data.fcmToken + fcmToken ) target.add(current) completable = completable.andThen(register(current)) @@ -183,15 +185,16 @@ class PushRegistrationResolver( private fun analyzeRegistration( available: VKPushRegistration, - data: Data, + deviceId: String, + fcmToken: String, accountId: Long ): Reason { when { - data.deviceId != available.deviceId -> { + deviceId != available.deviceId -> { return Reason.REMOVE } - data.fcmToken != available.fcmToken -> { + fcmToken != available.fcmToken -> { return Reason.REMOVE } @@ -207,16 +210,10 @@ class PushRegistrationResolver( } } - private val info: Flow - get() = FCMToken.fcmToken.map { - Data(it, deviceIdProvider.deviceId) - } - private enum class Reason { OK, REMOVE, UNREGISTER_AND_REMOVE } - private class Data(val fcmToken: String, val deviceId: String) companion object { private val TAG = PushRegistrationResolver::class.simpleName.orEmpty() } diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/settings/AccountsSettings.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/settings/AccountsSettings.kt index 827d75854..847e5e9ec 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/settings/AccountsSettings.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/settings/AccountsSettings.kt @@ -41,7 +41,7 @@ internal class AccountsSettings @SuppressLint("UseSparseArrays") constructor(con @SuppressLint("CheckResult") private fun fireAccountChange() { currentPublisher.myEmit(current) - pushRegistrationResolver.resolvePushRegistration().hiddenIO() + pushRegistrationResolver.resolvePushRegistration(current, app).hiddenIO() } override var current: Long diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/settings/backup/SettingsBackup.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/settings/backup/SettingsBackup.kt index 9efbc8fa2..1beef801c 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/settings/backup/SettingsBackup.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/settings/backup/SettingsBackup.kt @@ -22,6 +22,7 @@ class SettingsBackup { @Suppress("unused") class AppPreferencesList { //Main + var hidden_device_id: String? = null var send_by_enter: Boolean? = null var theme_overlay: String? = null var audio_round_icon: Boolean? = null diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/util/Utils.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/util/Utils.kt index 406f20253..a044c3dcb 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/util/Utils.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/util/Utils.kt @@ -2,7 +2,6 @@ package dev.ragnarok.fenrir.util import android.animation.Animator import android.animation.ObjectAnimator -import android.annotation.SuppressLint import android.app.Activity import android.app.PendingIntent import android.content.Context @@ -40,6 +39,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.floatingactionbutton.FloatingActionButton +import de.maxr1998.modernpreferences.PreferenceScreen import dev.ragnarok.fenrir.AccountType import dev.ragnarok.fenrir.BuildConfig import dev.ragnarok.fenrir.Constants @@ -119,6 +119,7 @@ object Utils { var follower_kick_mode = false private val displaySize = Point() private var device_id: String? = null + private var hiddenDevice_id: String? = null var density = 1f private set var scaledDensity = 1f @@ -978,8 +979,11 @@ object Utils { get() = isHiddenAccount(Settings.get().accounts().current) fun isHiddenAccount(account_id: Long): Boolean { - val accType = Settings.get().accounts().getType(account_id) - return accType == AccountType.VK_ANDROID_HIDDEN || accType == AccountType.KATE_HIDDEN || accType == AccountType.IOS_HIDDEN + return isHiddenType(Settings.get().accounts().getType(account_id)) + } + + fun isHiddenType(@AccountType accountType: Int): Boolean { + return accountType == AccountType.VK_ANDROID_HIDDEN || accountType == AccountType.KATE_HIDDEN || accountType == AccountType.IOS_HIDDEN } val isOfficialVKCurrent: Boolean @@ -990,16 +994,36 @@ object Utils { return accType == AccountType.VK_ANDROID || accType == AccountType.VK_ANDROID_HIDDEN || accType == AccountType.IOS_HIDDEN } - @SuppressLint("HardwareIds") - fun getDeviceId(context: Context): String { - if (device_id.isNullOrEmpty()) { - device_id = android.provider.Settings.Secure.getString( - context.contentResolver, - android.provider.Settings.Secure.ANDROID_ID - ) - if (device_id.isNullOrEmpty()) device_id = "0123456789A" + fun getDeviceId(@AccountType type: Int, context: Context): String { + if (isHiddenType(type)) { + if (hiddenDevice_id.isNullOrEmpty()) { + hiddenDevice_id = + PreferenceScreen.getPreferences(context).getString("hidden_device_id", null) + if (hiddenDevice_id.isNullOrEmpty()) { + val allowedChars = ('a'..'f') + ('0'..'9') + hiddenDevice_id = (1..16).map { allowedChars.random() } + .joinToString("") + ":" + (1..32).map { allowedChars.random() } + .joinToString("") + PreferenceScreen.getPreferences(context).edit() + .putString("hidden_device_id", hiddenDevice_id).apply() + } + } + return hiddenDevice_id!! + } else { + if (device_id.isNullOrEmpty()) { + device_id = + PreferenceScreen.getPreferences(context).getString("installation_id", null) + if (device_id.isNullOrEmpty()) { + val allowedChars = ('a'..'f') + ('0'..'9') + device_id = (1..16).map { allowedChars.random() } + .joinToString("") + ":" + (1..32).map { allowedChars.random() } + .joinToString("") + PreferenceScreen.getPreferences(context).edit() + .putString("installation_id", device_id).apply() + } + } + return device_id!! } - return device_id!! } /** diff --git a/app_filegallery/build.gradle b/app_filegallery/build.gradle index fa0d77359..3624603c8 100644 --- a/app_filegallery/build.gradle +++ b/app_filegallery/build.gradle @@ -78,14 +78,10 @@ android { minifyEnabled = true shrinkResources = true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - - signingConfig signingConfigs.fenrir } debug { minifyEnabled = false shrinkResources = false - - signingConfig signingConfigs.fenrir } } } diff --git a/build.gradle b/build.gradle index 1042524f0..af28c0dc7 100644 --- a/build.gradle +++ b/build.gradle @@ -31,21 +31,22 @@ buildscript { ext.dynamicanimationVersion = "1.1.0-alpha03" ext.exifinterfaceVersion = "1.3.7" ext.fragmentVersion = "1.8.2" + ext.graphicsVersion = "1.0.0" ext.lifecycleVersion = "2.8.4" ext.mediaVersion = "1.7.0" - ext.media3Version = "1.4.0" + ext.media3Version = "1.4.1" ext.resourceInspectionAnnotation = "1.0.1" ext.savedStateVersion = "1.3.0-alpha01" ext.swiperefreshlayoutVersion = "1.2.0-alpha01" ext.tracingVersion = "1.2.0" ext.transitionVersion = "1.5.1" ext.vectordrawableVersion = "1.2.0" - ext.webkitVersion = "1.11.0" + ext.webkitVersion = "1.12.0-beta01" ext.workVersion = "2.10.0-alpha02" //firebase libraries ext.firebaseDatatransportVersion = "19.0.0" - ext.firebaseMessagingVersion = "24.0.0" + ext.firebaseMessagingVersion = "24.0.1" //firebase common libraries ext.firebaseCommonVersion = "21.0.0" @@ -57,17 +58,17 @@ buildscript { ext.autoValueVersion = "1.11.0" //common libraries - ext.kotlin_version = "2.0.20-RC" - ext.kotlin_coroutines = "1.9.0-RC" + ext.kotlin_version = "2.0.20" + ext.kotlin_coroutines = "1.9.0-RC.2" ext.kotlin_serializer = "1.7.1" - //ext.okhttpLibraryVersion = "5.0.0-SNAPSHOT" - ext.okhttpLibraryVersion = "5.0.0-alpha.14" + ext.okhttpLibraryVersion = "5.0.0-SNAPSHOT" + //ext.okhttpLibraryVersion = "5.0.0-alpha.14" ext.okioVersion = "3.9.0" - ext.guavaVersion = "33.2.1-android" - ext.errorProneVersion = "2.29.2" + ext.guavaVersion = "33.3.0-android" + ext.errorProneVersion = "2.30.0" ext.checkerCompatQualVersion = "2.5.6" ext.checkerQualAndroidVersion = "3.46.0" - ext.desugarLibraryVersion = "2.0.4" + ext.desugarLibraryVersion = "2.1.0" //APP_PROPS ext.targetAbi = is_developer_build ? ["arm64-v8a", "x86_64"] : ["arm64-v8a", "armeabi-v7a", "x86_64"] @@ -104,7 +105,7 @@ allprojects { repositories { google() mavenCentral() - //maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots/' } + maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots/' } } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index df1c672a9..4505510c2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Wed Mar 20 16:00:00 MSK 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/libfenrir/ffmpeg.sh b/libfenrir/ffmpeg.sh index 27566a305..3e9b8439d 100644 --- a/libfenrir/ffmpeg.sh +++ b/libfenrir/ffmpeg.sh @@ -8,7 +8,7 @@ rm -r -f ".git" ENABLED_DECODERS=(mpeg4 h264 hevc mp3 aac ac3 eac3 flac vorbis alac) HOST_PLATFORM="linux-x86_64" -NDK_PATH="/home/umerov/Android/Sdk/ndk/27.0.12077973" +NDK_PATH="$HOME/Android/Sdk/ndk/27.0.12077973" echo 'Please input platform version (Example 21 - Android 5.0): ' read ANDROID_PLATFORM diff --git a/libfenrir/src/main/jni/animation/libyuv/include/libyuv/convert.h b/libfenrir/src/main/jni/animation/libyuv/include/libyuv/convert.h index 88619a4f6..5c7669b5d 100644 --- a/libfenrir/src/main/jni/animation/libyuv/include/libyuv/convert.h +++ b/libfenrir/src/main/jni/animation/libyuv/include/libyuv/convert.h @@ -418,6 +418,20 @@ int I412ToI420(const uint16_t* src_y, int width, int height); +// Convert 10 bit P010 to 8 bit NV12. +// dst_y can be NULL +LIBYUV_API +int P010ToNV12(const uint16_t* src_y, + int src_stride_y, + const uint16_t* src_uv, + int src_stride_uv, + uint8_t* dst_y, + int dst_stride_y, + uint8_t* dst_uv, + int dst_stride_uv, + int width, + int height); + #define I412ToI012 I410ToI010 #define H410ToH010 I410ToI010 #define H412ToH012 I410ToI010 @@ -511,6 +525,21 @@ int I010ToP010(const uint16_t* src_y, int width, int height); +// Convert 10 bit YUV I010 to NV12 +LIBYUV_API +int I010ToNV12(const uint16_t* src_y, + int src_stride_y, + const uint16_t* src_u, + int src_stride_u, + const uint16_t* src_v, + int src_stride_v, + uint8_t* dst_y, + int dst_stride_y, + uint8_t* dst_uv, + int dst_stride_uv, + int width, + int height); + // Convert I210 to P210 LIBYUV_API int I210ToP210(const uint16_t* src_y, diff --git a/libfenrir/src/main/jni/animation/libyuv/include/libyuv/cpu_id.h b/libfenrir/src/main/jni/animation/libyuv/include/libyuv/cpu_id.h index 7a2158789..8a295658d 100644 --- a/libfenrir/src/main/jni/animation/libyuv/include/libyuv/cpu_id.h +++ b/libfenrir/src/main/jni/animation/libyuv/include/libyuv/cpu_id.h @@ -93,8 +93,7 @@ int MipsCpuCaps(const char* cpuinfo_name); LIBYUV_API int RiscvCpuCaps(const char* cpuinfo_name); -#ifdef __aarch64__ -#if __linux__ +#ifdef __linux__ // On Linux, parse AArch64 features from getauxval(AT_HWCAP{,2}). LIBYUV_API int AArch64CpuCaps(unsigned long hwcap, unsigned long hwcap2); @@ -102,7 +101,6 @@ int AArch64CpuCaps(unsigned long hwcap, unsigned long hwcap2); LIBYUV_API int AArch64CpuCaps(); #endif -#endif // For testing, allow CPU flags to be disabled. // ie MaskCpuFlags(~kCpuHasSSSE3) to disable SSSE3. diff --git a/libfenrir/src/main/jni/animation/libyuv/include/libyuv/planar_functions.h b/libfenrir/src/main/jni/animation/libyuv/include/libyuv/planar_functions.h index f93447214..678074a14 100644 --- a/libfenrir/src/main/jni/animation/libyuv/include/libyuv/planar_functions.h +++ b/libfenrir/src/main/jni/animation/libyuv/include/libyuv/planar_functions.h @@ -766,6 +766,10 @@ int ARGBPolynomial(const uint8_t* src_argb, // Convert plane of 16 bit shorts to half floats. // Source values are multiplied by scale before storing as half float. +// +// Note: Unlike other libyuv functions that operate on uint16_t buffers, the +// src_stride_y and dst_stride_y parameters of HalfFloatPlane() are in bytes, +// not in units of uint16_t. LIBYUV_API int HalfFloatPlane(const uint16_t* src_y, int src_stride_y, diff --git a/libfenrir/src/main/jni/animation/libyuv/include/libyuv/rotate_row.h b/libfenrir/src/main/jni/animation/libyuv/include/libyuv/rotate_row.h index a45948094..d4a974c54 100644 --- a/libfenrir/src/main/jni/animation/libyuv/include/libyuv/rotate_row.h +++ b/libfenrir/src/main/jni/animation/libyuv/include/libyuv/rotate_row.h @@ -27,12 +27,10 @@ extern "C" { #define LIBYUV_DISABLE_NEON #endif -// clang >= 19.0.0 required for SME -#if !defined(LIBYUV_DISABLE_SME) && defined(__clang__) && defined(__aarch64__) -#if __clang_major__ < 19 +// temporary disable SME +#if !defined(LIBYUV_DISABLE_SME) #define LIBYUV_DISABLE_SME #endif -#endif // MemorySanitizer does not support assembly code yet. http://crbug.com/344505 #if defined(__has_feature) diff --git a/libfenrir/src/main/jni/animation/libyuv/include/libyuv/row.h b/libfenrir/src/main/jni/animation/libyuv/include/libyuv/row.h index 5e0d2ae9f..97eabbf67 100644 --- a/libfenrir/src/main/jni/animation/libyuv/include/libyuv/row.h +++ b/libfenrir/src/main/jni/animation/libyuv/include/libyuv/row.h @@ -400,10 +400,11 @@ extern "C" { // The following are available for AVX512 clang x86 platforms: // TODO(fbarchard): Port to GCC and Visual C -// TODO(fbarchard): re-enable HAS_ARGBTORGB24ROW_AVX512VBMI. Issue libyuv:789 +// TODO(b/42280744): re-enable HAS_ARGBTORGB24ROW_AVX512VBMI. #if !defined(LIBYUV_DISABLE_X86) && \ (defined(__x86_64__) || defined(__i386__)) && defined(CLANG_HAS_AVX512) #define HAS_ARGBTORGB24ROW_AVX512VBMI +#define HAS_CONVERT16TO8ROW_AVX512BW #define HAS_MERGEUVROW_AVX512BW #endif @@ -3337,6 +3338,10 @@ void Convert16To8Row_AVX2(const uint16_t* src_y, uint8_t* dst_y, int scale, int width); +void Convert16To8Row_AVX512BW(const uint16_t* src_y, + uint8_t* dst_y, + int scale, + int width); void Convert16To8Row_Any_SSSE3(const uint16_t* src_ptr, uint8_t* dst_ptr, int scale, @@ -3345,6 +3350,10 @@ void Convert16To8Row_Any_AVX2(const uint16_t* src_ptr, uint8_t* dst_ptr, int scale, int width); +void Convert16To8Row_Any_AVX512BW(const uint16_t* src_ptr, + uint8_t* dst_ptr, + int scale, + int width); void Convert16To8Row_NEON(const uint16_t* src_y, uint8_t* dst_y, int scale, @@ -6679,6 +6688,7 @@ void GaussCol_F32_C(const float* src0, int width); void GaussRow_C(const uint32_t* src, uint16_t* dst, int width); +void GaussRow_NEON(const uint32_t* src, uint16_t* dst, int width); void GaussCol_C(const uint16_t* src0, const uint16_t* src1, const uint16_t* src2, @@ -6686,6 +6696,13 @@ void GaussCol_C(const uint16_t* src0, const uint16_t* src4, uint32_t* dst, int width); +void GaussCol_NEON(const uint16_t* src0, + const uint16_t* src1, + const uint16_t* src2, + const uint16_t* src3, + const uint16_t* src4, + uint32_t* dst, + int width); void ClampFloatToZero_SSE2(const float* src_x, float* dst_y, int width); diff --git a/libfenrir/src/main/jni/animation/libyuv/include/libyuv/scale_row.h b/libfenrir/src/main/jni/animation/libyuv/include/libyuv/scale_row.h index 40f728b79..3d9a11b02 100644 --- a/libfenrir/src/main/jni/animation/libyuv/include/libyuv/scale_row.h +++ b/libfenrir/src/main/jni/animation/libyuv/include/libyuv/scale_row.h @@ -347,6 +347,10 @@ void ScaleRowDown2Box_16_C(const uint16_t* src_ptr, ptrdiff_t src_stride, uint16_t* dst, int dst_width); +void ScaleRowDown2Box_16_NEON(const uint16_t* src_ptr, + ptrdiff_t src_stride, + uint16_t* dst, + int dst_width); void ScaleRowDown2Box_16To8_C(const uint16_t* src_ptr, ptrdiff_t src_stride, uint8_t* dst, diff --git a/libfenrir/src/main/jni/animation/libyuv/include/libyuv/version.h b/libfenrir/src/main/jni/animation/libyuv/include/libyuv/version.h index ba083f5bd..c4b410a8a 100644 --- a/libfenrir/src/main/jni/animation/libyuv/include/libyuv/version.h +++ b/libfenrir/src/main/jni/animation/libyuv/include/libyuv/version.h @@ -11,6 +11,6 @@ #ifndef INCLUDE_LIBYUV_VERSION_H_ #define INCLUDE_LIBYUV_VERSION_H_ -#define LIBYUV_VERSION 1892 +#define LIBYUV_VERSION 1895 #endif // INCLUDE_LIBYUV_VERSION_H_ diff --git a/libfenrir/src/main/jni/animation/libyuv/source/convert.cc b/libfenrir/src/main/jni/animation/libyuv/source/convert.cc index 2d9a24518..7d44d8ae4 100644 --- a/libfenrir/src/main/jni/animation/libyuv/source/convert.cc +++ b/libfenrir/src/main/jni/animation/libyuv/source/convert.cc @@ -202,8 +202,10 @@ static int Planar16bitTo8bit(const uint16_t* src_y, } // Convert Y plane. - Convert16To8Plane(src_y, src_stride_y, dst_y, dst_stride_y, scale, width, - height); + if (dst_y) { + Convert16To8Plane(src_y, src_stride_y, dst_y, dst_stride_y, scale, width, + height); + } // Convert UV planes. Convert16To8Plane(src_u, src_stride_u, dst_u, dst_stride_u, scale, uv_width, uv_height); @@ -643,6 +645,155 @@ int I010ToP010(const uint16_t* src_y, width, height, 1, 1, 10); } +LIBYUV_API +int I010ToNV12(const uint16_t* src_y, + int src_stride_y, + const uint16_t* src_u, + int src_stride_u, + const uint16_t* src_v, + int src_stride_v, + uint8_t* dst_y, + int dst_stride_y, + uint8_t* dst_uv, + int dst_stride_uv, + int width, + int height) { + int y; + int halfwidth = (width + 1) >> 1; + int halfheight = (height + 1) >> 1; + const int scale = 16385; // 16384 for 10 bits + void (*Convert16To8Row)(const uint16_t* src_y, uint8_t* dst_y, int scale, + int width) = Convert16To8Row_C; + void (*MergeUVRow)(const uint8_t* src_u, const uint8_t* src_v, + uint8_t* dst_uv, int width) = MergeUVRow_C; + if ((!src_y && dst_y) || !src_u || !src_v || !dst_uv || width <= 0 || + height == 0) { + return -1; + } + // Negative height means invert the image. + if (height < 0) { + height = -height; + halfheight = (height + 1) >> 1; + src_y = src_y + (height - 1) * src_stride_y; + src_u = src_u + (halfheight - 1) * src_stride_u; + src_v = src_v + (halfheight - 1) * src_stride_v; + src_stride_y = -src_stride_y; + src_stride_u = -src_stride_u; + src_stride_v = -src_stride_v; + } +#if defined(HAS_CONVERT16TO8ROW_NEON) + if (TestCpuFlag(kCpuHasNEON)) { + Convert16To8Row = Convert16To8Row_Any_NEON; + if (IS_ALIGNED(width, 16)) { + Convert16To8Row = Convert16To8Row_NEON; + } + } +#endif +#if defined(HAS_CONVERT16TO8ROW_SSSE3) + if (TestCpuFlag(kCpuHasSSSE3)) { + Convert16To8Row = Convert16To8Row_Any_SSSE3; + if (IS_ALIGNED(width, 16)) { + Convert16To8Row = Convert16To8Row_SSSE3; + } + } +#endif +#if defined(HAS_CONVERT16TO8ROW_AVX2) + if (TestCpuFlag(kCpuHasAVX2)) { + Convert16To8Row = Convert16To8Row_Any_AVX2; + if (IS_ALIGNED(width, 32)) { + Convert16To8Row = Convert16To8Row_AVX2; + } + } +#endif +#if defined(HAS_CONVERT16TO8ROW_AVX512BW) + if (TestCpuFlag(kCpuHasAVX512BW)) { + Convert16To8Row = Convert16To8Row_Any_AVX512BW; + if (IS_ALIGNED(width, 64)) { + Convert16To8Row = Convert16To8Row_AVX512BW; + } + } +#endif + +#if defined(HAS_MERGEUVROW_SSE2) + if (TestCpuFlag(kCpuHasSSE2)) { + MergeUVRow = MergeUVRow_Any_SSE2; + if (IS_ALIGNED(halfwidth, 16)) { + MergeUVRow = MergeUVRow_SSE2; + } + } +#endif +#if defined(HAS_MERGEUVROW_AVX2) + if (TestCpuFlag(kCpuHasAVX2)) { + MergeUVRow = MergeUVRow_Any_AVX2; + if (IS_ALIGNED(halfwidth, 16)) { + MergeUVRow = MergeUVRow_AVX2; + } + } +#endif +#if defined(HAS_MERGEUVROW_AVX512BW) + if (TestCpuFlag(kCpuHasAVX512BW)) { + MergeUVRow = MergeUVRow_Any_AVX512BW; + if (IS_ALIGNED(halfwidth, 32)) { + MergeUVRow = MergeUVRow_AVX512BW; + } + } +#endif +#if defined(HAS_MERGEUVROW_NEON) + if (TestCpuFlag(kCpuHasNEON)) { + MergeUVRow = MergeUVRow_Any_NEON; + if (IS_ALIGNED(halfwidth, 16)) { + MergeUVRow = MergeUVRow_NEON; + } + } +#endif +#if defined(HAS_MERGEUVROW_MSA) + if (TestCpuFlag(kCpuHasMSA)) { + MergeUVRow = MergeUVRow_Any_MSA; + if (IS_ALIGNED(halfwidth, 16)) { + MergeUVRow = MergeUVRow_MSA; + } + } +#endif +#if defined(HAS_MERGEUVROW_LSX) + if (TestCpuFlag(kCpuHasLSX)) { + MergeUVRow = MergeUVRow_Any_LSX; + if (IS_ALIGNED(halfwidth, 16)) { + MergeUVRow = MergeUVRow_LSX; + } + } +#endif +#if defined(HAS_MERGEUVROW_RVV) + if (TestCpuFlag(kCpuHasRVV)) { + MergeUVRow = MergeUVRow_RVV; + } +#endif + + // Convert Y plane. + if (dst_y) { + Convert16To8Plane(src_y, src_stride_y, dst_y, dst_stride_y, scale, width, + height); + } + + { + // Allocate a row of uv. + align_buffer_64(row_u, ((halfwidth + 31) & ~31) * 2); + uint8_t* row_v = row_u + ((halfwidth + 31) & ~31); + if (!row_u) + return 1; + + for (y = 0; y < halfheight; ++y) { + Convert16To8Row(src_u, row_u, scale, halfwidth); + Convert16To8Row(src_v, row_v, scale, halfwidth); + MergeUVRow(row_u, row_v, dst_uv, halfwidth); + src_u += src_stride_u; + src_v += src_stride_v; + dst_uv += dst_stride_uv; + } + free_aligned_buffer_64(row_u); + } + return 0; +} + LIBYUV_API int I210ToP210(const uint16_t* src_y, int src_stride_y, @@ -4147,6 +4298,66 @@ int Android420ToI420(const uint8_t* src_y, dst_stride_v, width, height, kRotate0); } +// depth is source bits measured from lsb; For msb use 16 +static int Biplanar16bitTo8bit(const uint16_t* src_y, + int src_stride_y, + const uint16_t* src_uv, + int src_stride_uv, + uint8_t* dst_y, + int dst_stride_y, + uint8_t* dst_uv, + int dst_stride_uv, + int width, + int height, + int subsample_x, + int subsample_y, + int depth) { + int uv_width = SUBSAMPLE(width, subsample_x, subsample_x); + int uv_height = SUBSAMPLE(height, subsample_y, subsample_y); + int scale = 1 << (24 - depth); + if ((!src_y && dst_y) || !src_uv || !dst_uv || width <= 0 || height == 0) { + return -1; + } + // Negative height means invert the image. + if (height < 0) { + height = -height; + uv_height = -uv_height; + src_y = src_y + (height - 1) * src_stride_y; + src_uv = src_uv + (uv_height - 1) * src_stride_uv; + src_stride_y = -src_stride_y; + src_stride_uv = -src_stride_uv; + } + + // Convert Y plane. + if (dst_y) { + Convert16To8Plane(src_y, src_stride_y, dst_y, dst_stride_y, scale, width, + height); + } + // Convert UV planes. + Convert16To8Plane(src_uv, src_stride_uv, dst_uv, dst_stride_uv, scale, + uv_width * 2, uv_height); + return 0; +} + +// Convert 10 bit P010 to 8 bit NV12. +// Depth set to 16 because P010 uses 10 msb and this function keeps the upper 8 +// bits of the specified number of bits. +LIBYUV_API +int P010ToNV12(const uint16_t* src_y, + int src_stride_y, + const uint16_t* src_uv, + int src_stride_uv, + uint8_t* dst_y, + int dst_stride_y, + uint8_t* dst_uv, + int dst_stride_uv, + int width, + int height) { + return Biplanar16bitTo8bit(src_y, src_stride_y, src_uv, src_stride_uv, dst_y, + dst_stride_y, dst_uv, dst_stride_uv, width, height, + 1, 1, 16); +} + #ifdef __cplusplus } // extern "C" } // namespace libyuv diff --git a/libfenrir/src/main/jni/animation/libyuv/source/planar_functions.cc b/libfenrir/src/main/jni/animation/libyuv/source/planar_functions.cc index 6191e4423..6e8801cda 100644 --- a/libfenrir/src/main/jni/animation/libyuv/source/planar_functions.cc +++ b/libfenrir/src/main/jni/animation/libyuv/source/planar_functions.cc @@ -149,6 +149,14 @@ void Convert16To8Plane(const uint16_t* src_y, } } #endif +#if defined(HAS_CONVERT16TO8ROW_AVX512BW) + if (TestCpuFlag(kCpuHasAVX512BW)) { + Convert16To8Row = Convert16To8Row_Any_AVX512BW; + if (IS_ALIGNED(width, 64)) { + Convert16To8Row = Convert16To8Row_AVX512BW; + } + } +#endif // Convert plane for (y = 0; y < height; ++y) { diff --git a/libfenrir/src/main/jni/animation/libyuv/source/row_any.cc b/libfenrir/src/main/jni/animation/libyuv/source/row_any.cc index 2118ad500..67dc8d2f6 100644 --- a/libfenrir/src/main/jni/animation/libyuv/source/row_any.cc +++ b/libfenrir/src/main/jni/animation/libyuv/source/row_any.cc @@ -1684,8 +1684,8 @@ ANY11T(AB64ToARGBRow_Any_NEON, AB64ToARGBRow_NEON, 8, 4, uint16_t, uint8_t, 7) // Any 1 to 1 with parameter and shorts. BPP measures in shorts. #define ANY11C(NAMEANY, ANY_SIMD, SBPP, BPP, STYPE, DTYPE, MASK) \ void NAMEANY(const STYPE* src_ptr, DTYPE* dst_ptr, int scale, int width) { \ - SIMD_ALIGNED(STYPE vin[32]); \ - SIMD_ALIGNED(DTYPE vout[32]); \ + SIMD_ALIGNED(STYPE vin[64]); \ + SIMD_ALIGNED(DTYPE vout[64]); \ memset(vin, 0, sizeof(vin)); /* for msan */ \ int r = width & MASK; \ int n = width & ~MASK; \ @@ -1715,6 +1715,15 @@ ANY11C(Convert16To8Row_Any_AVX2, uint8_t, 31) #endif +#ifdef HAS_CONVERT16TO8ROW_AVX512BW +ANY11C(Convert16To8Row_Any_AVX512BW, + Convert16To8Row_AVX512BW, + 2, + 1, + uint16_t, + uint8_t, + 63) +#endif #ifdef HAS_CONVERT16TO8ROW_NEON ANY11C(Convert16To8Row_Any_NEON, Convert16To8Row_NEON, diff --git a/libfenrir/src/main/jni/animation/libyuv/source/row_gcc.cc b/libfenrir/src/main/jni/animation/libyuv/source/row_gcc.cc index 69babb453..cb757c755 100644 --- a/libfenrir/src/main/jni/animation/libyuv/source/row_gcc.cc +++ b/libfenrir/src/main/jni/animation/libyuv/source/row_gcc.cc @@ -5202,8 +5202,7 @@ void MultiplyRow_16_AVX2(const uint16_t* src_y, int width) { asm volatile ( "vmovd %3,%%xmm3 \n" - "vpunpcklwd %%xmm3,%%xmm3,%%xmm3 \n" - "vbroadcastss %%xmm3,%%ymm3 \n" + "vpbroadcastw %%xmm3,%%ymm3 \n" "sub %0,%1 \n" // 32 pixels per loop. @@ -5239,8 +5238,7 @@ void DivideRow_16_AVX2(const uint16_t* src_y, int width) { asm volatile ( "vmovd %3,%%xmm3 \n" - "vpunpcklwd %%xmm3,%%xmm3,%%xmm3 \n" - "vbroadcastss %%xmm3,%%ymm3 \n" + "vpbroadcastw %%xmm3,%%ymm3 \n" "sub %0,%1 \n" // 32 pixels per loop. @@ -5306,8 +5304,7 @@ void Convert16To8Row_AVX2(const uint16_t* src_y, int width) { asm volatile ( "vmovd %3,%%xmm2 \n" - "vpunpcklwd %%xmm2,%%xmm2,%%xmm2 \n" - "vbroadcastss %%xmm2,%%ymm2 \n" + "vpbroadcastw %%xmm2,%%ymm2 \n" // 32 pixels per loop. LABELALIGN @@ -5332,6 +5329,38 @@ void Convert16To8Row_AVX2(const uint16_t* src_y, } #endif // HAS_CONVERT16TO8ROW_AVX2 +#ifdef HAS_CONVERT16TO8ROW_AVX512BW +void Convert16To8Row_AVX512BW(const uint16_t* src_y, + uint8_t* dst_y, + int scale, + int width) { + asm volatile ( + "vpbroadcastw %3,%%zmm2 \n" + + // 64 pixels per loop. + LABELALIGN + "1: \n" + "vmovdqu8 (%0),%%zmm0 \n" + "vmovdqu8 0x40(%0),%%zmm1 \n" + "add $0x80,%0 \n" + "vpmulhuw %%zmm2,%%zmm0,%%zmm0 \n" + "vpmulhuw %%zmm2,%%zmm1,%%zmm1 \n" + "vpmovuswb %%zmm0,%%ymm0 \n" + "vpmovuswb %%zmm1,%%ymm1 \n" + "vmovdqu8 %%ymm0,(%1) \n" + "vmovdqu8 %%ymm1,0x20(%1) \n" + "add $0x40,%1 \n" + "sub $0x40,%2 \n" + "jg 1b \n" + "vzeroupper \n" + : "+r"(src_y), // %0 + "+r"(dst_y), // %1 + "+r"(width) // %2 + : "r"(scale) // %3 + : "memory", "cc", "xmm0", "xmm1", "xmm2"); +} +#endif // HAS_CONVERT16TO8ROW_AVX2 + // Use scale to convert to lsb formats depending how many bits there are: // 512 = 9 bits // 1024 = 10 bits @@ -5374,8 +5403,7 @@ void Convert8To16Row_AVX2(const uint8_t* src_y, int width) { asm volatile ( "vmovd %3,%%xmm2 \n" - "vpunpcklwd %%xmm2,%%xmm2,%%xmm2 \n" - "vbroadcastss %%xmm2,%%ymm2 \n" + "vpbroadcastw %%xmm2,%%ymm2 \n" // 32 pixels per loop. LABELALIGN diff --git a/libfenrir/src/main/jni/animation/libyuv/source/row_neon.cc b/libfenrir/src/main/jni/animation/libyuv/source/row_neon.cc index d99b13727..1211a3727 100644 --- a/libfenrir/src/main/jni/animation/libyuv/source/row_neon.cc +++ b/libfenrir/src/main/jni/animation/libyuv/source/row_neon.cc @@ -1833,11 +1833,12 @@ struct RgbUVConstants { }; // 8x1 pixels. -void ARGBToUV444MatrixRow_NEON(const uint8_t* src_argb, - uint8_t* dst_u, - uint8_t* dst_v, - int width, - const struct RgbUVConstants* rgbuvconstants) { +static void ARGBToUV444MatrixRow_NEON( + const uint8_t* src_argb, + uint8_t* dst_u, + uint8_t* dst_v, + int width, + const struct RgbUVConstants* rgbuvconstants) { asm volatile ( "vld1.8 {d0}, [%4] \n" // load rgbuvconstants @@ -2752,10 +2753,10 @@ static const struct RgbConstants kRgb24I601Constants = {{25, 129, 66, 0}, static const struct RgbConstants kRawI601Constants = {{66, 129, 25, 0}, 0x1080}; // ARGB expects first 3 values to contain RGB and 4th value is ignored. -void ARGBToYMatrixRow_NEON(const uint8_t* src_argb, - uint8_t* dst_y, - int width, - const struct RgbConstants* rgbconstants) { +static void ARGBToYMatrixRow_NEON(const uint8_t* src_argb, + uint8_t* dst_y, + int width, + const struct RgbConstants* rgbconstants) { asm volatile ( "vld1.8 {d0}, [%3] \n" // load rgbconstants "vdup.u8 d20, d0[0] \n" @@ -2802,10 +2803,10 @@ void ABGRToYJRow_NEON(const uint8_t* src_abgr, uint8_t* dst_yj, int width) { // RGBA expects first value to be A and ignored, then 3 values to contain RGB. // Same code as ARGB, except the LD4 -void RGBAToYMatrixRow_NEON(const uint8_t* src_rgba, - uint8_t* dst_y, - int width, - const struct RgbConstants* rgbconstants) { +static void RGBAToYMatrixRow_NEON(const uint8_t* src_rgba, + uint8_t* dst_y, + int width, + const struct RgbConstants* rgbconstants) { asm volatile ( "vld1.8 {d0}, [%3] \n" // load rgbconstants "vdup.u8 d20, d0[0] \n" @@ -2846,10 +2847,10 @@ void BGRAToYRow_NEON(const uint8_t* src_bgra, uint8_t* dst_y, int width) { RGBAToYMatrixRow_NEON(src_bgra, dst_y, width, &kRawI601Constants); } -void RGBToYMatrixRow_NEON(const uint8_t* src_rgb, - uint8_t* dst_y, - int width, - const struct RgbConstants* rgbconstants) { +static void RGBToYMatrixRow_NEON(const uint8_t* src_rgb, + uint8_t* dst_y, + int width, + const struct RgbConstants* rgbconstants) { asm volatile ( "vld1.8 {d0}, [%3] \n" // load rgbconstants "vdup.u8 d20, d0[0] \n" diff --git a/libfenrir/src/main/jni/animation/libyuv/source/row_neon64.cc b/libfenrir/src/main/jni/animation/libyuv/source/row_neon64.cc index 8320bcb0b..fae634234 100644 --- a/libfenrir/src/main/jni/animation/libyuv/source/row_neon64.cc +++ b/libfenrir/src/main/jni/animation/libyuv/source/row_neon64.cc @@ -2722,11 +2722,12 @@ struct RgbUVConstantsI8 { }; // 8x1 pixels. -void ARGBToUV444MatrixRow_NEON(const uint8_t* src_argb, - uint8_t* dst_u, - uint8_t* dst_v, - int width, - const struct RgbUVConstantsU8* rgbuvconstants) { +static void ARGBToUV444MatrixRow_NEON( + const uint8_t* src_argb, + uint8_t* dst_u, + uint8_t* dst_v, + int width, + const struct RgbUVConstantsU8* rgbuvconstants) { asm volatile( "ldr d0, [%4] \n" // load rgbuvconstants "dup v24.16b, v0.b[0] \n" // UB 0.875 coefficient @@ -2763,7 +2764,7 @@ void ARGBToUV444MatrixRow_NEON(const uint8_t* src_argb, "v27", "v28", "v29"); } -void ARGBToUV444MatrixRow_NEON_I8MM( +static void ARGBToUV444MatrixRow_NEON_I8MM( const uint8_t* src_argb, uint8_t* dst_u, uint8_t* dst_v, @@ -3519,10 +3520,10 @@ struct RgbConstants { }; // ARGB expects first 3 values to contain RGB and 4th value is ignored. -void ARGBToYMatrixRow_NEON(const uint8_t* src_argb, - uint8_t* dst_y, - int width, - const struct RgbConstants* rgbconstants) { +static void ARGBToYMatrixRow_NEON(const uint8_t* src_argb, + uint8_t* dst_y, + int width, + const struct RgbConstants* rgbconstants) { asm volatile ( "ldr d0, [%3] \n" // load rgbconstants "dup v6.16b, v0.b[0] \n" @@ -3552,11 +3553,11 @@ void ARGBToYMatrixRow_NEON(const uint8_t* src_argb, "v17"); } -void -ARGBToYMatrixRow_NEON_DotProd(const uint8_t* src_argb, - uint8_t* dst_y, - int width, - const struct RgbConstants* rgbconstants) { +static void ARGBToYMatrixRow_NEON_DotProd( + const uint8_t* src_argb, + uint8_t* dst_y, + int width, + const struct RgbConstants* rgbconstants) { asm volatile ( "ldr d0, [%3] \n" // load rgbconstants "dup v16.4s, v0.s[0] \n" @@ -3655,10 +3656,10 @@ void ABGRToYJRow_NEON_DotProd(const uint8_t* src_abgr, // RGBA expects first value to be A and ignored, then 3 values to contain RGB. // Same code as ARGB, except the LD4 -void RGBAToYMatrixRow_NEON(const uint8_t* src_rgba, - uint8_t* dst_y, - int width, - const struct RgbConstants* rgbconstants) { +static void RGBAToYMatrixRow_NEON(const uint8_t* src_rgba, + uint8_t* dst_y, + int width, + const struct RgbConstants* rgbconstants) { asm volatile ( "ldr d0, [%3] \n" // load rgbconstants "dup v6.16b, v0.b[0] \n" @@ -3727,10 +3728,10 @@ void BGRAToYRow_NEON_DotProd(const uint8_t* src_bgra, &kRawI601DotProdConstants); } -void RGBToYMatrixRow_NEON(const uint8_t* src_rgb, - uint8_t* dst_y, - int width, - const struct RgbConstants* rgbconstants) { +static void RGBToYMatrixRow_NEON(const uint8_t* src_rgb, + uint8_t* dst_y, + int width, + const struct RgbConstants* rgbconstants) { asm volatile ( "ldr d0, [%3] \n" // load rgbconstants "dup v5.16b, v0.b[0] \n" diff --git a/libfenrir/src/main/jni/animation/libyuv/source/row_rvv.cc b/libfenrir/src/main/jni/animation/libyuv/source/row_rvv.cc index 0533866c0..62c6b2631 100644 --- a/libfenrir/src/main/jni/animation/libyuv/source/row_rvv.cc +++ b/libfenrir/src/main/jni/animation/libyuv/source/row_rvv.cc @@ -2079,10 +2079,10 @@ static const struct RgbConstants kRawI601Constants = {{66, 129, 25, 0}, // ARGB expects first 3 values to contain RGB and 4th value is ignored #ifdef HAS_ARGBTOYMATRIXROW_RVV #ifdef LIBYUV_RVV_HAS_TUPLE_TYPE -void ARGBToYMatrixRow_RVV(const uint8_t* src_argb, - uint8_t* dst_y, - int width, - const struct RgbConstants* rgbconstants) { +static void ARGBToYMatrixRow_RVV(const uint8_t* src_argb, + uint8_t* dst_y, + int width, + const struct RgbConstants* rgbconstants) { assert(width != 0); size_t w = (size_t)width; vuint8m2_t v_by, v_gy, v_ry; // vectors are to store RGBToY constant @@ -2112,10 +2112,10 @@ void ARGBToYMatrixRow_RVV(const uint8_t* src_argb, } while (w > 0); } #else -void ARGBToYMatrixRow_RVV(const uint8_t* src_argb, - uint8_t* dst_y, - int width, - const struct RgbConstants* rgbconstants) { +static void ARGBToYMatrixRow_RVV(const uint8_t* src_argb, + uint8_t* dst_y, + int width, + const struct RgbConstants* rgbconstants) { assert(width != 0); size_t w = (size_t)width; vuint8m2_t v_by, v_gy, v_ry; // vectors are to store RGBToY constant @@ -2171,10 +2171,10 @@ void ABGRToYJRow_RVV(const uint8_t* src_abgr, uint8_t* dst_yj, int width) { // RGBA expects first value to be A and ignored, then 3 values to contain RGB. #ifdef HAS_RGBATOYMATRIXROW_RVV #ifdef LIBYUV_RVV_HAS_TUPLE_TYPE -void RGBAToYMatrixRow_RVV(const uint8_t* src_rgba, - uint8_t* dst_y, - int width, - const struct RgbConstants* rgbconstants) { +static void RGBAToYMatrixRow_RVV(const uint8_t* src_rgba, + uint8_t* dst_y, + int width, + const struct RgbConstants* rgbconstants) { assert(width != 0); size_t w = (size_t)width; vuint8m2_t v_by, v_gy, v_ry; // vectors are to store RGBToY constant @@ -2204,10 +2204,10 @@ void RGBAToYMatrixRow_RVV(const uint8_t* src_rgba, } while (w > 0); } #else -void RGBAToYMatrixRow_RVV(const uint8_t* src_rgba, - uint8_t* dst_y, - int width, - const struct RgbConstants* rgbconstants) { +static void RGBAToYMatrixRow_RVV(const uint8_t* src_rgba, + uint8_t* dst_y, + int width, + const struct RgbConstants* rgbconstants) { assert(width != 0); size_t w = (size_t)width; vuint8m2_t v_by, v_gy, v_ry; // vectors are to store RGBToY constant @@ -2256,10 +2256,10 @@ void BGRAToYRow_RVV(const uint8_t* src_bgra, uint8_t* dst_y, int width) { #ifdef HAS_RGBTOYMATRIXROW_RVV #ifdef LIBYUV_RVV_HAS_TUPLE_TYPE -void RGBToYMatrixRow_RVV(const uint8_t* src_rgb, - uint8_t* dst_y, - int width, - const struct RgbConstants* rgbconstants) { +static void RGBToYMatrixRow_RVV(const uint8_t* src_rgb, + uint8_t* dst_y, + int width, + const struct RgbConstants* rgbconstants) { assert(width != 0); size_t w = (size_t)width; vuint8m2_t v_by, v_gy, v_ry; // vectors are to store RGBToY constant @@ -2289,10 +2289,10 @@ void RGBToYMatrixRow_RVV(const uint8_t* src_rgb, } while (w > 0); } #else -void RGBToYMatrixRow_RVV(const uint8_t* src_rgb, - uint8_t* dst_y, - int width, - const struct RgbConstants* rgbconstants) { +static void RGBToYMatrixRow_RVV(const uint8_t* src_rgb, + uint8_t* dst_y, + int width, + const struct RgbConstants* rgbconstants) { assert(width != 0); size_t w = (size_t)width; vuint8m2_t v_by, v_gy, v_ry; // vectors are to store RGBToY constant diff --git a/libfenrir/src/main/jni/animation/libyuv/source/row_sve.cc b/libfenrir/src/main/jni/animation/libyuv/source/row_sve.cc index f847f4b92..a6da07b98 100644 --- a/libfenrir/src/main/jni/animation/libyuv/source/row_sve.cc +++ b/libfenrir/src/main/jni/animation/libyuv/source/row_sve.cc @@ -588,12 +588,12 @@ static const int16_t kABGRToUVJCoefficients[] = { -21, 63, -42, 0, 63, -10, -53, 0, }; -void ARGBToUVMatrixRow_SVE2(const uint8_t* src_argb, - int src_stride_argb, - uint8_t* dst_u, - uint8_t* dst_v, - int width, - const int16_t* uvconstants) { +static void ARGBToUVMatrixRow_SVE2(const uint8_t* src_argb, + int src_stride_argb, + uint8_t* dst_u, + uint8_t* dst_v, + int width, + const int16_t* uvconstants) { const uint8_t* src_argb_1 = src_argb + src_stride_argb; uint64_t vl; asm volatile ( @@ -1052,8 +1052,8 @@ void YUY2ToARGBRow_SVE2(const uint8_t* src_yuy2, uint8_t* dst_argb, const struct YuvConstants* yuvconstants, int width) { - uint32_t nv_uv_start = 0x0301'0301U; - uint32_t nv_uv_step = 0x0404'0404U; + uint32_t nv_uv_start = 0x03010301U; + uint32_t nv_uv_step = 0x04040404U; uint64_t vl; asm("cnth %0" : "=r"(vl)); int width_last_y = width & (vl - 1); @@ -1105,8 +1105,8 @@ void UYVYToARGBRow_SVE2(const uint8_t* src_uyvy, uint8_t* dst_argb, const struct YuvConstants* yuvconstants, int width) { - uint32_t nv_uv_start = 0x0200'0200U; - uint32_t nv_uv_step = 0x0404'0404U; + uint32_t nv_uv_start = 0x02000200U; + uint32_t nv_uv_step = 0x04040404U; uint64_t vl; asm("cnth %0" : "=r"(vl)); int width_last_y = width & (vl - 1); diff --git a/libfenrir/src/main/jni/animation/libyuv/source/scale_neon.cc b/libfenrir/src/main/jni/animation/libyuv/source/scale_neon.cc index 309d7b0bf..ba25fc6ec 100644 --- a/libfenrir/src/main/jni/animation/libyuv/source/scale_neon.cc +++ b/libfenrir/src/main/jni/animation/libyuv/source/scale_neon.cc @@ -9,6 +9,7 @@ */ #include "libyuv/row.h" +#include "libyuv/scale_row.h" #ifdef __cplusplus namespace libyuv { @@ -1080,91 +1081,6 @@ void ScaleFilterCols_NEON(uint8_t* dst_ptr, #undef LOAD2_DATA8_LANE -// 16x2 -> 16x1 -void ScaleFilterRows_NEON(uint8_t* dst_ptr, - const uint8_t* src_ptr, - ptrdiff_t src_stride, - int dst_width, - int source_y_fraction) { - asm volatile ( - "cmp %4, #0 \n" - "beq 100f \n" - "add %2, %1 \n" - "cmp %4, #64 \n" - "beq 75f \n" - "cmp %4, #128 \n" - "beq 50f \n" - "cmp %4, #192 \n" - "beq 25f \n" - - "vdup.8 d5, %4 \n" - "rsb %4, #256 \n" - "vdup.8 d4, %4 \n" - // General purpose row blend. - "1: \n" - "vld1.8 {q0}, [%1]! \n" - "vld1.8 {q1}, [%2]! \n" - "subs %3, %3, #16 \n" - "vmull.u8 q13, d0, d4 \n" - "vmull.u8 q14, d1, d4 \n" - "vmlal.u8 q13, d2, d5 \n" - "vmlal.u8 q14, d3, d5 \n" - "vrshrn.u16 d0, q13, #8 \n" - "vrshrn.u16 d1, q14, #8 \n" - "vst1.8 {q0}, [%0]! \n" - "bgt 1b \n" - "b 99f \n" - - // Blend 25 / 75. - "25: \n" - "vld1.8 {q0}, [%1]! \n" - "vld1.8 {q1}, [%2]! \n" - "subs %3, %3, #16 \n" - "vrhadd.u8 q0, q1 \n" - "vrhadd.u8 q0, q1 \n" - "vst1.8 {q0}, [%0]! \n" - "bgt 25b \n" - "b 99f \n" - - // Blend 50 / 50. - "50: \n" - "vld1.8 {q0}, [%1]! \n" - "vld1.8 {q1}, [%2]! \n" - "subs %3, %3, #16 \n" - "vrhadd.u8 q0, q1 \n" - "vst1.8 {q0}, [%0]! \n" - "bgt 50b \n" - "b 99f \n" - - // Blend 75 / 25. - "75: \n" - "vld1.8 {q1}, [%1]! \n" - "vld1.8 {q0}, [%2]! \n" - "subs %3, %3, #16 \n" - "vrhadd.u8 q0, q1 \n" - "vrhadd.u8 q0, q1 \n" - "vst1.8 {q0}, [%0]! \n" - "bgt 75b \n" - "b 99f \n" - - // Blend 100 / 0 - Copy row unchanged. - "100: \n" - "vld1.8 {q0}, [%1]! \n" - "subs %3, %3, #16 \n" - "vst1.8 {q0}, [%0]! \n" - "bgt 100b \n" - - "99: \n" - "vst1.8 {d1[7]}, [%0] \n" - : "+r"(dst_ptr), // %0 - "+r"(src_ptr), // %1 - "+r"(src_stride), // %2 - "+r"(dst_width), // %3 - "+r"(source_y_fraction) // %4 - : - : "q0", "q1", "d4", "d5", "q13", "q14", "memory", "cc"); -} - void ScaleARGBRowDown2_NEON(const uint8_t* src_ptr, ptrdiff_t src_stride, uint8_t* dst, diff --git a/libfenrir/src/main/jni/animation/libyuv/source/scale_neon64.cc b/libfenrir/src/main/jni/animation/libyuv/source/scale_neon64.cc index d440c28c9..51f65d68d 100644 --- a/libfenrir/src/main/jni/animation/libyuv/source/scale_neon64.cc +++ b/libfenrir/src/main/jni/animation/libyuv/source/scale_neon64.cc @@ -1458,58 +1458,6 @@ void ScaleRowDown2Box_16_NEON(const uint16_t* src_ptr, ); } -// Read 8x2 upsample with filtering and write 16x1. -// Actually reads an extra pixel, so 9x2. -void ScaleRowUp2_16_NEON(const uint16_t* src_ptr, - ptrdiff_t src_stride, - uint16_t* dst, - int dst_width) { - asm volatile ( - "add %1, %0, %1, lsl #1 \n" // ptr + stide * 2 - "movi v0.8h, #9 \n" // constants - "movi v1.4s, #3 \n" - - "1: \n" - "ld1 {v3.8h}, [%0], %4 \n" // TL read first 8 - "ld1 {v4.8h}, [%0], %5 \n" // TR read 8 offset by 1 - "ld1 {v5.8h}, [%1], %4 \n" // BL read 8 from next row - "ld1 {v6.8h}, [%1], %5 \n" // BR offset by 1 - "subs %w3, %w3, #16 \n" // 16 dst pixels per loop - "umull v16.4s, v3.4h, v0.4h \n" - "umull2 v7.4s, v3.8h, v0.8h \n" - "umull v18.4s, v4.4h, v0.4h \n" - "umull2 v17.4s, v4.8h, v0.8h \n" - "prfm pldl1keep, [%0, 448] \n" // prefetch 7 lines ahead - "uaddw v16.4s, v16.4s, v6.4h \n" - "uaddl2 v19.4s, v6.8h, v3.8h \n" - "uaddl v3.4s, v6.4h, v3.4h \n" - "uaddw2 v6.4s, v7.4s, v6.8h \n" - "uaddl2 v7.4s, v5.8h, v4.8h \n" - "uaddl v4.4s, v5.4h, v4.4h \n" - "uaddw v18.4s, v18.4s, v5.4h \n" - "prfm pldl1keep, [%1, 448] \n" - "mla v16.4s, v4.4s, v1.4s \n" - "mla v18.4s, v3.4s, v1.4s \n" - "mla v6.4s, v7.4s, v1.4s \n" - "uaddw2 v4.4s, v17.4s, v5.8h \n" - "uqrshrn v16.4h, v16.4s, #4 \n" - "mla v4.4s, v19.4s, v1.4s \n" - "uqrshrn2 v16.8h, v6.4s, #4 \n" - "uqrshrn v17.4h, v18.4s, #4 \n" - "uqrshrn2 v17.8h, v4.4s, #4 \n" - "st2 {v16.8h-v17.8h}, [%2], #32 \n" - "b.gt 1b \n" - : "+r"(src_ptr), // %0 - "+r"(src_stride), // %1 - "+r"(dst), // %2 - "+r"(dst_width) // %3 - : "r"(2LL), // %4 - "r"(14LL) // %5 - : "memory", "cc", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v16", - "v17", "v18", "v19" // Clobber List - ); -} - void ScaleUVRowDown2_NEON(const uint8_t* src_ptr, ptrdiff_t src_stride, uint8_t* dst, diff --git a/libfenrir/src/main/jni/animation/libyuv/source/scale_uv.cc b/libfenrir/src/main/jni/animation/libyuv/source/scale_uv.cc index 31c27e913..18ad43491 100644 --- a/libfenrir/src/main/jni/animation/libyuv/source/scale_uv.cc +++ b/libfenrir/src/main/jni/animation/libyuv/source/scale_uv.cc @@ -8,7 +8,7 @@ * be found in the AUTHORS file in the root of the source tree. */ -#include "libyuv/scale.h" +#include "libyuv/scale_uv.h" #include #include @@ -686,6 +686,7 @@ static void ScaleUVLinearUp2(int src_width, int dy; // This function can only scale up by 2 times horizontally. + (void)src_width; assert(src_width == ((dst_width + 1) / 2)); #ifdef HAS_SCALEUVROWUP2_LINEAR_SSSE3 @@ -744,6 +745,7 @@ static void ScaleUVBilinearUp2(int src_width, int x; // This function can only scale up by 2 times. + (void)src_width; assert(src_width == ((dst_width + 1) / 2)); assert(src_height == ((dst_height + 1) / 2)); @@ -805,6 +807,7 @@ static void ScaleUVLinearUp2_16(int src_width, int dy; // This function can only scale up by 2 times horizontally. + (void)src_width; assert(src_width == ((dst_width + 1) / 2)); #ifdef HAS_SCALEUVROWUP2_LINEAR_16_SSE41 @@ -857,6 +860,7 @@ static void ScaleUVBilinearUp2_16(int src_width, int x; // This function can only scale up by 2 times. + (void)src_width; assert(src_width == ((dst_width + 1) / 2)); assert(src_height == ((dst_height + 1) / 2)); diff --git a/libfenrir/src/main/jni/animation/rlottie/inc/rlottie.h b/libfenrir/src/main/jni/animation/rlottie/inc/rlottie.h index 9e454658b..4cb41dfa3 100644 --- a/libfenrir/src/main/jni/animation/rlottie/inc/rlottie.h +++ b/libfenrir/src/main/jni/animation/rlottie/inc/rlottie.h @@ -102,7 +102,9 @@ enum class Property { TrPosition, /*!< Transform Position property of Layer and Group object , value type is rlottie::Point */ TrScale, /*!< Transform Scale property of Layer and Group object , value type is rlottie::Size. range[0 ..100] */ TrRotation, /*!< Transform Rotation property of Layer and Group object , value type is float. range[0 .. 360] in degrees*/ - TrOpacity /*!< Transform Opacity property of Layer and Group object , value type is float [ 0 .. 100] */ + TrOpacity, /*!< Transform Opacity property of Layer and Group object , value type is float [ 0 .. 100] */ + TrimStart, /*!< Trim Start property of Shape object , value type is float [ 0 .. 100] */ + TrimEnd /*!< Trim End property of Shape object , value type is rlottie::Point [ 0 .. 100] */ }; struct Color_Type{}; @@ -447,8 +449,8 @@ template<> struct MapType> template<> struct MapType>: Point_Type{}; template<> struct MapType>: Point_Type{}; template<> struct MapType>: Size_Type{}; - - +template<> struct MapType>: Float_Type{}; +template<> struct MapType>: Point_Type{}; } // namespace lotplayer #endif // _RLOTTIE_H_ diff --git a/libfenrir/src/main/jni/animation/rlottie/rlottie-master.zip b/libfenrir/src/main/jni/animation/rlottie/rlottie-master.zip index e70f070bd529bb34aa4666a47f4af19820a881b6..033b9258afbe8824b2b19d6608311bfb7dec4935 100644 GIT binary patch delta 65372 zcmZ^~1yodT)Hh0ZcjwSBq`=TagER<8mvp0m)DY5bFm!_`ND4@INvCvo34)}+nSn{7Q%l&_83&plkosV-NHk*{}Y93 z>;mYa3;-}1`7=FK1jPTF0t3bu%!~3L8d4KpgBV_e6kdZIUV{=|gBo6g7G8rMUV{-{ zgBf0f6<&iKUV{@}gBxCh7hZ!OUPBOGLl|B|6kbCdUPBUILmFN~R$D{P%#HG&Hqd!) za+usmTpserMe(>O9~af*qJCU7kBjzk(LFBu$Hnlt7#|l?JuVOP$7q<4Z*|N9f0e60 zu4A6!gn_+l%uit8!y0p34h&2T0Z72W(GWlmnD9RxC<34v&WMlRK|(;FN!BKY4zYj< zVMHIh0N#i&PR1SpKMd6G0idG5K=T0r@Gl5W9soGNu=@c3%%Xb!?*K+WEtPRCE}-)bu|}muY|joU(rgzyo8yo&o6M!zi`30erB{-)sXWV3^Sk zTuW%;4xkB!J%*O^e}o(sP-x690OlZQ*De5p_n#~1$#qaRg|3PfVABJARfTg=rb}C(6BQu&>rki(5>m+s#O$);dZWZg*)^8G8S5P^{pBb}#w)pl!(w^)sXz1j>C zQh4KKhQY3zBw#PoBs@kGZkrBfuNcZ0h>%T3TqGko-3dW4w*~&_$&<>KC=hBYN?b^4 zyRRd<`Ex`j#JWoyDQwx71o+9rk_jp<6xIqP`0)iCida1WziQ-&3%prN_TI(bq}8a? zfZpc*1oo{rP24Cf=>q);R*B9U+a9S$Ix@$IpP)v*>*IPu%#O@hSE-#OWA=w55Zcc+ zpS-W*`c(P@$HjZP#?A^Lf-wsfy8MaY?g zlNzc;aXPCI=gu1y9TcMj(ihpY%aZ__y8H{rk%OOE&)U1X)fz!aA2qk{P=B(h^jkBT z7)WR*TCagJjj)Y>(HWK0(swhzgQ)YkuSH`xnUTm|cA2m27?NtZp!6Wevf=A)=q)S+ zqUy<0Fj9Rx3gtygdsV-J`nJrNRxynFz)bKJpCX?SdQM!za!8qQ=YccgG46r4yB(rY zs?o{~P8jvrP0pIMVw*0)U7MUPeY}N*j!=QfA(dE6d{C4B5B?kaHRQ$%bO_nY_-so{ z6DJ0*-f^5j`BT5fxz?ViM`7bn%0^LRURfIk#smoRfy44_jTCRoOVjw81tY_f6BJ(Z zd9LiPq^2tW+zTv?iq=NEVQUT^|1xu=BaHQN$ t6ziztq{bnjxpvii>}Z$$Pp);~ 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�Ou&Pf|Jpu z7}HN$T~7N6CWPBy)la-Xb{eIHm8G#1suCi*fQ(rULRR_KvfxT$QF zwgO8}*+7Vhq|6RT$ zDKm0l@4x0;4GD-jExK12XptYW8Hqr@d<3EjKTU)vc-RsFci|Bh=IX^_$u@yjf!A6J zsQ(27CE``i%O@)usEYdr?e>o(+X&QJeS3hEEpHI`%FQ(LS{v>KxbYbjmoSZGC#%xm z8*8L~oc9@_H3}lWz(yLR_hI%rs)6Ks9_(BbkA(gosPwH#l#V;>Ndj7WQX3K?gsO35 z#4Zi>|I_%t_uxqF7r=jF!Bf)mIv!}5 zB#+{l_EB0DD((ZgX=Maw92*@QLEaycuKat`j?{2`1}iMcpNY*$211S+t+ZDb7B@7c z5M)9Cj{5C7crF=Ih7%z}^1EUQOp=a0U{r;GSVmlK{%%PiCDj3q*v2Vw?Cqp;qCh=bx87l zV5{3cE4Gq>33mg@50;Mv+>AepOY$?MFnK#1063{^6xVRXkejTt-FvDLs0Bi%_cqcU z?Jor=?b3n&v>Eg`6pbcp;SZoR=C zhJ7GkQU@zB;$)|{n$Q9r^1OpHKpW515D+r(SSGkjQ=364ob(Z}ds_d&a<&+=knQKc z_ex0z$+KI3=vjsT-|7c0$^_!>fBFL_zcRDkazHr}Bovklq|?Y0D_oZi1_`)m$`0dM zOelD7WEK!}1*|9k#WKP-vM3zn|9g*>G`c-C`@@7nc5MRgyb{L$iY!Ho|EPqzMLq#W zZH>SlS(-4wM=edrk8yt0TPMNl5<|FwM0|S4<}W?@bB}*7%aVo%R~tk$;pQxPw`n;F zG&HqEVB51HA$ZK%g#5*al=1=vJy4jYJ3)H%?3B+RdI=IQLJR@g_2-W`04k&qNM*z=?rxARS{s%4Yf2H}A}1-W{Y_5- z9hEB(*zS`Qrij1SiZK{zqO9p^ErC=@|NljelIyA$Ou+1X9xRlJYu&#u6R0WFbT^&? zsvT@5AVgr+Yo_G0#h%O!4ckG&&<_ro6JL&b(1wtXouZHnjxKu0XoEyk9pr<=mfwA6 zg*4~3DTU3JtvNuqZ}$-aNj((0*6ZG<`Xr!R9OxM0hPRLZA*C0jytf5&!9dQTNkp4j zQ>OnPoxwhfAocFKRWyYIlRX5~;+Itv?caOS(hhv3{u{cJOsxx8~>b8B`=K;DC5D`~#KfrYbpmfYx0I}#& zcAdZXze($c3TB9W!fkMp5RZuNUnM|V{coH`BoN*!Koao& zn^^LoQ3F|7zktWjL2LidzEapZP@mwVg5cz3oIKgm#HFD#L$V-}x`UxCMii22s8`5% zVKpjB_7*-5*bC7D95W;c7gIwNE*{|txchW}aU5{*Is@`W&cClRAWaQmh7|@vu-FAk zi$2^1LVH?2D6Q78&w6cEsYEWk_103bxmC#4L=QU9SvQ7e}AnVnwq|v XJ{mp-{+haeJM?w6{q+sJ^)>$obv~UI delta 64996 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⁢5v(?-9s#j+S2==`nDUSD5?RwA}X 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 : public FilterBase } }; +template <> +class Filter: public FilterBase +{ +public: + Filter(model::Trim* model) : FilterBase(model) {} + + model::Trim::Segment segment(int frameNo) const + { + if (this->hasFilter(rlottie::Property::TrimStart)) { + this->model_->updateTrimStartValue(this->filter()->value(rlottie::Property::TrimStart, frameNo)); + } + if (this->hasFilter(rlottie::Property::TrimEnd)) { + this->model_->updateTrimEndValue(this->filter()->point(rlottie::Property::TrimEnd, frameNo)); + } + model::Trim::Segment segment = this->model()->segment(frameNo); + return segment; + } +}; + } // namespace model diff --git a/libfenrir/src/main/jni/animation/rlottie/src/lottie/lottieitem.cpp b/libfenrir/src/main/jni/animation/rlottie/src/lottie/lottieitem.cpp index f1a0625ee..bbeab5a8a 100644 --- a/libfenrir/src/main/jni/animation/rlottie/src/lottie/lottieitem.cpp +++ b/libfenrir/src/main/jni/animation/rlottie/src/lottie/lottieitem.cpp @@ -73,6 +73,17 @@ static bool strokeProp(rlottie::Property prop) } } +static bool trimProp(rlottie::Property prop) +{ + switch (prop) { + case rlottie::Property::TrimStart: + case rlottie::Property::TrimEnd: + return true; + default: + return false; + } +} + static renderer::Layer *createLayerItem(model::Layer *layerData, VArenaAlloc * allocator) { @@ -1385,6 +1396,21 @@ bool renderer::GradientStroke::updateContent(int frameNo, const VMatrix &matrix, return !vIsZero(combinedAlpha); } +bool renderer::Trim::resolveKeyPath(LOTKeyPath &keyPath, uint32_t depth, + LOTVariant &value) +{ + if (!keyPath.matches(mModel.name(), depth)) { + return false; + } + + if (keyPath.fullyResolvesTo(mModel.name(), depth) && + trimProp(value.property())) { + mModel.filter()->addValue(value); + return true; + } + return false; +} + void renderer::Trim::update(int frameNo, const VMatrix & /*parentMatrix*/, float /*parentAlpha*/, const DirtyFlag & /*flag*/) { @@ -1392,7 +1418,7 @@ void renderer::Trim::update(int frameNo, const VMatrix & /*parentMatrix*/, if (mCache.mFrameNo == frameNo) return; - model::Trim::Segment segment = mData->segment(frameNo); + model::Trim::Segment segment = mModel.segment(frameNo); if (!(vCompare(mCache.mSegment.start, segment.start) && vCompare(mCache.mSegment.end, segment.end))) { diff --git a/libfenrir/src/main/jni/animation/rlottie/src/lottie/lottieitem.h b/libfenrir/src/main/jni/animation/rlottie/src/lottie/lottieitem.h index 174e84fe3..4105117bf 100644 --- a/libfenrir/src/main/jni/animation/rlottie/src/lottie/lottieitem.h +++ b/libfenrir/src/main/jni/animation/rlottie/src/lottie/lottieitem.h @@ -583,13 +583,17 @@ class GradientStroke final : public Paint { class Trim final : public Object { public: - explicit Trim(model::Trim *data) : mData(data) {} + explicit Trim(model::Trim *data) : mData(data), mModel(data) {} void update(int frameNo, const VMatrix &parentMatrix, float parentAlpha, const DirtyFlag &flag) final; Object::Type type() const final { return Object::Type::Trim; } void update(); void addPathItems(std::vector &list, size_t startOffset); +protected: + bool resolveKeyPath(LOTKeyPath &keyPath, uint32_t depth, + LOTVariant &value) final; + private: bool pathDirty() const { @@ -607,6 +611,8 @@ class Trim final : public Object { model::Trim * mData{nullptr}; VPathMesure mPathMesure; bool mDirty{true}; + + model::Filter mModel; }; class Repeater final : public Group { diff --git a/libfenrir/src/main/jni/animation/rlottie/src/lottie/lottiemodel.h b/libfenrir/src/main/jni/animation/rlottie/src/lottie/lottiemodel.h index 48e6e962d..b210835e4 100644 --- a/libfenrir/src/main/jni/animation/rlottie/src/lottie/lottiemodel.h +++ b/libfenrir/src/main/jni/animation/rlottie/src/lottie/lottiemodel.h @@ -1202,6 +1202,20 @@ class Trim : public Object { }; enum class TrimType { Simultaneously, Individually }; Trim() : Object(Object::Type::Trim) {} + + void updateTrimStartValue(float start) + { + mStart.value() = start; + } + + void updateTrimEndValue(VPointF pos) + { + for (auto &keyFrame : mEnd.animation().frames_) { + keyFrame.value_.start_ = pos.x(); + keyFrame.value_.end_ = pos.y(); + } + } + /* * if start > end vector trims the path as a loop ( 2 segment) * if start < end vector trims the path without loop ( 1 segment). diff --git a/libfenrir/src/main/jni/animation/rlottie/src/lottie/rapidjson/reader.h b/libfenrir/src/main/jni/animation/rlottie/src/lottie/rapidjson/reader.h index 55546601e..f7ef61024 100644 --- a/libfenrir/src/main/jni/animation/rlottie/src/lottie/rapidjson/reader.h +++ b/libfenrir/src/main/jni/animation/rlottie/src/lottie/rapidjson/reader.h @@ -1584,7 +1584,7 @@ class GenericReader { // Parse frac = decimal-point 1*DIGIT int expFrac = 0; size_t decimalPosition; - if (Consume(s, '.')) { + if (!useNanOrInf && Consume(s, '.')) { decimalPosition = s.Length(); if (RAPIDJSON_UNLIKELY(!(s.Peek() >= '0' && s.Peek() <= '9'))) @@ -1631,7 +1631,7 @@ class GenericReader { // Parse exp = e [ minus / plus ] 1*DIGIT int exp = 0; - if (Consume(s, 'e') || Consume(s, 'E')) { + if (!useNanOrInf && (Consume(s, 'e') || Consume(s, 'E'))) { if (!useDouble) { d = static_cast(use64bit ? i64 : i); useDouble = true; diff --git a/libfenrir/src/main/jni/animation/thorvg/inc/thorvg.h b/libfenrir/src/main/jni/animation/thorvg/inc/thorvg.h index ab69d62b1..c16b75fdb 100644 --- a/libfenrir/src/main/jni/animation/thorvg/inc/thorvg.h +++ b/libfenrir/src/main/jni/animation/thorvg/inc/thorvg.h @@ -157,7 +157,7 @@ enum class FillRule : uint8_t enum class CompositeMethod : uint8_t { None = 0, ///< No composition is applied. - ClipPath, ///< The intersection of the source and the target is determined and only the resulting pixels from the source are rendered. + ClipPath, ///< The intersection of the source and the target is determined and only the resulting pixels from the source are rendered. Note that ClipPath only supports the Shape type. AlphaMask, ///< Alpha Masking using the compositing target's pixels as an alpha value. InvAlphaMask, ///< Alpha Masking using the complement to the compositing target's pixels as an alpha value. LumaMask, ///< Alpha Masking using the grayscale (0.2125R + 0.7154G + 0.0721*B) of the compositing target's pixels. @since 0.9 @@ -165,7 +165,9 @@ enum class CompositeMethod : uint8_t AddMask, ///< Combines the target and source objects pixels using target alpha. (T * TA) + (S * (255 - TA)) (Experimental API) SubtractMask, ///< Subtracts the source color from the target color while considering their respective target alpha. (T * TA) - (S * (255 - TA)) (Experimental API) IntersectMask, ///< Computes the result by taking the minimum value between the target alpha and the source alpha and multiplies it with the target color. (T * min(TA, SA)) (Experimental API) - DifferenceMask ///< Calculates the absolute difference between the target color and the source color multiplied by the complement of the target alpha. abs(T - S * (255 - TA)) (Experimental API) + DifferenceMask, ///< Calculates the absolute difference between the target color and the source color multiplied by the complement of the target alpha. abs(T - S * (255 - TA)) (Experimental API) + LightenMask, ///< Where multiple masks intersect, the highest transparency value is used. (Experimental API) + DarkenMask ///< Where multiple masks intersect, the lowest transparency value is used. (Experimental API) }; @@ -255,34 +257,6 @@ struct Matrix }; -/** - * @brief A data structure representing a texture mesh vertex - * - * @param pt The vertex coordinate - * @param uv The normalized texture coordinate in the range (0.0..1.0, 0.0..1.0) - * - * @note Experimental API - */ -struct Vertex -{ - Point pt; - Point uv; -}; - - -/** - * @brief A data structure representing a triangle in a texture mesh - * - * @param vertex The three vertices that make up the polygon - * - * @note Experimental API - */ -struct Polygon -{ - Vertex vertex[3]; -}; - - /** * @class Paint * @@ -384,20 +358,21 @@ class TVG_API Paint * * @note Experimental API */ - Result blend(BlendMethod method) const noexcept; + Result blend(BlendMethod method) noexcept; /** * @brief Gets the axis-aligned bounding box of the paint object. * - * In case @p transform is @c true, all object's transformations are applied first, and then the bounding box is established. Otherwise, the bounding box is determined before any transformations. - * * @param[out] x The x-coordinate of the upper-left corner of the object. * @param[out] y The y-coordinate of the upper-left corner of the object. * @param[out] w The width of the object. * @param[out] h The height of the object. - * @param[in] transformed If @c true, the paint's transformations are taken into account, otherwise they aren't. + * @param[in] transformed If @c true, the paint's transformations are taken into account in the scene it belongs to. Otherwise they aren't. * + * @note This is useful when you need to figure out the bounding box of the paint in the canvas space. * @note The bounding box doesn't indicate the actual drawing region. It's the smallest rectangle that encloses the object. + * @note If @p transformed is @c true, the paint needs to be pushed into a canvas and updated before this api is called. + * @see Canvas::update() */ Result bounds(float* x, float* y, float* w, float* h, bool transformed = false) const noexcept; @@ -429,9 +404,9 @@ class TVG_API Paint CompositeMethod composite(const Paint** target) const noexcept; /** - * @brief Gets the blending method of the object. + * @brief Retrieves the current blending method applied to the paint object. * - * @return The blending method + * @return The currently set blending method. * * @note Experimental API */ @@ -448,6 +423,15 @@ class TVG_API Paint */ virtual Type type() const noexcept = 0; + /** + * @brief Unique ID of this instance. + * + * This is reserved to specify an paint instance in a scene. + * + * @since Experimental API + */ + uint32_t id = 0; + /** * @see Paint::type() */ @@ -946,7 +930,7 @@ class TVG_API Shape final : public Paint * * @note Setting @p sweep value greater than 360 degrees, is equivalent to calling appendCircle(cx, cy, radius, radius). */ - Result appendArc(float cx, float cy, float radius, float startAngle, float sweep, bool pie) noexcept; + TVG_DEPRECATED Result appendArc(float cx, float cy, float radius, float startAngle, float sweep, bool pie) noexcept; /** * @brief Appends a given sub-path to the path. @@ -1126,7 +1110,6 @@ class TVG_API Shape final : public Paint * @param[out] b The blue color channel value in the range [0 ~ 255]. * @param[out] a The alpha channel value in the range [0 ~ 255], where 0 is completely transparent and 255 is opaque. * - * @return Result::Success when succeed. */ Result fillColor(uint8_t* r, uint8_t* g, uint8_t* b, uint8_t* a = nullptr) const noexcept; @@ -1274,7 +1257,7 @@ class TVG_API Picture final : public Paint * when the @p copy has @c false. This means that loading the same data again will not result in duplicate operations * for the sharable @p data. Instead, ThorVG will reuse the previously loaded picture data. * - * @param[in] data A pointer to a memory location where the content of the picture file is stored. + * @param[in] data A pointer to a memory location where the content of the picture file is stored. A null-terminated string is expected for non-binary data if @p copy is @c false. * @param[in] size The size in bytes of the memory occupied by the @p data. * @param[in] mimeType Mimetype or extension of data such as "jpg", "jpeg", "lottie", "svg", "svg+xml", "png", etc. In case an empty string or an unknown type is provided, the loaders will be tried one by one. * @param[in] rpath A resource directory path, if the @p data needs to access any external resources. @@ -1312,7 +1295,7 @@ class TVG_API Picture final : public Paint Result size(float* w, float* h) const noexcept; /** - * @brief Loads a raw data from a memory block with a given size. + * @brief Loads raw data in ARGB8888 format from a memory block of the given size. * * ThorVG efficiently caches the loaded data using the specified @p data address as a key * when the @p copy has @c false. This means that loading the same data again will not result in duplicate operations @@ -1329,39 +1312,19 @@ class TVG_API Picture final : public Paint Result load(uint32_t* data, uint32_t w, uint32_t h, bool premultiplied, bool copy = false) noexcept; /** - * @brief Sets or removes the triangle mesh to deform the image. + * @brief Retrieve a paint object from the Picture scene by its Unique ID. * - * If a mesh is provided, the transform property of the Picture will apply to the triangle mesh, and the - * image data will be used as the texture. + * This function searches for a paint object within the Picture scene that matches the provided @p id. * - * If @p triangles is @c nullptr, or @p triangleCnt is 0, the mesh will be removed. + * @param[in] id The Unique ID of the paint object. * - * Only raster image types are supported at this time (png, jpg). Vector types like svg and tvg do not support. - * mesh deformation. However, if required you should be able to render a vector image to a raster image and then apply a mesh. + * @return A pointer to the paint object that matches the given identifier, or @c nullptr if no matching paint object is found. * - * @param[in] triangles An array of Polygons(triangles) that make up the mesh, or null to remove the mesh. - * @param[in] triangleCnt The number of Polygons(triangles) provided, or 0 to remove the mesh. - * - * @note The Polygons are copied internally, so modifying them after calling Mesh::mesh has no affect. - * @warning Please do not use it, this API is not official one. It could be modified in the next version. - * - * @note Experimental API - */ - Result mesh(const Polygon* triangles, uint32_t triangleCnt) noexcept; - - /** - * @brief Return the number of triangles in the mesh, and optionally get a pointer to the array of triangles in the mesh. - * - * @param[out] triangles Optional. A pointer to the array of Polygons used by this mesh. - * - * @return The number of polygons in the array. - * - * @note Modifying the triangles returned by this method will modify them directly within the mesh. - * @warning Please do not use it, this API is not official one. It could be modified in the next version. + * @see Accessor::id() * * @note Experimental API */ - uint32_t mesh(const Polygon** triangles) const noexcept; + const Paint* paint(uint32_t id) noexcept; /** * @brief Creates a new Picture object. @@ -1522,8 +1485,6 @@ class TVG_API Text final : public Paint * @param[in] g The green color channel value in the range [0 ~ 255]. The default value is 0. * @param[in] b The blue color channel value in the range [0 ~ 255]. The default value is 0. * - * @retval Result::InsufficientCondition when the font has not been set up prior to this operation. - * * @see Text::font() * * @note Experimental API @@ -1537,8 +1498,6 @@ class TVG_API Text final : public Paint * * @param[in] f The unique pointer to the gradient fill. * - * @retval Result::InsufficientCondition when the font has not been set up prior to this operation. - * * @note Either a solid color or a gradient fill is applied, depending on what was set as last. * @note Experimental API * @@ -2111,17 +2070,36 @@ class TVG_API Accessor final public: ~Accessor(); + TVG_DEPRECATED std::unique_ptr set(std::unique_ptr picture, std::function func) noexcept; + /** * @brief Set the access function for traversing the Picture scene tree nodes. * * @param[in] picture The picture node to traverse the internal scene-tree. * @param[in] func The callback function calling for every paint nodes of the Picture. - * - * @return Return the given @p picture instance. + * @param[in] data Data passed to the @p func as its argument. * * @note The bitmap based picture might not have the scene-tree. + * + * @note Experimental API + */ + Result set(const Picture* picture, std::function func, void* data) noexcept; + + /** + * @brief Generate a unique ID (hash key) from a given name. + * + * This function computes a unique identifier value based on the provided string. + * You can use this to assign a unique ID to the Paint object. + * + * @param[in] name The input string to generate the unique identifier from. + * + * @return The generated unique identifier value. + * + * @see Paint::id + * + * @note Experimental API */ - std::unique_ptr set(std::unique_ptr picture, std::function func) noexcept; + static uint32_t id(const char* name) noexcept; /** * @brief Creates a new Accessor object. diff --git a/libfenrir/src/main/jni/animation/thorvg/src/common/tvgArray.h b/libfenrir/src/main/jni/animation/thorvg/src/common/tvgArray.h index 8178bd0e4..19c8f6972 100644 --- a/libfenrir/src/main/jni/animation/thorvg/src/common/tvgArray.h +++ b/libfenrir/src/main/jni/animation/thorvg/src/common/tvgArray.h @@ -59,7 +59,7 @@ struct Array data[count++] = element; } - void push(Array& rhs) + void push(const Array& rhs) { if (rhs.count == 0) return; grow(rhs.count); diff --git a/libfenrir/src/main/jni/animation/thorvg/src/common/tvgCompressor.cpp b/libfenrir/src/main/jni/animation/thorvg/src/common/tvgCompressor.cpp index e7560e794..ffc563f77 100644 --- a/libfenrir/src/main/jni/animation/thorvg/src/common/tvgCompressor.cpp +++ b/libfenrir/src/main/jni/animation/thorvg/src/common/tvgCompressor.cpp @@ -478,6 +478,8 @@ size_t b64Decode(const char* encoded, const size_t len, char** decoded) unsigned long djb2Encode(const char* str) { + if (!str) return 0; + unsigned long hash = 5381; int c; diff --git a/libfenrir/src/main/jni/animation/thorvg/src/common/tvgInlist.h b/libfenrir/src/main/jni/animation/thorvg/src/common/tvgInlist.h index ff28cfd48..fc99ae3d1 100644 --- a/libfenrir/src/main/jni/animation/thorvg/src/common/tvgInlist.h +++ b/libfenrir/src/main/jni/animation/thorvg/src/common/tvgInlist.h @@ -100,7 +100,7 @@ struct Inlist if (element == tail) tail = element->prev; } - bool empty() + bool empty() const { return head ? false : true; } diff --git a/libfenrir/src/main/jni/animation/thorvg/src/common/tvgMath.cpp b/libfenrir/src/main/jni/animation/thorvg/src/common/tvgMath.cpp index 6c738e290..2b2f1b321 100644 --- a/libfenrir/src/main/jni/animation/thorvg/src/common/tvgMath.cpp +++ b/libfenrir/src/main/jni/animation/thorvg/src/common/tvgMath.cpp @@ -79,7 +79,7 @@ float _bezAt(const Bezier& bz, float at, float length, LengthFunc lineLengthFunc Bezier left; right.split(t, left); length = _bezLength(left, lineLengthFunc); - if (fabsf(length - at) < BEZIER_EPSILON || fabsf(smallest - biggest) < BEZIER_EPSILON) { + if (fabsf(length - at) < BEZIER_EPSILON || fabsf(smallest - biggest) < 1e-3f) { break; } if (length < at) { @@ -212,6 +212,17 @@ Point operator*(const Point& pt, const Matrix& m) } +Point normal(const Point& p1, const Point& p2) +{ + auto dir = p2 - p1; + auto len = length(dir); + if (tvg::zero(len)) return {}; + + auto unitDir = dir / len; + return {-unitDir.y, unitDir.x}; +} + + float Line::length() const { return _lineLength(pt1, pt2); @@ -273,27 +284,27 @@ float Bezier::lengthApprox() const } -void Bezier::split(float at, Bezier& left) +void Bezier::split(float t, Bezier& left) { left.start = start; - left.ctrl1.x = start.x + at * (ctrl1.x - start.x); - left.ctrl1.y = start.y + at * (ctrl1.y - start.y); + left.ctrl1.x = start.x + t * (ctrl1.x - start.x); + left.ctrl1.y = start.y + t * (ctrl1.y - start.y); - left.ctrl2.x = ctrl1.x + at * (ctrl2.x - ctrl1.x); //temporary holding spot - left.ctrl2.y = ctrl1.y + at * (ctrl2.y - ctrl1.y); //temporary holding spot + left.ctrl2.x = ctrl1.x + t * (ctrl2.x - ctrl1.x); //temporary holding spot + left.ctrl2.y = ctrl1.y + t * (ctrl2.y - ctrl1.y); //temporary holding spot - ctrl2.x = ctrl2.x + at * (end.x - ctrl2.x); - ctrl2.y = ctrl2.y + at * (end.y - ctrl2.y); + ctrl2.x = ctrl2.x + t * (end.x - ctrl2.x); + ctrl2.y = ctrl2.y + t * (end.y - ctrl2.y); - ctrl1.x = left.ctrl2.x + at * (ctrl2.x - left.ctrl2.x); - ctrl1.y = left.ctrl2.y + at * (ctrl2.y - left.ctrl2.y); + ctrl1.x = left.ctrl2.x + t * (ctrl2.x - left.ctrl2.x); + ctrl1.y = left.ctrl2.y + t * (ctrl2.y - left.ctrl2.y); - left.ctrl2.x = left.ctrl1.x + at * (left.ctrl2.x - left.ctrl1.x); - left.ctrl2.y = left.ctrl1.y + at * (left.ctrl2.y - left.ctrl1.y); + left.ctrl2.x = left.ctrl1.x + t * (left.ctrl2.x - left.ctrl1.x); + left.ctrl2.y = left.ctrl1.y + t * (left.ctrl2.y - left.ctrl1.y); - left.end.x = start.x = left.ctrl2.x + at * (ctrl1.x - left.ctrl2.x); - left.end.y = start.y = left.ctrl2.y + at * (ctrl1.y - left.ctrl2.y); + left.end.x = start.x = left.ctrl2.x + t * (ctrl1.x - left.ctrl2.x); + left.end.y = start.y = left.ctrl2.y + t * (ctrl1.y - left.ctrl2.y); } diff --git a/libfenrir/src/main/jni/animation/thorvg/src/common/tvgMath.h b/libfenrir/src/main/jni/animation/thorvg/src/common/tvgMath.h index fa8ef706a..71509d575 100644 --- a/libfenrir/src/main/jni/animation/thorvg/src/common/tvgMath.h +++ b/libfenrir/src/main/jni/animation/thorvg/src/common/tvgMath.h @@ -165,7 +165,7 @@ static inline void log(const Matrix& m) void operator*=(Point& pt, const Matrix& m); Point operator*(const Point& pt, const Matrix& m); - +Point normal(const Point& p1, const Point& p2); static inline bool zero(const Point& p) { @@ -264,7 +264,7 @@ struct Bezier Point ctrl2; Point end; - void split(float at, Bezier& left); + void split(float t, Bezier& left); void split(Bezier& left, Bezier& right) const; void split(float at, Bezier& left, Bezier& right) const; float length() const; diff --git a/libfenrir/src/main/jni/animation/thorvg/src/renderer/sw_engine/tvgSwCommon.h b/libfenrir/src/main/jni/animation/thorvg/src/renderer/sw_engine/tvgSwCommon.h index 2289aa0ec..a6db3ec58 100644 --- a/libfenrir/src/main/jni/animation/thorvg/src/renderer/sw_engine/tvgSwCommon.h +++ b/libfenrir/src/main/jni/animation/thorvg/src/renderer/sw_engine/tvgSwCommon.h @@ -515,7 +515,7 @@ bool strokeParseOutline(SwStroke* stroke, const SwOutline& outline); SwOutline* strokeExportOutline(SwStroke* stroke, SwMpool* mpool, unsigned tid); void strokeFree(SwStroke* stroke); -bool imagePrepare(SwImage* image, const RenderMesh* mesh, const Matrix& transform, const SwBBox& clipRegion, SwBBox& renderRegion, SwMpool* mpool, unsigned tid); +bool imagePrepare(SwImage* image, const Matrix& transform, const SwBBox& clipRegion, SwBBox& renderRegion, SwMpool* mpool, unsigned tid); bool imageGenRle(SwImage* image, const SwBBox& renderRegion, bool antiAlias); void imageDelOutline(SwImage* image, SwMpool* mpool, uint32_t tid); void imageReset(SwImage* image); @@ -560,7 +560,7 @@ void mpoolRetDashOutline(SwMpool* mpool, unsigned idx); bool rasterCompositor(SwSurface* surface); bool rasterGradientShape(SwSurface* surface, SwShape* shape, const Fill* fdata, uint8_t opacity); bool rasterShape(SwSurface* surface, SwShape* shape, uint8_t r, uint8_t g, uint8_t b, uint8_t a); -bool rasterImage(SwSurface* surface, SwImage* image, const RenderMesh* mesh, const Matrix& transform, const SwBBox& bbox, uint8_t opacity); +bool rasterImage(SwSurface* surface, SwImage* image, const Matrix& transform, const SwBBox& bbox, uint8_t opacity); bool rasterStroke(SwSurface* surface, SwShape* shape, uint8_t r, uint8_t g, uint8_t b, uint8_t a); bool rasterGradientStroke(SwSurface* surface, SwShape* shape, const Fill* fdata, uint8_t opacity); bool rasterClear(SwSurface* surface, uint32_t x, uint32_t y, uint32_t w, uint32_t h); diff --git a/libfenrir/src/main/jni/animation/thorvg/src/renderer/sw_engine/tvgSwImage.cpp b/libfenrir/src/main/jni/animation/thorvg/src/renderer/sw_engine/tvgSwImage.cpp index 488f3ff31..d2c02bb93 100644 --- a/libfenrir/src/main/jni/animation/thorvg/src/renderer/sw_engine/tvgSwImage.cpp +++ b/libfenrir/src/main/jni/animation/thorvg/src/renderer/sw_engine/tvgSwImage.cpp @@ -34,7 +34,7 @@ static inline bool _onlyShifted(const Matrix& m) } -static bool _genOutline(SwImage* image, const RenderMesh* mesh, const Matrix& transform, SwMpool* mpool, unsigned tid) +static bool _genOutline(SwImage* image, const Matrix& transform, SwMpool* mpool, unsigned tid) { image->outline = mpoolReqOutline(mpool, tid); auto outline = image->outline; @@ -45,48 +45,12 @@ static bool _genOutline(SwImage* image, const RenderMesh* mesh, const Matrix& tr outline->closed.reserve(1); Point to[4]; - if (mesh->triangleCnt > 0) { - // TODO: Optimise me. We appear to calculate this exact min/max bounding area in multiple - // places. We should be able to re-use one we have already done? Also see: - // tvgPicture.h --> bounds - // tvgSwRasterTexmap.h --> _rasterTexmapPolygonMesh - // - // TODO: Should we calculate the exact path(s) of the triangle mesh instead? - // i.e. copy tvgSwShape.capp -> _genOutline? - // - // TODO: Cntrs? - auto triangles = mesh->triangles; - auto min = triangles[0].vertex[0].pt; - auto max = triangles[0].vertex[0].pt; - - for (uint32_t i = 0; i < mesh->triangleCnt; ++i) { - if (triangles[i].vertex[0].pt.x < min.x) min.x = triangles[i].vertex[0].pt.x; - else if (triangles[i].vertex[0].pt.x > max.x) max.x = triangles[i].vertex[0].pt.x; - if (triangles[i].vertex[0].pt.y < min.y) min.y = triangles[i].vertex[0].pt.y; - else if (triangles[i].vertex[0].pt.y > max.y) max.y = triangles[i].vertex[0].pt.y; - - if (triangles[i].vertex[1].pt.x < min.x) min.x = triangles[i].vertex[1].pt.x; - else if (triangles[i].vertex[1].pt.x > max.x) max.x = triangles[i].vertex[1].pt.x; - if (triangles[i].vertex[1].pt.y < min.y) min.y = triangles[i].vertex[1].pt.y; - else if (triangles[i].vertex[1].pt.y > max.y) max.y = triangles[i].vertex[1].pt.y; - - if (triangles[i].vertex[2].pt.x < min.x) min.x = triangles[i].vertex[2].pt.x; - else if (triangles[i].vertex[2].pt.x > max.x) max.x = triangles[i].vertex[2].pt.x; - if (triangles[i].vertex[2].pt.y < min.y) min.y = triangles[i].vertex[2].pt.y; - else if (triangles[i].vertex[2].pt.y > max.y) max.y = triangles[i].vertex[2].pt.y; - } - to[0] = {min.x, min.y}; - to[1] = {max.x, min.y}; - to[2] = {max.x, max.y}; - to[3] = {min.x, max.y}; - } else { - auto w = static_cast(image->w); - auto h = static_cast(image->h); - to[0] = {0, 0}; - to[1] = {w, 0}; - to[2] = {w, h}; - to[3] = {0, h}; - } + auto w = static_cast(image->w); + auto h = static_cast(image->h); + to[0] = {0, 0}; + to[1] = {w, 0}; + to[2] = {w, h}; + to[3] = {0, h}; for (int i = 0; i < 4; i++) { outline->pts.push(mathTransform(&to[i], transform)); @@ -108,7 +72,7 @@ static bool _genOutline(SwImage* image, const RenderMesh* mesh, const Matrix& tr /* External Class Implementation */ /************************************************************************/ -bool imagePrepare(SwImage* image, const RenderMesh* mesh, const Matrix& transform, const SwBBox& clipRegion, SwBBox& renderRegion, SwMpool* mpool, unsigned tid) +bool imagePrepare(SwImage* image, const Matrix& transform, const SwBBox& clipRegion, SwBBox& renderRegion, SwMpool* mpool, unsigned tid) { image->direct = _onlyShifted(transform); @@ -126,7 +90,7 @@ bool imagePrepare(SwImage* image, const RenderMesh* mesh, const Matrix& transfor else image->scaled = false; } - if (!_genOutline(image, mesh, transform, mpool, tid)) return false; + if (!_genOutline(image, transform, mpool, tid)) return false; return mathUpdateOutlineBBox(image->outline, clipRegion, renderRegion, image->direct); } diff --git a/libfenrir/src/main/jni/animation/thorvg/src/renderer/sw_engine/tvgSwRaster.cpp b/libfenrir/src/main/jni/animation/thorvg/src/renderer/sw_engine/tvgSwRaster.cpp index e8eaaf7a8..fe20282aa 100644 --- a/libfenrir/src/main/jni/animation/thorvg/src/renderer/sw_engine/tvgSwRaster.cpp +++ b/libfenrir/src/main/jni/animation/thorvg/src/renderer/sw_engine/tvgSwRaster.cpp @@ -194,10 +194,21 @@ static inline uint8_t _opMaskDifference(uint8_t s, uint8_t d, uint8_t a) } +static inline uint8_t _opMaskLighten(uint8_t s, uint8_t d, uint8_t a) +{ + return (s > d) ? s : d; +} + + +static inline uint8_t _opMaskDarken(uint8_t s, uint8_t d, uint8_t a) +{ + return (s < d) ? s : d; +} + + static inline bool _direct(CompositeMethod method) { - //subtract & Intersect allows the direct composition - if (method == CompositeMethod::SubtractMask || method == CompositeMethod::IntersectMask) return true; + if (method == CompositeMethod::SubtractMask || method == CompositeMethod::IntersectMask || method == CompositeMethod::DarkenMask) return true; return false; } @@ -209,6 +220,8 @@ static inline SwMask _getMaskOp(CompositeMethod method) case CompositeMethod::SubtractMask: return _opMaskSubtract; case CompositeMethod::DifferenceMask: return _opMaskDifference; case CompositeMethod::IntersectMask: return _opMaskIntersect; + case CompositeMethod::LightenMask: return _opMaskLighten; + case CompositeMethod::DarkenMask: return _opMaskDarken; default: return nullptr; } } @@ -1974,13 +1987,12 @@ bool rasterStroke(SwSurface* surface, SwShape* shape, uint8_t r, uint8_t g, uint } -bool rasterImage(SwSurface* surface, SwImage* image, const RenderMesh* mesh, const Matrix& transform, const SwBBox& bbox, uint8_t opacity) +bool rasterImage(SwSurface* surface, SwImage* image, const Matrix& transform, const SwBBox& bbox, uint8_t opacity) { //Outside of the viewport, skip the rendering if (bbox.max.x < 0 || bbox.max.y < 0 || bbox.min.x >= static_cast(surface->w) || bbox.min.y >= static_cast(surface->h)) return true; - if (mesh && mesh->triangleCnt > 0) return _rasterTexmapPolygonMesh(surface, image, mesh, transform, &bbox, opacity); - else return _rasterImage(surface, image, transform, bbox, opacity); + return _rasterImage(surface, image, transform, bbox, opacity); } diff --git a/libfenrir/src/main/jni/animation/thorvg/src/renderer/sw_engine/tvgSwRasterTexmap.h b/libfenrir/src/main/jni/animation/thorvg/src/renderer/sw_engine/tvgSwRasterTexmap.h index c12415342..02dc5099a 100644 --- a/libfenrir/src/main/jni/animation/thorvg/src/renderer/sw_engine/tvgSwRasterTexmap.h +++ b/libfenrir/src/main/jni/animation/thorvg/src/renderer/sw_engine/tvgSwRasterTexmap.h @@ -53,12 +53,10 @@ static bool _arrange(const SwImage* image, const SwBBox* region, int& yStart, in regionBottom = image->rle->spans[image->rle->size - 1].y; } - if (yStart >= regionBottom) return false; - if (yStart < regionTop) yStart = regionTop; if (yEnd > regionBottom) yEnd = regionBottom; - return true; + return yEnd > yStart; } @@ -868,10 +866,8 @@ static void _calcVertCoverage(AALine *lines, int32_t eidx, int32_t y, int32_t re static void _calcHorizCoverage(AALine *lines, int32_t eidx, int32_t y, int32_t x, int32_t x2) { - if (lines[y].length[eidx] < abs(x - x2)) { - lines[y].length[eidx] = abs(x - x2); - lines[y].coverage[eidx] = (255 / (lines[y].length[eidx] + 1)); - } + lines[y].length[eidx] = abs(x - x2); + lines[y].coverage[eidx] = (255 / (lines[y].length[eidx] + 1)); } @@ -897,9 +893,14 @@ static void _calcAAEdge(AASpans *aaSpans, int32_t eidx) ptx[1] = tx[1]; \ } while (0) + struct Point + { + int32_t x, y; + }; + int32_t y = 0; - SwPoint pEdge = {-1, -1}; //previous edge point - SwPoint edgeDiff = {0, 0}; //temporary used for point distance + Point pEdge = {-1, -1}; //previous edge point + Point edgeDiff = {0, 0}; //temporary used for point distance /* store bigger to tx[0] between prev and current edge's x positions. */ int32_t tx[2] = {0, 0}; @@ -1024,6 +1025,7 @@ static void _calcAAEdge(AASpans *aaSpans, int32_t eidx) static bool _apply(SwSurface* surface, AASpans* aaSpans) { + auto end = surface->buf32 + surface->h * surface->stride; auto y = aaSpans->yStart; uint32_t pixel; uint32_t* dst; @@ -1044,8 +1046,13 @@ static bool _apply(SwSurface* surface, AASpans* aaSpans) dst = surface->buf32 + (offset + line->x[0]); if (line->x[0] > 1) pixel = *(dst - 1); else pixel = *dst; - pos = 1; + + //exceptional handling. out of memory bound. + if (dst + line->length[0] >= end) { + pos += (dst + line->length[0] - end); + } + while (pos <= line->length[0]) { *dst = INTERPOLATE(*dst, pixel, line->coverage[0] * pos); ++dst; @@ -1053,17 +1060,21 @@ static bool _apply(SwSurface* surface, AASpans* aaSpans) } //Right edge - dst = surface->buf32 + (offset + line->x[1] - 1); + dst = surface->buf32 + offset + line->x[1] - 1; + if (line->x[1] < (int32_t)(surface->w - 1)) pixel = *(dst + 1); else pixel = *dst; + pos = line->length[1]; - pos = width; - while ((int32_t)(width - line->length[1]) < pos) { - *dst = INTERPOLATE(*dst, pixel, 255 - (line->coverage[1] * (line->length[1] - (width - pos)))); + //exceptional handling. out of memory bound. + if (dst - pos < surface->buf32) --pos; + + while (pos > 0) { + *dst = INTERPOLATE(*dst, pixel, 255 - (line->coverage[1] * pos)); --dst; --pos; } - } + } y++; } @@ -1134,69 +1145,4 @@ static bool _rasterTexmapPolygon(SwSurface* surface, const SwImage* image, const } #endif return _apply(surface, aaSpans); -} - - -/* - Provide any number of triangles to draw a mesh using the supplied image. - Indexes are not used, so each triangle (Polygon) vertex has to be defined, even if they copy the previous one. - Example: - - 0 -- 1 0 -- 1 0 - | / | --> | / / | - | / | | / / | - 2 -- 3 2 1 -- 2 - - Should provide two Polygons, one for each triangle. - // TODO: region? -*/ -static bool _rasterTexmapPolygonMesh(SwSurface* surface, const SwImage* image, const RenderMesh* mesh, const Matrix& transform, const SwBBox* region, uint8_t opacity) -{ - if (surface->channelSize == sizeof(uint8_t)) { - TVGERR("SW_ENGINE", "Not supported grayscale Textmap polygon mesh!"); - return false; - } - - //Exceptions: No dedicated drawing area? - if ((!image->rle && !region) || (image->rle && image->rle->size == 0)) return false; - - // Step polygons once to transform - auto transformedTris = (Polygon*)malloc(sizeof(Polygon) * mesh->triangleCnt); - float ys = FLT_MAX, ye = -1.0f; - for (uint32_t i = 0; i < mesh->triangleCnt; i++) { - transformedTris[i] = mesh->triangles[i]; - transformedTris[i].vertex[0].pt *= transform; - transformedTris[i].vertex[1].pt *= transform; - transformedTris[i].vertex[2].pt *= transform; - - if (transformedTris[i].vertex[0].pt.y < ys) ys = transformedTris[i].vertex[0].pt.y; - else if (transformedTris[i].vertex[0].pt.y > ye) ye = transformedTris[i].vertex[0].pt.y; - if (transformedTris[i].vertex[1].pt.y < ys) ys = transformedTris[i].vertex[1].pt.y; - else if (transformedTris[i].vertex[1].pt.y > ye) ye = transformedTris[i].vertex[1].pt.y; - if (transformedTris[i].vertex[2].pt.y < ys) ys = transformedTris[i].vertex[2].pt.y; - else if (transformedTris[i].vertex[2].pt.y > ye) ye = transformedTris[i].vertex[2].pt.y; - - // Convert normalized UV coordinates to image coordinates - transformedTris[i].vertex[0].uv.x *= (float)image->w; - transformedTris[i].vertex[0].uv.y *= (float)image->h; - transformedTris[i].vertex[1].uv.x *= (float)image->w; - transformedTris[i].vertex[1].uv.y *= (float)image->h; - transformedTris[i].vertex[2].uv.x *= (float)image->w; - transformedTris[i].vertex[2].uv.y *= (float)image->h; - } - - // Get AA spans and step polygons again to draw - if (auto aaSpans = _AASpans(ys, ye, image, region)) { - for (uint32_t i = 0; i < mesh->triangleCnt; i++) { - _rasterPolygonImage(surface, image, region, transformedTris[i], aaSpans, opacity); - } -#if 0 - if (_compositing(surface) && _masking(surface) && !_direct(surface->compositor->method)) { - _compositeMaskImage(surface, &surface->compositor->image, surface->compositor->bbox); - } -#endif - _apply(surface, aaSpans); - } - free(transformedTris); - return true; -} +} \ No newline at end of file diff --git a/libfenrir/src/main/jni/animation/thorvg/src/renderer/sw_engine/tvgSwRenderer.cpp b/libfenrir/src/main/jni/animation/thorvg/src/renderer/sw_engine/tvgSwRenderer.cpp index 2e707a880..4e30490be 100644 --- a/libfenrir/src/main/jni/animation/thorvg/src/renderer/sw_engine/tvgSwRenderer.cpp +++ b/libfenrir/src/main/jni/animation/thorvg/src/renderer/sw_engine/tvgSwRenderer.cpp @@ -199,64 +199,10 @@ struct SwShapeTask : SwTask }; -struct SwSceneTask : SwTask -{ - Array scene; //list of paints render data (SwTask) - SwRle* sceneRle = nullptr; - - bool clip(SwRle* target) override - { - //Only one shape - if (scene.count == 1) { - return static_cast(*scene.data)->clip(target); - } - - //More than one shapes - if (sceneRle) rleClipPath(target, sceneRle); - else TVGLOG("SW_ENGINE", "No clippers in a scene?"); - - return true; - } - - SwRle* rle() override - { - return sceneRle; - } - - void run(unsigned tid) override - { - //TODO: Skip the run if the scene hasn't changed. - if (!sceneRle) sceneRle = static_cast(calloc(1, sizeof(SwRle))); - else rleReset(sceneRle); - - //Merge shapes if it has more than one shapes - if (scene.count > 1) { - //Merge first two clippers - auto clipper1 = static_cast(*scene.data); - auto clipper2 = static_cast(*(scene.data + 1)); - - rleMerge(sceneRle, clipper1->rle(), clipper2->rle()); - - //Unify the remained clippers - for (auto rd = scene.begin() + 2; rd < scene.end(); ++rd) { - auto clipper = static_cast(*rd); - rleMerge(sceneRle, sceneRle, clipper->rle()); - } - } - } - - void dispose() override - { - rleFree(sceneRle); - } -}; - - struct SwImageTask : SwTask { SwImage image; Surface* source; //Image source - const RenderMesh* mesh = nullptr; //Should be valid ptr in action bool clip(SwRle* target) override { @@ -289,10 +235,9 @@ struct SwImageTask : SwTask imageReset(&image); if (!image.data || image.w == 0 || image.h == 0) goto end; - if (!imagePrepare(&image, mesh, transform, clipRegion, bbox, mpool, tid)) goto end; + if (!imagePrepare(&image, transform, clipRegion, bbox, mpool, tid)) goto end; - // TODO: How do we clip the triangle mesh? Only clip non-meshed images for now - if (mesh->triangleCnt == 0 && clips.count > 0) { + if (clips.count > 0) { if (!imageGenRle(&image, bbox, false)) goto end; if (image.rle) { //Clear current task memorypool here if the clippers would use the same memory pool @@ -471,7 +416,7 @@ bool SwRenderer::renderImage(RenderData data) if (task->opacity == 0) return true; - return rasterImage(surface, &task->image, task->mesh, task->transform, task->bbox, task->opacity); + return rasterImage(surface, &task->image, task->transform, task->bbox, task->opacity); } @@ -680,7 +625,7 @@ bool SwRenderer::endComposite(Compositor* cmp) //Default is alpha blending if (p->method == CompositeMethod::None) { Matrix m = {1, 0, 0, 0, 1, 0, 0, 0, 1}; - return rasterImage(surface, &p->image, nullptr, m, p->bbox, p->opacity); + return rasterImage(surface, &p->image, m, p->bbox, p->opacity); } return true; @@ -745,7 +690,7 @@ void* SwRenderer::prepareCommon(SwTask* task, const Matrix& transform, const Arr } -RenderData SwRenderer::prepare(Surface* surface, const RenderMesh* mesh, RenderData data, const Matrix& transform, Array& clips, uint8_t opacity, RenderUpdateFlag flags) +RenderData SwRenderer::prepare(Surface* surface, RenderData data, const Matrix& transform, Array& clips, uint8_t opacity, RenderUpdateFlag flags) { //prepare task auto task = static_cast(data); @@ -753,27 +698,6 @@ RenderData SwRenderer::prepare(Surface* surface, const RenderMesh* mesh, RenderD else task->done(); task->source = surface; - task->mesh = mesh; - - return prepareCommon(task, transform, clips, opacity, flags); -} - - -RenderData SwRenderer::prepare(const Array& scene, RenderData data, const Matrix& transform, Array& clips, uint8_t opacity, RenderUpdateFlag flags) -{ - //prepare task - auto task = static_cast(data); - if (!task) task = new SwSceneTask; - else task->done(); - - task->scene = scene; - - //TODO: Failed threading them. It would be better if it's possible. - //See: https://github.com/thorvg/thorvg/issues/1409 - //Guarantee composition targets get ready. - for (auto task = scene.begin(); task < scene.end(); ++task) { - static_cast(*task)->done(); - } return prepareCommon(task, transform, clips, opacity, flags); } diff --git a/libfenrir/src/main/jni/animation/thorvg/src/renderer/sw_engine/tvgSwRenderer.h b/libfenrir/src/main/jni/animation/thorvg/src/renderer/sw_engine/tvgSwRenderer.h index 2ee462085..fcd8ad462 100644 --- a/libfenrir/src/main/jni/animation/thorvg/src/renderer/sw_engine/tvgSwRenderer.h +++ b/libfenrir/src/main/jni/animation/thorvg/src/renderer/sw_engine/tvgSwRenderer.h @@ -37,8 +37,7 @@ class SwRenderer : public RenderMethod { public: RenderData prepare(const RenderShape& rshape, RenderData data, const Matrix& transform, Array& clips, uint8_t opacity, RenderUpdateFlag flags, bool clipper) override; - RenderData prepare(const Array& scene, RenderData data, const Matrix& transform, Array& clips, uint8_t opacity, RenderUpdateFlag flags) override; - RenderData prepare(Surface* surface, const RenderMesh* mesh, RenderData data, const Matrix& transform, Array& clips, uint8_t opacity, RenderUpdateFlag flags) override; + RenderData prepare(Surface* surface, RenderData data, const Matrix& transform, Array& clips, uint8_t opacity, RenderUpdateFlag flags) override; bool preRender() override; bool renderShape(RenderData data) override; bool renderImage(RenderData data) override; diff --git a/libfenrir/src/main/jni/animation/thorvg/src/renderer/sw_engine/tvgSwRle.cpp b/libfenrir/src/main/jni/animation/thorvg/src/renderer/sw_engine/tvgSwRle.cpp index f7b105f5a..cb73e599a 100644 --- a/libfenrir/src/main/jni/animation/thorvg/src/renderer/sw_engine/tvgSwRle.cpp +++ b/libfenrir/src/main/jni/animation/thorvg/src/renderer/sw_engine/tvgSwRle.cpp @@ -822,46 +822,6 @@ static SwSpan* _intersectSpansRect(const SwBBox *bbox, const SwRle *targetRle, S } -static SwSpan* _mergeSpansRegion(const SwRle *clip1, const SwRle *clip2, SwSpan *outSpans) -{ - auto out = outSpans; - auto spans1 = clip1->spans; - auto end1 = clip1->spans + clip1->size; - auto spans2 = clip2->spans; - auto end2 = clip2->spans + clip2->size; - - //list two spans up in y order - //TODO: Remove duplicated regions? - while (spans1 < end1 && spans2 < end2) { - while (spans1 < end1 && spans1->y <= spans2->y) { - *out = *spans1; - ++spans1; - ++out; - } - if (spans1 >= end1) break; - while (spans2 < end2 && spans2->y <= spans1->y) { - *out = *spans2; - ++spans2; - ++out; - } - } - - //Leftovers - while (spans1 < end1) { - *out = *spans1; - ++spans1; - ++out; - } - while (spans2 < end2) { - *out = *spans2; - ++spans2; - ++out; - } - - return out; -} - - void _replaceClipSpan(SwRle *rle, SwSpan* clippedSpans, uint32_t size) { free(rle->spans); @@ -1030,45 +990,6 @@ void rleFree(SwRle* rle) } -void rleMerge(SwRle* rle, SwRle* clip1, SwRle* clip2) -{ - if (!rle || (!clip1 && !clip2)) return; - if (clip1 && clip1->size == 0 && clip2 && clip2->size == 0) return; - - TVGLOG("SW_ENGINE", "Unifying Rle!"); - - //clip1 is empty, just copy clip2 - if (!clip1 || clip1->size == 0) { - if (clip2) { - auto spans = static_cast(malloc(sizeof(SwSpan) * (clip2->size))); - memcpy(spans, clip2->spans, clip2->size); - _replaceClipSpan(rle, spans, clip2->size); - } else { - _replaceClipSpan(rle, nullptr, 0); - } - return; - } - - //clip2 is empty, just copy clip1 - if (!clip2 || clip2->size == 0) { - if (clip1) { - auto spans = static_cast(malloc(sizeof(SwSpan) * (clip1->size))); - memcpy(spans, clip1->spans, clip1->size); - _replaceClipSpan(rle, spans, clip1->size); - } else { - _replaceClipSpan(rle, nullptr, 0); - } - return; - } - - auto spanCnt = clip1->size + clip2->size; - auto spans = static_cast(malloc(sizeof(SwSpan) * spanCnt)); - auto spansEnd = _mergeSpansRegion(clip1, clip2, spans); - - _replaceClipSpan(rle, spans, spansEnd - spans); -} - - void rleClipPath(SwRle *rle, const SwRle *clip) { if (rle->size == 0 || clip->size == 0) return; diff --git a/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgAccessor.cpp b/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgAccessor.cpp index 903437f29..a14472680 100644 --- a/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgAccessor.cpp +++ b/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgAccessor.cpp @@ -21,20 +21,21 @@ */ #include "tvgIteratorAccessor.h" +#include "tvgCompressor.h" /************************************************************************/ /* Internal Class Implementation */ /************************************************************************/ -static bool accessChildren(Iterator* it, function func) +static bool accessChildren(Iterator* it, function func, void* data) { while (auto child = it->next()) { //Access the child - if (!func(child)) return false; + if (!func(child, data)) return false; //Access the children of the child if (auto it2 = IteratorAccessor::iterator(child)) { - if (!accessChildren(it2, func)) { + if (!accessChildren(it2, func, data)) { delete(it2); return false; } @@ -44,26 +45,46 @@ static bool accessChildren(Iterator* it, function func return true; } + /************************************************************************/ /* External Class Implementation */ /************************************************************************/ -unique_ptr Accessor::set(unique_ptr picture, function func) noexcept +TVG_DEPRECATED unique_ptr Accessor::set(unique_ptr picture, function func) noexcept +{ + auto backward = [](const tvg::Paint* paint, void* data) -> bool + { + auto func = reinterpret_cast*>(data); + if (!(*func)(paint)) return false; + return true; + }; + + set(picture.get(), backward, reinterpret_cast(&func)); + return picture; +} + + +Result Accessor::set(const Picture* picture, function func, void* data) noexcept { - auto p = picture.get(); - if (!p || !func) return picture; + if (!picture || !func) return Result::InvalidArguments; //Use the Preorder Tree-Search //Root - if (!func(p)) return picture; + if (!func(picture, data)) return Result::Success; //Children - if (auto it = IteratorAccessor::iterator(p)) { - accessChildren(it, func); + if (auto it = IteratorAccessor::iterator(picture)) { + accessChildren(it, func, data); delete(it); } - return picture; + return Result::Success; +} + + +uint32_t Accessor::id(const char* name) noexcept +{ + return djb2Encode(name); } diff --git a/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgLoadModule.h b/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgLoadModule.h index 84d35bc0c..c573285c2 100644 --- a/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgLoadModule.h +++ b/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgLoadModule.h @@ -101,7 +101,8 @@ struct FontLoader : LoadModule FontLoader(FileType type) : LoadModule(type) {} - virtual bool request(Shape* shape, char* text, bool italic = false) = 0; + virtual bool request(Shape* shape, char* text) = 0; + virtual bool transform(Paint* paint, float fontSize, bool italic) = 0; }; #endif //_TVG_LOAD_MODULE_H_ diff --git a/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgPaint.cpp b/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgPaint.cpp index 46e62116d..6d6361cee 100644 --- a/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgPaint.cpp +++ b/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgPaint.cpp @@ -292,8 +292,9 @@ RenderData Paint::Impl::update(RenderMethod* renderer, const Matrix& pm, Arrayopacity); RenderData rd = nullptr; - auto m = pm * tr.m; - PAINT_METHOD(rd, update(renderer, m, clips, opacity, newFlag, clipper)); + + tr.cm = pm * tr.m; + PAINT_METHOD(rd, update(renderer, tr.cm, clips, opacity, newFlag, clipper)); /* 3. Composition Post Processing */ if (compFastTrack == Result::Success) renderer->viewport(viewport); @@ -303,13 +304,13 @@ RenderData Paint::Impl::update(RenderMethod* renderer, const Matrix& pm, Arraytransform(); + const auto& m = this->transform(origin); //Case: No transformed, quick return! - if (!transformed || tvg::identity(&m)) { + if (!transformed || identity(&m)) { PAINT_METHOD(ret, bounds(x, y, w, h, stroking)); return ret; } @@ -350,6 +351,26 @@ bool Paint::Impl::bounds(float* x, float* y, float* w, float* h, bool transforme } +void Paint::Impl::reset() +{ + if (compData) { + if (P(compData->target)->unref() == 0) delete(compData->target); + free(compData); + compData = nullptr; + } + tvg::identity(&tr.m); + tr.degree = 0.0f; + tr.scale = 1.0f; + tr.overriding = false; + + blendMethod = BlendMethod::Normal; + renderFlag = RenderUpdateFlag::None; + ctxFlag = ContextFlag::Invalid; + opacity = 255; + paint->id = 0; +} + + /************************************************************************/ /* External Class Implementation */ /************************************************************************/ @@ -401,7 +422,7 @@ Matrix Paint::transform() noexcept Result Paint::bounds(float* x, float* y, float* w, float* h, bool transformed) const noexcept { - if (pImpl->bounds(x, y, w, h, transformed, true)) return Result::Success; + if (pImpl->bounds(x, y, w, h, transformed, true, transformed)) return Result::Success; return Result::InsufficientCondition; } @@ -414,6 +435,11 @@ Paint* Paint::duplicate() const noexcept Result Paint::composite(std::unique_ptr target, CompositeMethod method) noexcept { + if (method == CompositeMethod::ClipPath && target && target->type() != Type::Shape) { + TVGERR("RENDERER", "ClipPath only allows the Shape!"); + return Result::NonSupport; + } + auto p = target.release(); if (pImpl->composite(this, p, method)) return Result::Success; delete(p); @@ -456,7 +482,7 @@ TVG_DEPRECATED uint32_t Paint::identifier() const noexcept } -Result Paint::blend(BlendMethod method) const noexcept +Result Paint::blend(BlendMethod method) noexcept { if (pImpl->blendMethod != method) { pImpl->blendMethod = method; @@ -470,4 +496,4 @@ Result Paint::blend(BlendMethod method) const noexcept BlendMethod Paint::blend() const noexcept { return pImpl->blendMethod; -} +} \ No newline at end of file diff --git a/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgPaint.h b/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgPaint.h index 0d0882f41..6c3ec6f74 100644 --- a/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgPaint.h +++ b/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgPaint.h @@ -50,12 +50,12 @@ namespace tvg Paint* paint = nullptr; Composite* compData = nullptr; RenderMethod* renderer = nullptr; - BlendMethod blendMethod = BlendMethod::Normal; //uint8_t struct { - Matrix m; - float degree = 0.0f; //rotation degree - float scale = 1.0f; //scale factor - bool overriding = false; //user transform? + Matrix m; //input matrix + Matrix cm; //multipled parents matrix + float degree; //rotation degree + float scale; //scale factor + bool overriding; //user transform? void update() { @@ -71,14 +71,15 @@ namespace tvg tvg::rotate(&m, degree); } } tr; - uint8_t renderFlag = RenderUpdateFlag::None; - uint8_t ctxFlag = ContextFlag::Invalid; - uint8_t opacity = 255; + BlendMethod blendMethod; + uint8_t renderFlag; + uint8_t ctxFlag; + uint8_t opacity; uint8_t refCnt = 0; //reference count Impl(Paint* pnt) : paint(pnt) { - identity(&tr.m); + reset(); } ~Impl() @@ -111,10 +112,11 @@ namespace tvg return true; } - Matrix& transform() + Matrix& transform(bool origin = false) { //update transform if (renderFlag & RenderUpdateFlag::Transform) tr.update(); + if (origin) return tr.cm; return tr.m; } @@ -150,10 +152,11 @@ namespace tvg bool rotate(float degree); bool scale(float factor); bool translate(float x, float y); - bool bounds(float* x, float* y, float* w, float* h, bool transformed, bool stroking); + bool bounds(float* x, float* y, float* w, float* h, bool transformed, bool stroking, bool origin = false); RenderData update(RenderMethod* renderer, const Matrix& pm, Array& clips, uint8_t opacity, RenderUpdateFlag pFlag, bool clipper = false); bool render(RenderMethod* renderer); Paint* duplicate(Paint* ret = nullptr); + void reset(); }; } diff --git a/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgPicture.cpp b/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgPicture.cpp index 4b846dbe7..6ed151e57 100644 --- a/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgPicture.cpp +++ b/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgPicture.cpp @@ -199,18 +199,24 @@ Result Picture::size(float* w, float* h) const noexcept } -Result Picture::mesh(const Polygon* triangles, uint32_t triangleCnt) noexcept -{ - if (!triangles && triangleCnt > 0) return Result::InvalidArguments; - if (triangles && triangleCnt == 0) return Result::InvalidArguments; - - pImpl->mesh(triangles, triangleCnt); - return Result::Success; -} - +const Paint* Picture::paint(uint32_t id) noexcept +{ + struct Value + { + uint32_t id; + const Paint* ret; + } value = {id, nullptr}; + + auto cb = [](const tvg::Paint* paint, void* data) -> bool + { + auto p = static_cast(data); + if (p->id == paint->id) { + p->ret = paint; + return false; + } + return true; + }; -uint32_t Picture::mesh(const Polygon** triangles) const noexcept -{ - if (triangles) *triangles = pImpl->rm.triangles; - return pImpl->rm.triangleCnt; -} + tvg::Accessor::gen()->set(this, cb, &value); + return value.ret; +} \ No newline at end of file diff --git a/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgPicture.h b/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgPicture.h index 1b03843bb..8262e8bf5 100644 --- a/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgPicture.h +++ b/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgPicture.h @@ -63,7 +63,6 @@ struct Picture::Impl Surface* surface = nullptr; //bitmap picture uses RenderData rd = nullptr; //engine data float w = 0, h = 0; - RenderMesh rm; //mesh data Picture* picture = nullptr; bool resizing = false; bool needComp = false; //need composition @@ -89,7 +88,7 @@ struct Picture::Impl delete(paint); } - RenderData update(RenderMethod* renderer, const Matrix& transform, Array& clips, uint8_t opacity, RenderUpdateFlag pFlag, bool clipper) + RenderData update(RenderMethod* renderer, const Matrix& transform, Array& clips, uint8_t opacity, RenderUpdateFlag pFlag, TVG_UNUSED bool clipper) { auto flag = static_cast(pFlag | load()); @@ -102,51 +101,24 @@ struct Picture::Impl auto scale = sx < sy ? sx : sy; auto m = transform * Matrix{scale, 0, 0, 0, scale, 0, 0, 0, 1}; - rd = renderer->prepare(surface, &rm, rd, m, clips, opacity, flag); + rd = renderer->prepare(surface, rd, m, clips, opacity, flag); } else if (paint) { if (resizing) { loader->resize(paint, w, h); resizing = false; } needComp = needComposition(opacity) ? true : false; - rd = paint->pImpl->update(renderer, transform, clips, opacity, flag, clipper); + rd = paint->pImpl->update(renderer, transform, clips, opacity, flag, false); } return rd; } bool bounds(float* x, float* y, float* w, float* h, bool stroking) { - if (rm.triangleCnt > 0) { - auto triangles = rm.triangles; - auto min = triangles[0].vertex[0].pt; - auto max = triangles[0].vertex[0].pt; - - for (uint32_t i = 0; i < rm.triangleCnt; ++i) { - if (triangles[i].vertex[0].pt.x < min.x) min.x = triangles[i].vertex[0].pt.x; - else if (triangles[i].vertex[0].pt.x > max.x) max.x = triangles[i].vertex[0].pt.x; - if (triangles[i].vertex[0].pt.y < min.y) min.y = triangles[i].vertex[0].pt.y; - else if (triangles[i].vertex[0].pt.y > max.y) max.y = triangles[i].vertex[0].pt.y; - - if (triangles[i].vertex[1].pt.x < min.x) min.x = triangles[i].vertex[1].pt.x; - else if (triangles[i].vertex[1].pt.x > max.x) max.x = triangles[i].vertex[1].pt.x; - if (triangles[i].vertex[1].pt.y < min.y) min.y = triangles[i].vertex[1].pt.y; - else if (triangles[i].vertex[1].pt.y > max.y) max.y = triangles[i].vertex[1].pt.y; - - if (triangles[i].vertex[2].pt.x < min.x) min.x = triangles[i].vertex[2].pt.x; - else if (triangles[i].vertex[2].pt.x > max.x) max.x = triangles[i].vertex[2].pt.x; - if (triangles[i].vertex[2].pt.y < min.y) min.y = triangles[i].vertex[2].pt.y; - else if (triangles[i].vertex[2].pt.y > max.y) max.y = triangles[i].vertex[2].pt.y; - } - if (x) *x = min.x; - if (y) *y = min.y; - if (w) *w = max.x - min.x; - if (h) *h = max.y - min.y; - } else { - if (x) *x = 0; - if (y) *y = 0; - if (w) *w = this->w; - if (h) *h = this->h; - } + if (x) *x = 0; + if (y) *y = 0; + if (w) *w = this->w; + if (h) *h = this->h; return true; } @@ -181,19 +153,6 @@ struct Picture::Impl return load(loader); } - void mesh(const Polygon* triangles, const uint32_t triangleCnt) - { - if (triangles && triangleCnt > 0) { - this->rm.triangleCnt = triangleCnt; - this->rm.triangles = (Polygon*)malloc(sizeof(Polygon) * triangleCnt); - memcpy(this->rm.triangles, triangles, sizeof(Polygon) * triangleCnt); - } else { - free(this->rm.triangles); - this->rm.triangles = nullptr; - this->rm.triangleCnt = 0; - } - } - Paint* duplicate(Paint* ret) { if (ret) TVGERR("RENDERER", "TODO: duplicate()"); @@ -216,12 +175,6 @@ struct Picture::Impl dup->h = h; dup->resizing = resizing; - if (rm.triangleCnt > 0) { - dup->rm.triangleCnt = rm.triangleCnt; - dup->rm.triangles = (Polygon*)malloc(sizeof(Polygon) * rm.triangleCnt); - memcpy(dup->rm.triangles, rm.triangles, sizeof(Polygon) * rm.triangleCnt); - } - return picture; } diff --git a/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgRender.h b/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgRender.h index a78c65953..5ac7e804f 100644 --- a/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgRender.h +++ b/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgRender.h @@ -84,15 +84,15 @@ struct Compositor uint8_t opacity; }; -struct RenderMesh +struct Vertex { - Polygon* triangles = nullptr; - uint32_t triangleCnt = 0; + Point pt; + Point uv; +}; - ~RenderMesh() - { - free(triangles); - } +struct Polygon +{ + Vertex vertex[3]; }; struct RenderRegion @@ -109,7 +109,6 @@ struct RenderRegion } }; - struct RenderStroke { float width = 0.0f; @@ -286,8 +285,7 @@ class RenderMethod virtual ~RenderMethod() {} virtual RenderData prepare(const RenderShape& rshape, RenderData data, const Matrix& transform, Array& clips, uint8_t opacity, RenderUpdateFlag flags, bool clipper) = 0; - virtual RenderData prepare(const Array& scene, RenderData data, const Matrix& transform, Array& clips, uint8_t opacity, RenderUpdateFlag flags) = 0; - virtual RenderData prepare(Surface* surface, const RenderMesh* mesh, RenderData data, const Matrix& transform, Array& clips, uint8_t opacity, RenderUpdateFlag flags) = 0; + virtual RenderData prepare(Surface* surface, RenderData data, const Matrix& transform, Array& clips, uint8_t opacity, RenderUpdateFlag flags) = 0; virtual bool preRender() = 0; virtual bool renderShape(RenderData data) = 0; virtual bool renderImage(RenderData data) = 0; @@ -321,6 +319,8 @@ static inline bool MASK_REGION_MERGING(CompositeMethod method) //these might expand the rendering region case CompositeMethod::AddMask: case CompositeMethod::DifferenceMask: + case CompositeMethod::LightenMask: + case CompositeMethod::DarkenMask: return true; default: TVGERR("RENDERER", "Unsupported Composite Method! = %d", (int)method); @@ -354,6 +354,8 @@ static inline ColorSpace COMPOSITE_TO_COLORSPACE(RenderMethod* renderer, Composi case CompositeMethod::DifferenceMask: case CompositeMethod::SubtractMask: case CompositeMethod::IntersectMask: + case CompositeMethod::LightenMask: + case CompositeMethod::DarkenMask: return ColorSpace::Grayscale8; //TODO: Optimize Luma/InvLuma colorspace to Grayscale8 case CompositeMethod::LumaMask: diff --git a/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgScene.h b/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgScene.h index 7965bd5b0..c9a6c8e1f 100644 --- a/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgScene.h +++ b/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgScene.h @@ -101,7 +101,7 @@ struct Scene::Impl return true; } - RenderData update(RenderMethod* renderer, const Matrix& transform, Array& clips, uint8_t opacity, RenderUpdateFlag flag, bool clipper) + RenderData update(RenderMethod* renderer, const Matrix& transform, Array& clips, uint8_t opacity, RenderUpdateFlag flag, TVG_UNUSED bool clipper) { if ((needComp = needComposition(opacity))) { /* Overriding opacity value. If this scene is half-translucent, @@ -109,20 +109,10 @@ struct Scene::Impl this->opacity = opacity; opacity = 255; } - - if (clipper) { - Array rds(paints.size()); - for (auto paint : paints) { - rds.push(paint->pImpl->update(renderer, transform, clips, opacity, flag, true)); - } - rd = renderer->prepare(rds, rd, transform, clips, opacity, flag); - return rd; - } else { - for (auto paint : paints) { - paint->pImpl->update(renderer, transform, clips, opacity, flag, false); - } - return nullptr; + for (auto paint : paints) { + paint->pImpl->update(renderer, transform, clips, opacity, flag, false); } + return nullptr; } bool render(RenderMethod* renderer) diff --git a/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgShape.cpp b/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgShape.cpp index 640b1de18..282afb042 100644 --- a/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgShape.cpp +++ b/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgShape.cpp @@ -156,7 +156,7 @@ Result Shape::appendCircle(float cx, float cy, float rx, float ry) noexcept } -Result Shape::appendArc(float cx, float cy, float radius, float startAngle, float sweep, bool pie) noexcept +TVG_DEPRECATED Result Shape::appendArc(float cx, float cy, float radius, float startAngle, float sweep, bool pie) noexcept { //just circle if (sweep >= 360.0f || sweep <= -360.0f) return appendCircle(cx, cy, radius, radius); diff --git a/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgShape.h b/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgShape.h index 2d5fd0157..a1e96a4af 100644 --- a/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgShape.h +++ b/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgShape.h @@ -379,6 +379,22 @@ struct Shape::Impl return shape; } + void reset() + { + PP(shape)->reset(); + rs.path.cmds.clear(); + rs.path.pts.clear(); + + rs.color[3] = 0; + rs.rule = FillRule::Winding; + + delete(rs.stroke); + rs.stroke = nullptr; + + delete(rs.fill); + rs.fill = nullptr; + } + Iterator* iterator() { return nullptr; diff --git a/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgText.cpp b/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgText.cpp index 6d509235f..b324b9504 100644 --- a/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgText.cpp +++ b/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgText.cpp @@ -35,7 +35,7 @@ /************************************************************************/ -Text::Text() : pImpl(new Impl) +Text::Text() : pImpl(new Impl(this)) { } @@ -94,20 +94,13 @@ Result Text::unload(const std::string& path) noexcept Result Text::fill(uint8_t r, uint8_t g, uint8_t b) noexcept { - if (!pImpl->paint) return Result::InsufficientCondition; - - return pImpl->fill(r, g, b); + return pImpl->shape->fill(r, g, b); } Result Text::fill(unique_ptr f) noexcept { - if (!pImpl->paint) return Result::InsufficientCondition; - - auto p = f.release(); - if (!p) return Result::MemoryCorruption; - - return pImpl->fill(p); + return pImpl->shape->fill(std::move(f)); } diff --git a/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgText.h b/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgText.h index 02c9ddf05..76a1b30ea 100644 --- a/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgText.h +++ b/libfenrir/src/main/jni/animation/thorvg/src/renderer/tvgText.h @@ -26,37 +26,27 @@ #include #include "tvgShape.h" #include "tvgFill.h" - -#ifdef THORVG_TTF_LOADER_SUPPORT - #include "tvgTtfLoader.h" -#else - #include "tvgLoader.h" -#endif +#include "tvgLoader.h" struct Text::Impl { FontLoader* loader = nullptr; - Shape* paint = nullptr; + Text* paint; + Shape* shape; char* utf8 = nullptr; float fontSize; bool italic = false; bool changed = false; - ~Impl() - { - free(utf8); - LoaderMgr::retrieve(loader); - delete(paint); - } - - Result fill(uint8_t r, uint8_t g, uint8_t b) + Impl(Text* p) : paint(p), shape(Shape::gen().release()) { - return paint->fill(r, g, b); } - Result fill(Fill* f) + ~Impl() { - return paint->fill(cast(f)); + free(utf8); + LoaderMgr::retrieve(loader); + delete(shape); } Result text(const char* utf8) @@ -74,6 +64,11 @@ struct Text::Impl auto loader = LoaderMgr::loader(name); if (!loader) return Result::InsufficientCondition; + if (style && strstr(style, "italic")) italic = true; + else italic = false; + + fontSize = size; + //Same resource has been loaded. if (this->loader == loader) { this->loader->sharing--; //make it sure the reference counting. @@ -83,50 +78,40 @@ struct Text::Impl } this->loader = static_cast(loader); - if (!paint) paint = Shape::gen().release(); - - fontSize = size; - if (style && strstr(style, "italic")) italic = true; changed = true; return Result::Success; } RenderRegion bounds(RenderMethod* renderer) { - if (paint) return P(paint)->bounds(renderer); - else return {0, 0, 0, 0}; + return P(shape)->bounds(renderer); } bool render(RenderMethod* renderer) { - if (paint) return PP(paint)->render(renderer); - return true; + return PP(shape)->render(renderer); } bool load() { if (!loader) return false; + loader->request(shape, utf8); //reload if (changed) { - loader->request(paint, utf8, italic); loader->read(); changed = false; } - if (paint) { - loader->resize(paint, fontSize, fontSize); - return true; - } - return false; + return loader->transform(shape, fontSize, italic); } - RenderData update(RenderMethod* renderer, const Matrix& transform, Array& clips, uint8_t opacity, RenderUpdateFlag pFlag, bool clipper) + RenderData update(RenderMethod* renderer, const Matrix& transform, Array& clips, uint8_t opacity, RenderUpdateFlag pFlag, TVG_UNUSED bool clipper) { if (!load()) return nullptr; //transform the gradient coordinates based on the final scaled font. - auto fill = P(paint)->rs.fill; - if (fill && P(paint)->flag & RenderUpdateFlag::Gradient) { + auto fill = P(shape)->rs.fill; + if (fill && P(shape)->flag & RenderUpdateFlag::Gradient) { auto scale = 1.0f / loader->scale; if (fill->type() == Type::LinearGradient) { P(static_cast(fill))->x1 *= scale; @@ -142,13 +127,13 @@ struct Text::Impl P(static_cast(fill))->fr *= scale; } } - return PP(paint)->update(renderer, transform, clips, opacity, pFlag, clipper); + return PP(shape)->update(renderer, transform, clips, opacity, pFlag, false); } bool bounds(float* x, float* y, float* w, float* h, TVG_UNUSED bool stroking) { - if (!load() || !paint) return false; - paint->bounds(x, y, w, h, true); + if (!load()) return false; + PP(shape)->bounds(x, y, w, h, true, true, false); return true; } @@ -160,7 +145,7 @@ struct Text::Impl auto text = Text::gen().release(); auto dup = text->pImpl; - if (paint) dup->paint = static_cast(paint->duplicate()); + P(shape)->duplicate(dup->shape); if (loader) { dup->loader = loader; diff --git a/libfenrir/src/main/jni/audio/taglib/toolkit/taglib.h b/libfenrir/src/main/jni/audio/taglib/toolkit/taglib.h index a6e9cf2f5..cd1635350 100644 --- a/libfenrir/src/main/jni/audio/taglib/toolkit/taglib.h +++ b/libfenrir/src/main/jni/audio/taglib/toolkit/taglib.h @@ -28,7 +28,7 @@ #define TAGLIB_MAJOR_VERSION 2 #define TAGLIB_MINOR_VERSION 0 -#define TAGLIB_PATCH_VERSION 1 +#define TAGLIB_PATCH_VERSION 2 #if (defined(_MSC_VER) && _MSC_VER >= 1600) #define TAGLIB_CONSTRUCT_BITSET(x) static_cast(x) diff --git a/material/build.gradle b/material/build.gradle index 5d85d114d..efe153b9b 100644 --- a/material/build.gradle +++ b/material/build.gradle @@ -32,6 +32,7 @@ def srcDirs = [ 'com/google/android/material/floatingactionbutton', 'com/google/android/material/imageview', 'com/google/android/material/internal', + 'com/google/android/material/loadingindicator', 'com/google/android/material/materialswitch', 'com/google/android/material/math', 'com/google/android/material/menu', @@ -108,6 +109,7 @@ dependencies { implementation("androidx.core:core-ktx:$coreVersion") implementation("androidx.activity:activity-ktx:$activityVersion") implementation("androidx.fragment:fragment-ktx:$fragmentVersion") + implementation("androidx.graphics:graphics-shapes:$graphicsVersion") implementation("androidx.cardview:cardview:$cardviewVersion") implementation("androidx.dynamicanimation:dynamicanimation:$dynamicanimationVersion") implementation("androidx.constraintlayout:constraintlayout:$constraintlayoutVersion") diff --git a/material/java/com/google/android/material/appbar/res/values/tokens.xml b/material/java/com/google/android/material/appbar/res/values/tokens.xml index 95e499785..9c971b45c 100644 --- a/material/java/com/google/android/material/appbar/res/values/tokens.xml +++ b/material/java/com/google/android/material/appbar/res/values/tokens.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> - + diff --git a/material/java/com/google/android/material/badge/res/values/tokens.xml b/material/java/com/google/android/material/badge/res/values/tokens.xml index 969d1b6d2..80b402431 100644 --- a/material/java/com/google/android/material/badge/res/values/tokens.xml +++ b/material/java/com/google/android/material/badge/res/values/tokens.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> - + diff --git a/material/java/com/google/android/material/bottomappbar/res/values/tokens.xml b/material/java/com/google/android/material/bottomappbar/res/values/tokens.xml index 713816f9f..995d7410c 100644 --- a/material/java/com/google/android/material/bottomappbar/res/values/tokens.xml +++ b/material/java/com/google/android/material/bottomappbar/res/values/tokens.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> - + diff --git a/material/java/com/google/android/material/bottomnavigation/res/values/tokens.xml b/material/java/com/google/android/material/bottomnavigation/res/values/tokens.xml index b412ea96c..3737a37dd 100644 --- a/material/java/com/google/android/material/bottomnavigation/res/values/tokens.xml +++ b/material/java/com/google/android/material/bottomnavigation/res/values/tokens.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> - + diff --git a/material/java/com/google/android/material/bottomsheet/res/values/tokens.xml b/material/java/com/google/android/material/bottomsheet/res/values/tokens.xml index e54dbc61d..238829114 100644 --- a/material/java/com/google/android/material/bottomsheet/res/values/tokens.xml +++ b/material/java/com/google/android/material/bottomsheet/res/values/tokens.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> - + diff --git a/material/java/com/google/android/material/button/MaterialButton.java b/material/java/com/google/android/material/button/MaterialButton.java index 323421f1e..7df751fe2 100644 --- a/material/java/com/google/android/material/button/MaterialButton.java +++ b/material/java/com/google/android/material/button/MaterialButton.java @@ -18,6 +18,11 @@ import com.google.android.material.R; +import static android.view.Gravity.CENTER_HORIZONTAL; +import static android.view.Gravity.END; +import static android.view.Gravity.LEFT; +import static android.view.Gravity.RIGHT; +import static android.view.Gravity.START; import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; import static com.google.android.material.theme.overlay.MaterialThemeOverlay.wrap; import static java.lang.Math.ceil; @@ -49,6 +54,7 @@ import android.widget.Button; import android.widget.Checkable; import android.widget.CompoundButton; +import android.widget.LinearLayout; import androidx.annotation.ColorInt; import androidx.annotation.ColorRes; import androidx.annotation.DimenRes; @@ -62,6 +68,8 @@ import androidx.annotation.RestrictTo; import androidx.core.graphics.drawable.DrawableCompat; import androidx.customview.view.AbsSavedState; +import androidx.dynamicanimation.animation.FloatPropertyCompat; +import androidx.dynamicanimation.animation.SpringAnimation; import androidx.dynamicanimation.animation.SpringForce; import com.google.android.material.internal.ThemeEnforcement; import com.google.android.material.internal.ViewUtils; @@ -71,6 +79,7 @@ import com.google.android.material.shape.ShapeAppearanceModel; import com.google.android.material.shape.Shapeable; import com.google.android.material.shape.StateListShapeAppearanceModel; +import com.google.android.material.shape.StateListSizeChange; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.LinkedHashSet; @@ -204,8 +213,11 @@ interface OnPressedChangeListener { private static final int DEF_STYLE_RES = R.style.Widget_MaterialComponents_Button; - private static final float DEFAULT_BUTTON_CORNER_SPRING_DAMPING = 0.8f; - private static final float DEFAULT_BUTTON_SPRING_STIFFNESS = 380; + // Use Fast Bouncy spring as default. + private static final float DEFAULT_BUTTON_SPRING_DAMPING = 0.6f; + private static final float DEFAULT_BUTTON_SPRING_STIFFNESS = 800f; + + private static final int UNSET = -1; @NonNull private final MaterialButtonHelper materialButtonHelper; @@ -228,6 +240,19 @@ interface OnPressedChangeListener { private boolean broadcasting = false; @IconGravity private int iconGravity; + private int orientation = UNSET; + private float originalWidth = UNSET; + @Px private int originalPaddingStart = UNSET; + @Px private int originalPaddingEnd = UNSET; + + // Fields for size morphing. + @Px int allowedWidthDecrease = UNSET; + @Nullable StateListSizeChange sizeChange; + @Px int widthChangeMax; + private float displayedWidthIncrease; + private float displayedWidthDecrease; + @Nullable private SpringAnimation widthIncreaseSpringAnimation; + public MaterialButton(@NonNull Context context) { this(context, null /* attrs */); } @@ -280,9 +305,14 @@ public MaterialButton(@NonNull Context context, @Nullable AttributeSet attrs, in updateIcon(/* needsIconReset= */ icon != null); } + private void initializeSizeAnimation() { + widthIncreaseSpringAnimation = new SpringAnimation(this, WIDTH_INCREASE); + widthIncreaseSpringAnimation.setSpring(createSpringForce()); + } + private SpringForce createSpringForce() { return new SpringForce() - .setDampingRatio(DEFAULT_BUTTON_CORNER_SPRING_DAMPING) + .setDampingRatio(DEFAULT_BUTTON_SPRING_DAMPING) .setStiffness(DEFAULT_BUTTON_SPRING_STIFFNESS); } @@ -489,6 +519,36 @@ protected void onLayout(boolean changed, int left, int top, int right, int botto materialButtonHelper.updateMaskBounds(bottom - top, right - left); } updateIconPosition(getMeasuredWidth(), getMeasuredHeight()); + + int curOrientation = getResources().getConfiguration().orientation; + if (orientation != curOrientation) { + orientation = curOrientation; + originalWidth = UNSET; + } + if (originalWidth == UNSET) { + originalWidth = right - left; + } + + if (allowedWidthDecrease == UNSET) { + int localIconSizeAndPadding = + icon == null + ? 0 + : getIconPadding() + (iconSize == 0 ? icon.getIntrinsicWidth() : iconSize); + allowedWidthDecrease = getMeasuredWidth() - getTextLayoutWidth() - localIconSizeAndPadding; + } + + if (originalPaddingStart == UNSET) { + originalPaddingStart = getPaddingStart(); + } + if (originalPaddingEnd == UNSET) { + originalPaddingEnd = getPaddingEnd(); + } + } + + @Override + public void setWidth(@Px int pixels) { + originalWidth = UNSET; + super.setWidth(pixels); } @Override @@ -545,13 +605,13 @@ public void setTextAlignment(int textAlignment) { */ private Alignment getGravityTextAlignment() { switch (getGravity() & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) { - case Gravity.CENTER_HORIZONTAL: + case CENTER_HORIZONTAL: return Alignment.ALIGN_CENTER; - case Gravity.END: - case Gravity.RIGHT: + case END: + case RIGHT: return Alignment.ALIGN_OPPOSITE; - case Gravity.START: - case Gravity.LEFT: + case START: + case LEFT: default: return Alignment.ALIGN_NORMAL; } @@ -1400,6 +1460,7 @@ public void setPressed(boolean pressed) { onPressedChangeListenerInternal.onPressedChanged(this, pressed); } super.setPressed(pressed); + maybeAnimateSize(/* skipAnimation= */ false); } private boolean isUsingOriginalBackground() { @@ -1412,6 +1473,99 @@ void setShouldDrawSurfaceColorStroke(boolean shouldDrawSurfaceColorStroke) { } } + private void maybeAnimateSize(boolean skipAnimation) { + if (sizeChange == null) { + return; + } + if (widthIncreaseSpringAnimation == null) { + initializeSizeAnimation(); + } + if (getParent() instanceof MaterialButtonGroup) { + if (((MaterialButtonGroup) getParent()).getOrientation() == LinearLayout.HORIZONTAL) { + // Animate width. + int widthChange = + min( + widthChangeMax, + sizeChange + .getSizeChangeForState(getDrawableState()) + .widthChange + .getChange(getWidth())); + widthIncreaseSpringAnimation.animateToFinalPosition(widthChange); + if (skipAnimation) { + widthIncreaseSpringAnimation.skipToEnd(); + } + } + } + } + + void setSizeChange(@NonNull StateListSizeChange sizeChange) { + if (this.sizeChange != sizeChange) { + this.sizeChange = sizeChange; + maybeAnimateSize(/* skipAnimation= */ true); + } + } + + void setWidthChangeMax(@Px int widthChangeMax) { + if (this.widthChangeMax != widthChangeMax) { + this.widthChangeMax = widthChangeMax; + maybeAnimateSize(/* skipAnimation= */ true); + } + } + + @Px + int getAllowedWidthDecrease() { + return allowedWidthDecrease; + } + + private float getDisplayedWidthIncrease() { + return displayedWidthIncrease; + } + + private void setDisplayedWidthIncrease(float widthIncrease) { + if (displayedWidthIncrease != widthIncrease) { + displayedWidthIncrease = widthIncrease; + updatePaddingsAndSize(); + invalidate(); + // Report width changed to the parent group. + if (getParent() instanceof MaterialButtonGroup) { + ((MaterialButtonGroup) getParent()) + .onButtonWidthChanged(this, (int) displayedWidthIncrease); + } + } + } + + void setDisplayedWidthDecrease(int widthDecrease) { + displayedWidthDecrease = min(widthDecrease, allowedWidthDecrease); + updatePaddingsAndSize(); + invalidate(); + } + + private void updatePaddingsAndSize() { + int widthChange = (int) (displayedWidthIncrease - displayedWidthDecrease); + int paddingStartChange = widthChange / 2; + setPaddingRelative( + originalPaddingStart + paddingStartChange, + getPaddingTop(), + originalPaddingEnd + widthChange - paddingStartChange, + getPaddingBottom()); + getLayoutParams().width = (int) (originalWidth + widthChange); + } + + // ******************* Properties ******************* + + private static final FloatPropertyCompat WIDTH_INCREASE = + new FloatPropertyCompat("widthIncrease") { + @Override + public float getValue(MaterialButton button) { + return button.getDisplayedWidthIncrease(); + } + + @Override + public void setValue(MaterialButton button, float value) { + button.setDisplayedWidthIncrease(value); + } + }; + static class SavedState extends AbsSavedState { boolean checked; diff --git a/material/java/com/google/android/material/button/MaterialButtonGroup.java b/material/java/com/google/android/material/button/MaterialButtonGroup.java new file mode 100644 index 000000000..97690ff97 --- /dev/null +++ b/material/java/com/google/android/material/button/MaterialButtonGroup.java @@ -0,0 +1,763 @@ +/* + * Copyright (C) 2024 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.google.android.material.button; + +import com.google.android.material.R; + +import static com.google.android.material.theme.overlay.MaterialThemeOverlay.wrap; +import static java.lang.Math.max; +import static java.lang.Math.min; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.Px; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; +import androidx.annotation.VisibleForTesting; +import com.google.android.material.button.MaterialButton.OnPressedChangeListener; +import com.google.android.material.internal.ThemeEnforcement; +import com.google.android.material.internal.ViewUtils; +import com.google.android.material.shape.AbsoluteCornerSize; +import com.google.android.material.shape.CornerSize; +import com.google.android.material.shape.RelativeCornerSize; +import com.google.android.material.shape.ShapeAppearanceModel; +import com.google.android.material.shape.StateListCornerSize; +import com.google.android.material.shape.StateListShapeAppearanceModel; +import com.google.android.material.shape.StateListSizeChange; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * A common container for a set of related {@link MaterialButton}s. The {@link MaterialButton}s in + * this group will be shown on a single line. + * + *

This layout currently only supports child views of type {@link MaterialButton}. Buttons can be + * added to this view group via XML, as follows: + * + *

+ * <com.google.android.material.button.MaterialButtonGroup
+ *     xmlns:android="http://schemas.android.com/apk/res/android"
+ *     android:id="@+id/toggle_button_group"
+ *     android:layout_width="wrap_content"
+ *     android:layout_height="wrap_content">
+ *
+ *     <com.google.android.material.button.MaterialButton
+ *         android:layout_width="wrap_content"
+ *         android:layout_height="wrap_content"
+ *         android:text="@string/button_label_private"/>
+ *     <com.google.android.material.button.MaterialButton
+ *         android:layout_width="wrap_content"
+ *         android:layout_height="wrap_content"
+ *         android:text="@string/button_label_team"/>
+ *     <com.google.android.material.button.MaterialButton
+ *         android:layout_width="wrap_content"
+ *         android:layout_height="wrap_content"
+ *         android:text="@string/button_label_everyone"/>
+ *     <com.google.android.material.button.MaterialButton
+ *         android:layout_width="wrap_content"
+ *         android:layout_height="wrap_content"
+ *         android:text="@string/button_label_custom"/>
+ *
+ * </com.google.android.material.button.MaterialButtonGroup>
+ * 
+ * + *

Buttons can also be added to this view group programmatically via the {@link #addView(View)} + * methods. + * + *

MaterialButtonGroup is a {@link LinearLayout}. Using {@code + * android:layout_width="MATCH_PARENT"} and removing {@code android:insetBottom} {@code + * android:insetTop} on the children is recommended if using {@code VERTICAL}. + * + *

It takes the group shape, if specified, as the original state list shape for the first and + * last buttons. Otherwise, it takes the state list shape (or build one from the shape appearance + * model, if state list shape is not specified) in the child button. + * + * @param isFirstVisible Whether this is the first visible child button regardless its index. + * @param isLastVisible Whether this is the last visible child button regardless its index. + * @param index The index of the child button. + */ + @NonNull + private StateListShapeAppearanceModel.Builder getOriginalStateListShapeBuilder( + boolean isFirstVisible, boolean isLastVisible, int index) { + StateListShapeAppearanceModel originalStateList = + groupStateListShapeAppearance != null && (isFirstVisible || isLastVisible) + ? groupStateListShapeAppearance + : originalChildStateListShapeAppearanceModels.get(index); + // If the state list shape is not specified, creates one from the shape appearance model. + return originalStateList == null + ? new StateListShapeAppearanceModel.Builder(originalChildShapeAppearanceModels.get(index)) + : originalStateList.toBuilder(); + } + + /** + * We keep track of which views are pressed and checked to draw them last. This prevents visual + * issues with overlapping strokes. + */ + @Override + protected int getChildDrawingOrder(int childCount, int i) { + if (childOrder == null || i >= childOrder.length) { + Log.w(LOG_TAG, "Child order wasn't updated"); + return i; + } + + return childOrder[i]; + } + + /** + * Sets a negative marginStart on all but the first child, if two adjacent children both have a + * stroke width greater than 0. This prevents a double-width stroke from being drawn for two + * adjacent stroked children, and instead draws the adjacent strokes directly on top of each + * other. + * + *

The negative margin adjustment amount will be equal to the smaller of the two adjacent + * stroke widths. + * + *

Also rearranges children such that they are shown in the correct visual order. + */ + private void adjustChildMarginsAndUpdateLayout() { + int firstVisibleChildIndex = getFirstVisibleChildIndex(); + if (firstVisibleChildIndex == -1) { + return; + } + + for (int i = firstVisibleChildIndex + 1; i < getChildCount(); i++) { + // Only adjusts margins if both adjacent children are MaterialButtons + MaterialButton currentButton = getChildButton(i); + MaterialButton previousButton = getChildButton(i - 1); + + // Calculates the margin adjustment to be the smaller of the two adjacent stroke widths + int smallestStrokeWidth = 0; + if (spacing <= 0) { + smallestStrokeWidth = min(currentButton.getStrokeWidth(), previousButton.getStrokeWidth()); + } + + LayoutParams params = buildLayoutParams(currentButton); + if (getOrientation() == HORIZONTAL) { + params.setMarginEnd(0); + params.setMarginStart(spacing - smallestStrokeWidth); + params.topMargin = 0; + } else { + params.bottomMargin = 0; + params.topMargin = spacing - smallestStrokeWidth; + params.setMarginStart(0); + } + + currentButton.setLayoutParams(params); + } + + resetChildMargins(firstVisibleChildIndex); + } + + private void resetChildMargins(int childIndex) { + if (getChildCount() == 0 || childIndex == -1) { + return; + } + + MaterialButton currentButton = getChildButton(childIndex); + LayoutParams params = (LayoutParams) currentButton.getLayoutParams(); + if (getOrientation() == VERTICAL) { + params.topMargin = 0; + params.bottomMargin = 0; + return; + } + + params.setMarginEnd(0); + params.setMarginStart(0); + params.leftMargin = 0; + params.rightMargin = 0; + } + + void onButtonWidthChanged(@NonNull MaterialButton button, int increaseSize) { + int buttonIndex = indexOfChild(button); + if (buttonIndex < 0) { + return; + } + MaterialButton prevVisibleButton = getPrevVisibleChildButton(buttonIndex); + MaterialButton nextVisibleButton = getNextVisibleChildButton(buttonIndex); + if (prevVisibleButton == null && nextVisibleButton == null) { + return; + } + if (prevVisibleButton == null) { + nextVisibleButton.setDisplayedWidthDecrease(increaseSize); + } + if (nextVisibleButton == null) { + prevVisibleButton.setDisplayedWidthDecrease(increaseSize); + } + if (prevVisibleButton != null && nextVisibleButton != null) { + // If there are two neighbors, each neighbor will absorb half of the expanded amount. + prevVisibleButton.setDisplayedWidthDecrease(increaseSize / 2); + // We want to avoid one pixel missing due to the casting, when increaseSize is odd. + nextVisibleButton.setDisplayedWidthDecrease((increaseSize + 1) / 2); + } + } + + /** + * Adjusts the max amount of size to expand for each child button. So that it won't squeeze its + * neighbors too much to cause text truncation; and the expansion amount per edge is same for all + * buttons, + */ + private void adjustChildSizeChange() { + if (buttonSizeChange == null) { + return; + } + int firstVisibleChildIndex = getFirstVisibleChildIndex(); + int lastVisibleChildIndex = getLastVisibleChildIndex(); + int widthIncreaseOnSingleEdge = Integer.MAX_VALUE; + for (int i = firstVisibleChildIndex; i <= lastVisibleChildIndex; i++) { + if (!isChildVisible(i)) { + continue; + } + // Calculates the allowed width increase for each child button with consideration of the max + // allowed width decrease of its neighbors. + int widthIncrease = getButtonAllowedWidthIncrease(i); + + // If the button expands on both edges, the width increase on each edge should be half of + // the total width increase. Calculates the minimum width increase on each edge, so that all + // buttons won't squeeze their neighbors too much. + widthIncreaseOnSingleEdge = + min( + widthIncreaseOnSingleEdge, + i != firstVisibleChildIndex && i != lastVisibleChildIndex + ? widthIncrease / 2 + : widthIncrease); + } + for (int i = firstVisibleChildIndex; i <= lastVisibleChildIndex; i++) { + if (!isChildVisible(i)) { + continue; + } + getChildButton(i).setSizeChange(buttonSizeChange); + // If the button expands on both edges, the total width increase should be double of the + // width increase on each edge. + getChildButton(i) + .setWidthChangeMax( + i == firstVisibleChildIndex || i == lastVisibleChildIndex + ? widthIncreaseOnSingleEdge + : widthIncreaseOnSingleEdge * 2); + } + } + + /** + * Returns the allowed width increase for a child button. + * + *

The allowed width increase is the smaller amount of the max width increase of the button in + * all states and the total allowed width decrease of its neighbors. + */ + private int getButtonAllowedWidthIncrease(int index) { + if (!isChildVisible(index) || buttonSizeChange == null) { + return 0; + } + MaterialButton currentButton = getChildButton(index); + int widthIncrease = max(0, buttonSizeChange.getMaxWidthChange(currentButton.getWidth())); + // Checking neighbors' allowed width decrease. + MaterialButton prevVisibleButton = getPrevVisibleChildButton(index); + int prevButtonAllowedWidthDecrease = + prevVisibleButton == null ? 0 : prevVisibleButton.getAllowedWidthDecrease(); + MaterialButton nextVisibleButton = getNextVisibleChildButton(index); + int nextButtonAllowedWidthDecrease = + nextVisibleButton == null ? 0 : nextVisibleButton.getAllowedWidthDecrease(); + return min(widthIncrease, prevButtonAllowedWidthDecrease + nextButtonAllowedWidthDecrease); + } + + // ================ Getters and setters =================== + + /** + * Returns the {@link StateListSizeChange} of child buttons on state changes. + * + * @hide + */ + @RestrictTo(Scope.LIBRARY_GROUP) + @Nullable + public StateListSizeChange getButtonSizeChange() { + return buttonSizeChange; + } + + /** + * Sets the {@link StateListSizeChange} of child buttons on state changes. + * + * @param buttonSizeChange The new {@link StateListSizeChange} of child buttons. + * @hide + */ + @RestrictTo(Scope.LIBRARY_GROUP) + public void setButtonSizeChange(@NonNull StateListSizeChange buttonSizeChange) { + if (this.buttonSizeChange != buttonSizeChange) { + this.buttonSizeChange = buttonSizeChange; + adjustChildSizeChange(); + requestLayout(); + invalidate(); + } + } + + /** Returns the spacing (in pixels) between each button in the group. */ + @Px + public int getSpacing() { + return spacing; + } + + /** + * Sets the spacing between each button in the group. + * + * @param spacing the spacing (in pixels) between each button in the group + */ + public void setSpacing(@Px int spacing) { + this.spacing = spacing; + invalidate(); + requestLayout(); + } + + /** Returns the inner corner size of the group. */ + @NonNull + public CornerSize getInnerCornerSize() { + return innerCornerSize.getDefaultCornerSize(); + } + + /** + * Sets the inner corner size of the group. + * + *

Can set as an {@link AbsoluteCornerSize} or {@link RelativeCornerSize}. Don't set relative + * corner size larger than 50% or absolute corner size larger than half height to avoid corner + * overlapping. + * + * @param cornerSize the inner corner size of the group + */ + public void setInnerCornerSize(@NonNull CornerSize cornerSize) { + innerCornerSize = StateListCornerSize.create(cornerSize); + updateChildShapes(); + invalidate(); + } + + /** + * Returns the inner corner size state list of the group. + * + * @hide + */ + @NonNull + @RestrictTo(Scope.LIBRARY_GROUP) + public StateListCornerSize getInnerCornerSizeStateList() { + return innerCornerSize; + } + + /** + * Sets the inner corner size state list of the group. + * + *

Can set as an {@link StateListCornerSize}. Don't set relative corner size larger than 50% or + * absolute corner size larger than half height to the corner size in any state to avoid corner + * overlapping. + * + * @param cornerSizeStateList the inner corner size state list of the group + * @hide + */ + @RestrictTo(Scope.LIBRARY_GROUP) + public void setInnerCornerSizeStateList(@NonNull StateListCornerSize cornerSizeStateList) { + innerCornerSize = cornerSizeStateList; + updateChildShapes(); + invalidate(); + } + + /** Returns the {@link ShapeAppearanceModel} of the group. */ + @Nullable + public ShapeAppearanceModel getShapeAppearance() { + return groupStateListShapeAppearance == null + ? null + : groupStateListShapeAppearance.getDefaultShape(/* withCornerSizeOverrides= */ true); + } + + /** + * Sets the {@link ShapeAppearanceModel} of the group. + * + * @param shapeAppearance The new {@link ShapeAppearanceModel} of the group. + */ + public void setShapeAppearance(@Nullable ShapeAppearanceModel shapeAppearance) { + groupStateListShapeAppearance = + new StateListShapeAppearanceModel.Builder(shapeAppearance).build(); + updateChildShapes(); + invalidate(); + } + + /** + * Returns the {@link StateListShapeAppearanceModel} of the group. + * + * @hide + */ + @Nullable + @RestrictTo(Scope.LIBRARY_GROUP) + public StateListShapeAppearanceModel getStateListShapeAppearance() { + return groupStateListShapeAppearance; + } + + /** + * Sets the {@link StateListShapeAppearanceModel} of the group. + * + * @param stateListShapeAppearance The new {@link StateListShapeAppearanceModel} of the group. + * @hide + */ + @RestrictTo(Scope.LIBRARY_GROUP) + public void setStateListShapeAppearance( + @Nullable StateListShapeAppearanceModel stateListShapeAppearance) { + groupStateListShapeAppearance = stateListShapeAppearance; + updateChildShapes(); + invalidate(); + } + + // ================ Helper functions =================== + + @NonNull + MaterialButton getChildButton(int index) { + return (MaterialButton) getChildAt(index); + } + + @NonNull + LinearLayout.LayoutParams buildLayoutParams(@NonNull View child) { + ViewGroup.LayoutParams layoutParams = child.getLayoutParams(); + if (layoutParams instanceof LinearLayout.LayoutParams) { + return (LayoutParams) layoutParams; + } + + return new LayoutParams(layoutParams.width, layoutParams.height); + } + + private int getFirstVisibleChildIndex() { + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + if (isChildVisible(i)) { + return i; + } + } + + return -1; + } + + private int getLastVisibleChildIndex() { + int childCount = getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + if (isChildVisible(i)) { + return i; + } + } + + return -1; + } + + private boolean isChildVisible(int i) { + View child = getChildAt(i); + return child.getVisibility() != View.GONE; + } + + private void setGeneratedIdIfNeeded(@NonNull MaterialButton materialButton) { + // Generates an ID if none is set, for relative positioning purposes + if (materialButton.getId() == View.NO_ID) { + materialButton.setId(View.generateViewId()); + } + } + + @Nullable + private MaterialButton getNextVisibleChildButton(int index) { + int childCount = getChildCount(); + for (int i = index + 1; i < childCount; i++) { + if (isChildVisible(i)) { + return getChildButton(i); + } + } + return null; + } + + @Nullable + private MaterialButton getPrevVisibleChildButton(int index) { + for (int i = index - 1; i >= 0; i--) { + if (isChildVisible(i)) { + return getChildButton(i); + } + } + return null; + } + + private void updateChildOrder() { + final SortedMap viewToIndexMap = new TreeMap<>(childOrderComparator); + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + viewToIndexMap.put(getChildButton(i), i); + } + + childOrder = viewToIndexMap.values().toArray(new Integer[0]); + } + + /** + * Enables this {@link MaterialButtonGroup} and all its {@link MaterialButton} children + * + * @param enabled boolean to setEnable {@link MaterialButtonGroup} + */ + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + // Enable or disable child buttons + for (int i = 0; i < getChildCount(); i++) { + MaterialButton childButton = getChildButton(i); + childButton.setEnabled(enabled); + } + } + + private class PressedStateTracker implements OnPressedChangeListener { + @Override + public void onPressedChanged(@NonNull MaterialButton button, boolean isPressed) { + invalidate(); + } + } +} diff --git a/material/java/com/google/android/material/button/MaterialButtonToggleGroup.java b/material/java/com/google/android/material/button/MaterialButtonToggleGroup.java index 44bb433db..01bed00e6 100644 --- a/material/java/com/google/android/material/button/MaterialButtonToggleGroup.java +++ b/material/java/com/google/android/material/button/MaterialButtonToggleGroup.java @@ -19,11 +19,9 @@ import com.google.android.material.R; import static com.google.android.material.theme.overlay.MaterialThemeOverlay.wrap; -import static java.lang.Math.min; import android.content.Context; import android.content.res.TypedArray; -import android.graphics.Canvas; import android.text.TextUtils.TruncateAt; import android.util.AttributeSet; import android.util.Log; @@ -37,33 +35,20 @@ import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.Px; -import androidx.annotation.RestrictTo; -import androidx.annotation.RestrictTo.Scope; -import androidx.annotation.VisibleForTesting; import androidx.core.view.AccessibilityDelegateCompat; import androidx.core.view.ViewCompat; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionItemInfoCompat; -import com.google.android.material.button.MaterialButton.OnPressedChangeListener; import com.google.android.material.internal.ThemeEnforcement; -import com.google.android.material.internal.ViewUtils; import com.google.android.material.shape.AbsoluteCornerSize; -import com.google.android.material.shape.CornerSize; -import com.google.android.material.shape.RelativeCornerSize; -import com.google.android.material.shape.ShapeAppearanceModel; import com.google.android.material.shape.StateListCornerSize; -import com.google.android.material.shape.StateListShapeAppearanceModel; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; -import java.util.SortedMap; -import java.util.TreeMap; /** * A common container for a set of related, toggleable {@link MaterialButton}s. The {@link @@ -135,7 +120,7 @@ * developer guidance and design * guidelines. */ -public class MaterialButtonToggleGroup extends LinearLayout { +public class MaterialButtonToggleGroup extends MaterialButtonGroup { /** * Interface definition for a callback to be invoked when a {@link MaterialButton} is checked or @@ -155,39 +140,13 @@ public interface OnButtonCheckedListener { private static final String LOG_TAG = "MButtonToggleGroup"; private static final int DEF_STYLE_RES = R.style.Widget_MaterialComponents_MaterialButtonToggleGroup; - - private final List originalChildShapeAppearanceModels = new ArrayList<>(); - private final List originalChildStateListShapeAppearanceModels = - new ArrayList<>(); - - private final PressedStateTracker pressedStateTracker = new PressedStateTracker(); private final LinkedHashSet onButtonCheckedListeners = new LinkedHashSet<>(); - private final Comparator childOrderComparator = - (v1, v2) -> { - int checked = Boolean.valueOf(v1.isChecked()).compareTo(v2.isChecked()); - if (checked != 0) { - return checked; - } - - int stateful = Boolean.valueOf(v1.isPressed()).compareTo(v2.isPressed()); - if (stateful != 0) { - return stateful; - } - - // don't return 0s - return Integer.compare(indexOfChild(v1), indexOfChild(v2)); - }; - - private Integer[] childOrder; + private boolean skipCheckedStateTracker = false; private boolean singleSelection; private boolean selectionRequired; - @NonNull private StateListCornerSize innerCornerSize; - @Nullable private StateListShapeAppearanceModel groupStateListShapeAppearance; - @Px private int spacing; - @IdRes private final int defaultCheckId; private Set checkedIds = new HashSet<>(); @@ -215,34 +174,11 @@ public MaterialButtonToggleGroup( selectionRequired = attributes.getBoolean(R.styleable.MaterialButtonToggleGroup_selectionRequired, false); - if (attributes.hasValue(R.styleable.MaterialButtonToggleGroup_shapeAppearance)) { - groupStateListShapeAppearance = - StateListShapeAppearanceModel.create( - context, attributes, R.styleable.MaterialButtonToggleGroup_shapeAppearance); - if (groupStateListShapeAppearance == null) { - groupStateListShapeAppearance = - new StateListShapeAppearanceModel.Builder( - ShapeAppearanceModel.builder( - context, - attributes.getResourceId( - R.styleable.MaterialButtonToggleGroup_shapeAppearance, 0), - attributes.getResourceId( - R.styleable.MaterialButtonToggleGroup_shapeAppearanceOverlay, 0)) - .build()) - .build(); - } + // If inner corner size is not specified in button group, set it to 0 as default. + if (innerCornerSize == null) { + innerCornerSize = StateListCornerSize.create(new AbsoluteCornerSize(0)); } - innerCornerSize = - StateListCornerSize.create( - context, - attributes, - R.styleable.MaterialButtonToggleGroup_innerCornerSize, - new AbsoluteCornerSize(0)); - - spacing = - attributes.getDimensionPixelSize(R.styleable.MaterialButtonToggleGroup_android_spacing, 0); - setChildrenDrawingOrderEnabled(true); setEnabled(attributes.getBoolean(R.styleable.MaterialButtonToggleGroup_android_enabled, true)); attributes.recycle(); @@ -257,18 +193,12 @@ protected void onFinishInflate() { } } - @Override - protected void dispatchDraw(@NonNull Canvas canvas) { - updateChildOrder(); - super.dispatchDraw(canvas); - } - /** * This override prohibits Views other than {@link MaterialButton} to be added. It also makes * updates to the add button shape and margins. */ @Override - public void addView(View child, int index, ViewGroup.LayoutParams params) { + public void addView(@NonNull View child, int index, @NonNull ViewGroup.LayoutParams params) { if (!(child instanceof MaterialButton)) { Log.e(LOG_TAG, "Child views must be of type MaterialButton."); return; @@ -276,20 +206,13 @@ public void addView(View child, int index, ViewGroup.LayoutParams params) { super.addView(child, index, params); MaterialButton buttonChild = (MaterialButton) child; - setGeneratedIdIfNeeded(buttonChild); + // Sets sensible default values and an internal checked change listener for this child setupButtonChild(buttonChild); // Update button group's checked states checkInternal(buttonChild.getId(), buttonChild.isChecked()); - // Saves original child shape appearance. - originalChildShapeAppearanceModels.add(buttonChild.getShapeAppearanceModel()); - originalChildStateListShapeAppearanceModels.add(buttonChild.getStateListShapeAppearanceModel()); - - // Enable children based on the MaterialButtonToggleGroup own isEnabled - buttonChild.setEnabled(isEnabled()); - ViewCompat.setAccessibilityDelegate( buttonChild, new AccessibilityDelegateCompat() { @@ -309,31 +232,6 @@ public void onInitializeAccessibilityNodeInfo( }); } - @Override - public void onViewRemoved(View child) { - super.onViewRemoved(child); - - if (child instanceof MaterialButton) { - ((MaterialButton) child).setOnPressedChangeListenerInternal(null); - } - - int indexOfChild = indexOfChild(child); - if (indexOfChild >= 0) { - originalChildShapeAppearanceModels.remove(indexOfChild); - originalChildStateListShapeAppearanceModels.remove(indexOfChild); - } - - updateChildShapes(); - adjustChildMarginsAndUpdateLayout(); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - updateChildShapes(); - adjustChildMarginsAndUpdateLayout(); - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - } - @Override public void onInitializeAccessibilityNodeInfo(@NonNull AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); @@ -489,13 +387,17 @@ public void setSingleSelection(boolean singleSelection) { } private void updateChildrenA11yClassName() { + String className = getChildrenA11yClassName(); for (int i = 0; i < getChildCount(); i++) { - String className = - singleSelection ? RadioButton.class.getName() : ToggleButton.class.getName(); getChildButton(i).setA11yClassName(className); } } + @NonNull + private String getChildrenA11yClassName() { + return singleSelection ? RadioButton.class.getName() : ToggleButton.class.getName(); + } + /** * Sets whether we prevent all child buttons from being deselected. * @@ -526,117 +428,6 @@ public void setSingleSelection(@BoolRes int id) { setSingleSelection(getResources().getBoolean(id)); } - /** Returns the spacing (in pixels) between each button in the group. */ - @Px - public int getSpacing() { - return spacing; - } - - /** - * Sets the spacing between each button in the group. - * - * @param spacing the spacing (in pixels) between each button in the group - */ - public void setSpacing(@Px int spacing) { - this.spacing = spacing; - invalidate(); - requestLayout(); - } - - /** Returns the inner corner size of the group. */ - @NonNull - public CornerSize getInnerCornerSize() { - return innerCornerSize.getDefaultCornerSize(); - } - - /** - * Sets the inner corner size of the group. - * - *

Can set as an {@link AbsoluteCornerSize} or {@link RelativeCornerSize}. Don't set relative - * corner size larger than 50% or absolute corner size larger than half height to avoid corner - * overlapping. - * - * @param cornerSize the inner corner size of the group - */ - public void setInnerCornerSize(@NonNull CornerSize cornerSize) { - innerCornerSize = StateListCornerSize.create(cornerSize); - updateChildShapes(); - invalidate(); - } - - /** - * Returns the inner corner size state list of the group. - * - * @hide - */ - @NonNull - @RestrictTo(Scope.LIBRARY_GROUP) - public StateListCornerSize getInnerCornerSizeStateList() { - return innerCornerSize; - } - - /** - * Sets the inner corner size state list of the group. - * - *

Can set as an {@link StateListCornerSize}. Don't set relative corner size larger than 50% or - * absolute corner size larger than half height to the corner size in any state to avoid corner - * overlapping. - * - * @param cornerSizeStateList the inner corner size state list of the group - * @hide - */ - @RestrictTo(Scope.LIBRARY_GROUP) - public void setInnerCornerSizeStateList(@NonNull StateListCornerSize cornerSizeStateList) { - innerCornerSize = cornerSizeStateList; - updateChildShapes(); - invalidate(); - } - - /** Returns the {@link ShapeAppearanceModel} of the group. */ - @Nullable - public ShapeAppearanceModel getShapeAppearance() { - return groupStateListShapeAppearance == null - ? null - : groupStateListShapeAppearance.getDefaultShape(/* withCornerSizeOverrides= */ true); - } - - /** - * Sets the {@link ShapeAppearanceModel} of the group. - * - * @param shapeAppearance The new {@link ShapeAppearanceModel} of the group. - */ - public void setShapeAppearance(@Nullable ShapeAppearanceModel shapeAppearance) { - groupStateListShapeAppearance = - new StateListShapeAppearanceModel.Builder(shapeAppearance).build(); - updateChildShapes(); - invalidate(); - } - - /** - * Returns the {@link StateListShapeAppearanceModel} of the group. - * - * @hide - */ - @Nullable - @RestrictTo(Scope.LIBRARY_GROUP) - public StateListShapeAppearanceModel getStateListShapeAppearance() { - return groupStateListShapeAppearance; - } - - /** - * Sets the {@link StateListShapeAppearanceModel} of the group. - * - * @param stateListShapeAppearance The new {@link StateListShapeAppearanceModel} of the group. - * @hide - */ - @RestrictTo(Scope.LIBRARY_GROUP) - public void setStateListShapeAppearance( - @Nullable StateListShapeAppearanceModel stateListShapeAppearance) { - groupStateListShapeAppearance = stateListShapeAppearance; - updateChildShapes(); - invalidate(); - } - private void setCheckedStateForView(@IdRes int viewId, boolean checked) { View checkedView = findViewById(viewId); if (checkedView instanceof MaterialButton) { @@ -646,191 +437,6 @@ private void setCheckedStateForView(@IdRes int viewId, boolean checked) { } } - /** - * Sets a negative marginStart on all but the first child, if two adjacent children both have a - * stroke width greater than 0. This prevents a double-width stroke from being drawn for two - * adjacent stroked children, and instead draws the adjacent strokes directly on top of each - * other. - * - *

The negative margin adjustment amount will be equal to the smaller of the two adjacent - * stroke widths. - * - *

Also rearranges children such that they are shown in the correct visual order. - */ - private void adjustChildMarginsAndUpdateLayout() { - int firstVisibleChildIndex = getFirstVisibleChildIndex(); - if (firstVisibleChildIndex == -1) { - return; - } - - for (int i = firstVisibleChildIndex + 1; i < getChildCount(); i++) { - // Only adjusts margins if both adjacent children are MaterialButtons - MaterialButton currentButton = getChildButton(i); - MaterialButton previousButton = getChildButton(i - 1); - - // Calculates the margin adjustment to be the smaller of the two adjacent stroke widths - int smallestStrokeWidth = 0; - if (spacing <= 0) { - smallestStrokeWidth = min(currentButton.getStrokeWidth(), previousButton.getStrokeWidth()); - } - - LayoutParams params = buildLayoutParams(currentButton); - if (getOrientation() == HORIZONTAL) { - params.setMarginEnd(0); - params.setMarginStart(spacing - smallestStrokeWidth); - params.topMargin = 0; - } else { - params.bottomMargin = 0; - params.topMargin = spacing - smallestStrokeWidth; - params.setMarginStart(0); - } - - currentButton.setLayoutParams(params); - } - - resetChildMargins(firstVisibleChildIndex); - } - - @NonNull - private MaterialButton getChildButton(int index) { - return (MaterialButton) getChildAt(index); - } - - private void resetChildMargins(int childIndex) { - if (getChildCount() == 0 || childIndex == -1) { - return; - } - - MaterialButton currentButton = getChildButton(childIndex); - LayoutParams params = (LayoutParams) currentButton.getLayoutParams(); - if (getOrientation() == VERTICAL) { - params.topMargin = 0; - params.bottomMargin = 0; - return; - } - - params.setMarginEnd(0); - params.setMarginStart(0); - params.leftMargin = 0; - params.rightMargin = 0; - } - - /** - * Sets all corner radii override to inner corner size except for leftmost and rightmost corners. - */ - @VisibleForTesting - void updateChildShapes() { - int childCount = getChildCount(); - int firstVisibleChildIndex = getFirstVisibleChildIndex(); - int lastVisibleChildIndex = getLastVisibleChildIndex(); - for (int i = 0; i < childCount; i++) { - MaterialButton button = getChildButton(i); - if (button.getVisibility() == GONE) { - continue; - } - boolean isFirstVisible = i == firstVisibleChildIndex; - boolean isLastVisible = i == lastVisibleChildIndex; - - StateListShapeAppearanceModel.Builder originalStateListShapeBuilder = - getOriginalStateListShapeBuilder(isFirstVisible, isLastVisible, i); - // Determines which corners of the original shape should be kept. - boolean isHorizontal = getOrientation() == HORIZONTAL; - boolean isRtl = ViewUtils.isLayoutRtl(this); - int cornerPositionBitsToKeep = 0; - if (isHorizontal) { - // When horizontal (ltr), keeps the left two original corners for the first button. - if (isFirstVisible) { - cornerPositionBitsToKeep |= - StateListShapeAppearanceModel.CORNER_TOP_LEFT - | StateListShapeAppearanceModel.CORNER_BOTTOM_LEFT; - } - // When horizontal (ltr), keeps the right two original corners for the last button. - if (isLastVisible) { - cornerPositionBitsToKeep |= - StateListShapeAppearanceModel.CORNER_TOP_RIGHT - | StateListShapeAppearanceModel.CORNER_BOTTOM_RIGHT; - } - // If rtl, swap the position bits of left corners and right corners. - if (isRtl) { - cornerPositionBitsToKeep = - StateListShapeAppearanceModel.swapCornerPositionRtl(cornerPositionBitsToKeep); - } - } else { - // When vertical, keeps the top two original corners for the first button. - if (isFirstVisible) { - cornerPositionBitsToKeep |= - StateListShapeAppearanceModel.CORNER_TOP_LEFT - | StateListShapeAppearanceModel.CORNER_TOP_RIGHT; - } - // When vertical, keeps the bottom two original corners for the last button. - if (isLastVisible) { - cornerPositionBitsToKeep |= - StateListShapeAppearanceModel.CORNER_BOTTOM_LEFT - | StateListShapeAppearanceModel.CORNER_BOTTOM_RIGHT; - } - } - // Overrides the corners that don't need to keep with unary operator. - int cornerPositionBitsToOverride = ~cornerPositionBitsToKeep; - StateListShapeAppearanceModel newStateListShape = - originalStateListShapeBuilder - .setCornerSizeOverride(innerCornerSize, cornerPositionBitsToOverride) - .build(); - if (newStateListShape.isStateful()) { - button.setStateListShapeAppearanceModel(newStateListShape); - } else { - button.setShapeAppearanceModel( - newStateListShape.getDefaultShape(/* withCornerSizeOverrides= */ true)); - } - } - } - - /** - * Returns a {@link StateListShapeAppearanceModel.Builder} as the original shape of a child - * button. - * - *

It takes the group shape, if specified, as the original state list shape for the first and - * last buttons. Otherwise, it takes the state list shape (or build one from the shape appearance - * model, if state list shape is not specified) in the child button. - * - * @param isFirstVisible Whether this is the first visible child button regardless its index. - * @param isLastVisible Whether this is the last visible child button regardless its index. - * @param index The index of the child button. - */ - @NonNull - private StateListShapeAppearanceModel.Builder getOriginalStateListShapeBuilder( - boolean isFirstVisible, boolean isLastVisible, int index) { - StateListShapeAppearanceModel originalStateList = - groupStateListShapeAppearance != null && (isFirstVisible || isLastVisible) - ? groupStateListShapeAppearance - : originalChildStateListShapeAppearanceModels.get(index); - // If the state list shape is not specified, creates one from the shape appearance model. - return originalStateList == null - ? new StateListShapeAppearanceModel.Builder(originalChildShapeAppearanceModels.get(index)) - : originalStateList.toBuilder(); - } - - private int getFirstVisibleChildIndex() { - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - if (isChildVisible(i)) { - return i; - } - } - - return -1; - } - - private int getLastVisibleChildIndex() { - int childCount = getChildCount(); - for (int i = childCount - 1; i >= 0; i--) { - if (isChildVisible(i)) { - return i; - } - } - - return -1; - } - private boolean isChildVisible(int i) { View child = getChildAt(i); return child.getVisibility() != View.GONE; @@ -904,13 +510,6 @@ private void dispatchOnButtonChecked(@IdRes int buttonId, boolean checked) { } } - private void setGeneratedIdIfNeeded(@NonNull MaterialButton materialButton) { - // Generates an ID if none is set, for relative positioning purposes - if (materialButton.getId() == View.NO_ID) { - materialButton.setId(View.generateViewId()); - } - } - /** * Sets sensible default values for {@link MaterialButton} child of this group, set child to * {@code checkable}, and set internal checked change listener for this child. @@ -922,47 +521,12 @@ private void setupButtonChild(@NonNull MaterialButton buttonChild) { buttonChild.setMaxLines(1); buttonChild.setEllipsize(TruncateAt.END); buttonChild.setCheckable(true); - - buttonChild.setOnPressedChangeListenerInternal(pressedStateTracker); + buttonChild.setA11yClassName(getChildrenA11yClassName()); // Enables surface layer drawing for semi-opaque strokes buttonChild.setShouldDrawSurfaceColorStroke(true); } - @NonNull - private LinearLayout.LayoutParams buildLayoutParams(@NonNull View child) { - ViewGroup.LayoutParams layoutParams = child.getLayoutParams(); - if (layoutParams instanceof LinearLayout.LayoutParams) { - return (LayoutParams) layoutParams; - } - - return new LayoutParams(layoutParams.width, layoutParams.height); - } - - /** - * We keep track of which views are pressed and checked to draw them last. This prevents visual - * issues with overlapping strokes. - */ - @Override - protected int getChildDrawingOrder(int childCount, int i) { - if (childOrder == null || i >= childOrder.length) { - Log.w(LOG_TAG, "Child order wasn't updated"); - return i; - } - - return childOrder[i]; - } - - private void updateChildOrder() { - final SortedMap viewToIndexMap = new TreeMap<>(childOrderComparator); - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - viewToIndexMap.put(getChildButton(i), i); - } - - childOrder = viewToIndexMap.values().toArray(new Integer[0]); - } - void onButtonCheckedStateChanged(@NonNull MaterialButton button, boolean isChecked) { // Checked state change is triggered by the button group, do not update checked ids again. if (skipCheckedStateTracker) { @@ -970,27 +534,4 @@ void onButtonCheckedStateChanged(@NonNull MaterialButton button, boolean isCheck } checkInternal(button.getId(), isChecked); } - - /** - * Enables this {@link MaterialButtonToggleGroup} and all its {@link MaterialButton} children - * - * @param enabled boolean to setEnable {@link MaterialButtonToggleGroup} - */ - @Override - public void setEnabled(boolean enabled) { - super.setEnabled(enabled); - // Enable or disable child buttons - for (int i = 0; i < getChildCount(); i++) { - MaterialButton childButton = getChildButton(i); - childButton.setEnabled(enabled); - } - } - - private class PressedStateTracker implements OnPressedChangeListener { - - @Override - public void onPressedChanged(@NonNull MaterialButton button, boolean isPressed) { - invalidate(); - } - } } diff --git a/material/java/com/google/android/material/button/res-public/values/public.xml b/material/java/com/google/android/material/button/res-public/values/public.xml index 40bd1fb65..6fd0794ee 100644 --- a/material/java/com/google/android/material/button/res-public/values/public.xml +++ b/material/java/com/google/android/material/button/res-public/values/public.xml @@ -25,12 +25,16 @@ + + + + - + @@ -57,6 +61,9 @@ + + + diff --git a/material/java/com/google/android/material/button/res/values/attrs.xml b/material/java/com/google/android/material/button/res/values/attrs.xml index 4d10039a1..d47028748 100644 --- a/material/java/com/google/android/material/button/res/values/attrs.xml +++ b/material/java/com/google/android/material/button/res/values/attrs.xml @@ -19,6 +19,12 @@ + + + + + + @@ -27,8 +33,8 @@ - - + + @@ -102,6 +108,24 @@ + + + + + + + + + + + + + + + - + diff --git a/material/java/com/google/android/material/button/res/values/icon_btn_tokens.xml b/material/java/com/google/android/material/button/res/values/icon_btn_tokens.xml index c54de5d91..fbf1cd149 100644 --- a/material/java/com/google/android/material/button/res/values/icon_btn_tokens.xml +++ b/material/java/com/google/android/material/button/res/values/icon_btn_tokens.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> - + diff --git a/material/java/com/google/android/material/button/res/values/styles.xml b/material/java/com/google/android/material/button/res/values/styles.xml index b44b8dbb7..344a474f3 100644 --- a/material/java/com/google/android/material/button/res/values/styles.xml +++ b/material/java/com/google/android/material/button/res/values/styles.xml @@ -122,7 +122,21 @@ - + + + + + + + diff --git a/material/java/com/google/android/material/materialswitch/res/values/tokens.xml b/material/java/com/google/android/material/materialswitch/res/values/tokens.xml index 415a868ad..12e8a4855 100644 --- a/material/java/com/google/android/material/materialswitch/res/values/tokens.xml +++ b/material/java/com/google/android/material/materialswitch/res/values/tokens.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> - + diff --git a/material/java/com/google/android/material/menu/res/values/tokens.xml b/material/java/com/google/android/material/menu/res/values/tokens.xml index adc13fa83..c6b785728 100644 --- a/material/java/com/google/android/material/menu/res/values/tokens.xml +++ b/material/java/com/google/android/material/menu/res/values/tokens.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> - + @@ -23,6 +23,9 @@ ?attr/colorSurfaceContainer @dimen/m3_sys_elevation_level2 - ?attr/colorSecondaryContainer + + + + ?attr/colorSecondaryContainer diff --git a/material/java/com/google/android/material/motion/res/values/tokens.xml b/material/java/com/google/android/material/motion/res/values/tokens.xml index 6e29fb91f..d149345c3 100644 --- a/material/java/com/google/android/material/motion/res/values/tokens.xml +++ b/material/java/com/google/android/material/motion/res/values/tokens.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> - + diff --git a/material/java/com/google/android/material/navigation/NavigationBarMenu.java b/material/java/com/google/android/material/navigation/NavigationBarMenu.java index 9db6b8b18..7f178ca6a 100644 --- a/material/java/com/google/android/material/navigation/NavigationBarMenu.java +++ b/material/java/com/google/android/material/navigation/NavigationBarMenu.java @@ -20,7 +20,6 @@ import android.content.Context; import androidx.appcompat.view.menu.MenuBuilder; -import androidx.appcompat.view.menu.MenuItemImpl; import android.view.MenuItem; import android.view.SubMenu; import androidx.annotation.NonNull; @@ -75,9 +74,6 @@ protected MenuItem addInternal( } stopDispatchingItemsChanged(); final MenuItem item = super.addInternal(group, id, categoryOrder, title); - if (item instanceof MenuItemImpl) { - ((MenuItemImpl) item).setExclusiveCheckable(true); - } startDispatchingItemsChanged(); return item; } diff --git a/material/java/com/google/android/material/navigation/NavigationBarMenuView.java b/material/java/com/google/android/material/navigation/NavigationBarMenuView.java index 78046fc88..df886123d 100644 --- a/material/java/com/google/android/material/navigation/NavigationBarMenuView.java +++ b/material/java/com/google/android/material/navigation/NavigationBarMenuView.java @@ -42,6 +42,7 @@ import androidx.annotation.RestrictTo; import androidx.annotation.StyleRes; import androidx.core.util.Pools; +import androidx.core.util.Pools.SynchronizedPool; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat; import androidx.transition.AutoTransition; @@ -63,7 +64,6 @@ */ @RestrictTo(LIBRARY_GROUP) public abstract class NavigationBarMenuView extends ViewGroup implements MenuView { - private static final int ITEM_POOL_SIZE = 7; private static final int NO_PADDING = -1; private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked}; @@ -71,11 +71,10 @@ public abstract class NavigationBarMenuView extends ViewGroup implements MenuVie @Nullable private final TransitionSet set; @NonNull private final OnClickListener onClickListener; - private final Pools.Pool itemPool = - new Pools.SynchronizedPool<>(ITEM_POOL_SIZE); + @Nullable private Pools.Pool itemPool; @NonNull - private final SparseArray onTouchListeners = new SparseArray<>(ITEM_POOL_SIZE); + private final SparseArray onTouchListeners = new SparseArray<>(); @NavigationBarView.LabelVisibility private int labelVisibilityMode; @@ -96,7 +95,7 @@ public abstract class NavigationBarMenuView extends ViewGroup implements MenuVie @Nullable private ColorStateList itemRippleColor; private int itemBackgroundRes; @NonNull private final SparseArray badgeDrawables = - new SparseArray<>(ITEM_POOL_SIZE); + new SparseArray<>(); private int itemPaddingTop = NO_PADDING; private int itemPaddingBottom = NO_PADDING; private int itemActiveIndicatorLabelPadding = NO_PADDING; @@ -118,6 +117,9 @@ public abstract class NavigationBarMenuView extends ViewGroup implements MenuVie private MenuBuilder menu; private boolean measurePaddingFromLabelBaseline; + private int itemPoolSize = 0; + private MenuItem checkedItem = null; + public NavigationBarMenuView(@NonNull Context context) { super(context); @@ -147,8 +149,11 @@ public NavigationBarMenuView(@NonNull Context context) { public void onClick(View v) { final NavigationBarItemView itemView = (NavigationBarItemView) v; MenuItem item = itemView.getItemData(); - if (!menu.performItemAction(item, presenter, 0)) { - item.setChecked(true); + boolean result = menu.performItemAction(item, presenter, 0); + if (item != null && item.isCheckable() && (!result || item.isChecked())) { + // If the item action was not invoked successfully (ie if there's no listener) or if + // the item was checked through the action, we should update the checked item. + setCheckedItem(item); } } }; @@ -156,6 +161,23 @@ public void onClick(View v) { setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); } + /** + * Set the checked item in the menu view. + * + * @param checkedItem the item to set checked + */ + public void setCheckedItem(@NonNull MenuItem checkedItem) { + if (this.checkedItem == checkedItem || !checkedItem.isCheckable()) { + return; + } + // Unset the previous checked item + if (this.checkedItem != null) { + this.checkedItem.setChecked(false); + } + checkedItem.setChecked(true); + this.checkedItem = checkedItem; + } + @Override public void initialize(@NonNull MenuBuilder menu) { this.menu = menu; @@ -866,7 +888,9 @@ public void buildMenuView() { if (buttons != null) { for (NavigationBarItemView item : buttons) { if (item != null) { - itemPool.release(item); + if (itemPool != null) { + itemPool.release(item); + } item.clear(); } } @@ -876,13 +900,19 @@ public void buildMenuView() { selectedItemId = 0; selectedItemPosition = 0; buttons = null; + itemPool = null; return; } + if (itemPool == null || itemPoolSize != menu.size()) { + itemPool = new SynchronizedPool<>(menu.size()); + itemPoolSize = menu.size(); + } removeUnusedBadges(); - buttons = new NavigationBarItemView[menu.size()]; + int menuSize = menu.size(); + buttons = new NavigationBarItemView[menuSize]; boolean shifting = isShifting(labelVisibilityMode, menu.getVisibleItems().size()); - for (int i = 0; i < menu.size(); i++) { + for (int i = 0; i < menuSize; i++) { presenter.setUpdateSuspended(true); menu.getItem(i).setCheckable(true); presenter.setUpdateSuspended(false); @@ -938,7 +968,7 @@ public void buildMenuView() { addView(child); } selectedItemPosition = Math.min(menu.size() - 1, selectedItemPosition); - menu.getItem(selectedItemPosition).setChecked(true); + setCheckedItem(menu.getItem(selectedItemPosition)); } public void updateMenuView() { @@ -958,6 +988,7 @@ public void updateMenuView() { for (int i = 0; i < menuSize; i++) { MenuItem item = menu.getItem(i); if (item.isChecked()) { + setCheckedItem(item); selectedItemId = item.getItemId(); selectedItemPosition = i; } @@ -980,7 +1011,7 @@ public void updateMenuView() { } private NavigationBarItemView getNewItem() { - NavigationBarItemView item = itemPool.acquire(); + NavigationBarItemView item = itemPool != null ? itemPool.acquire() : null; if (item == null) { item = createNavigationBarItemView(getContext()); } @@ -1005,7 +1036,7 @@ void tryRestoreSelectedItemId(int itemId) { if (itemId == item.getItemId()) { selectedItemId = itemId; selectedItemPosition = i; - item.setChecked(true); + setCheckedItem(item); break; } } diff --git a/material/java/com/google/android/material/navigation/NavigationBarView.java b/material/java/com/google/android/material/navigation/NavigationBarView.java index edd2511d1..97a29a8e5 100644 --- a/material/java/com/google/android/material/navigation/NavigationBarView.java +++ b/material/java/com/google/android/material/navigation/NavigationBarView.java @@ -911,8 +911,11 @@ public int getSelectedItemId() { public void setSelectedItemId(@IdRes int itemId) { MenuItem item = menu.findItem(itemId); if (item != null) { - if (!menu.performItemAction(item, presenter, 0)) { - item.setChecked(true); + boolean result = menu.performItemAction(item, presenter, 0); + // If the item action was not invoked successfully (ie if there's no listener) or if + // the item was checked through the action, we should update the checked item. + if (item.isCheckable() && (!result || item.isChecked())) { + menuView.setCheckedItem(item); } } } diff --git a/material/java/com/google/android/material/navigation/NavigationView.java b/material/java/com/google/android/material/navigation/NavigationView.java index e87273265..4014d7f10 100644 --- a/material/java/com/google/android/material/navigation/NavigationView.java +++ b/material/java/com/google/android/material/navigation/NavigationView.java @@ -142,6 +142,8 @@ public class NavigationView extends ScrimInsetsFrameLayout implements MaterialBa private OnGlobalLayoutListener onGlobalLayoutListener; private boolean topInsetScrimEnabled = true; private boolean bottomInsetScrimEnabled = true; + private boolean startInsetScrimEnabled = true; + private boolean endInsetScrimEnabled = true; @Px private int drawerLayoutCornerSize = 0; private final boolean drawerLayoutCornerSizeBackAnimationEnabled; @@ -328,6 +330,11 @@ public NavigationView(@NonNull Context context, @Nullable AttributeSet attrs, in setBottomInsetScrimEnabled( a.getBoolean(R.styleable.NavigationView_bottomInsetScrimEnabled, bottomInsetScrimEnabled)); + setStartInsetScrimEnabled( + a.getBoolean(R.styleable.NavigationView_startInsetScrimEnabled, startInsetScrimEnabled)); + + setEndInsetScrimEnabled( + a.getBoolean(R.styleable.NavigationView_endInsetScrimEnabled, endInsetScrimEnabled)); final int itemIconPadding = a.getDimensionPixelSize(R.styleable.NavigationView_itemIconPadding, 0); @@ -933,6 +940,34 @@ public void setBottomInsetScrimEnabled(boolean enabled) { this.bottomInsetScrimEnabled = enabled; } + /** Whether or not the NavigationView will draw a scrim behind the window's start inset. */ + public boolean isStartInsetScrimEnabled() { + return this.startInsetScrimEnabled; + } + + /** + * Set whether or not the NavigationView should draw a scrim behind the window's start inset. + * + * @param enabled true when the NavigationView should draw a scrim. + */ + public void setStartInsetScrimEnabled(boolean enabled) { + this.startInsetScrimEnabled = enabled; + } + + /** Whether or not the NavigationView will draw a scrim behind the window's end inset. */ + public boolean isEndInsetScrimEnabled() { + return this.endInsetScrimEnabled; + } + + /** + * Set whether or not the NavigationView should draw a scrim behind the window's end inset. + * + * @param enabled true when the NavigationView should draw a scrim. + */ + public void setEndInsetScrimEnabled(boolean enabled) { + this.endInsetScrimEnabled = enabled; + } + /** * Get the distance between the start edge of the NavigationView and the start of a menu divider. */ @@ -1087,10 +1122,12 @@ public void onGlobalLayout() { presenter.setBehindStatusBar(isBehindStatusBar); setDrawTopInsetForeground(isBehindStatusBar && isTopInsetScrimEnabled()); + boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; // The navigation view could be left aligned or just hidden out of view in a drawer // layout when the global layout listener is called. boolean isOnLeftSide = (tmpLocation[0] == 0) || (tmpLocation[0] + getWidth() == 0); - setDrawLeftInsetForeground(isOnLeftSide); + setDrawLeftInsetForeground( + isOnLeftSide && (isRtl ? isEndInsetScrimEnabled() : isStartInsetScrimEnabled())); Activity activity = ContextUtils.getActivity(getContext()); if (activity != null && VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { @@ -1108,7 +1145,8 @@ public void onGlobalLayout() { (displayBounds.width() == tmpLocation[0]) || (displayBounds.width() - getWidth() == tmpLocation[0]); - setDrawRightInsetForeground(isOnRightSide); + setDrawRightInsetForeground( + isOnRightSide && (isRtl ? isStartInsetScrimEnabled() : isEndInsetScrimEnabled())); } } }; diff --git a/material/java/com/google/android/material/navigation/res-public/values/public.xml b/material/java/com/google/android/material/navigation/res-public/values/public.xml index d62579b7f..87f745c60 100644 --- a/material/java/com/google/android/material/navigation/res-public/values/public.xml +++ b/material/java/com/google/android/material/navigation/res-public/values/public.xml @@ -39,6 +39,8 @@ + + diff --git a/material/java/com/google/android/material/navigation/res/values/attrs.xml b/material/java/com/google/android/material/navigation/res/values/attrs.xml index 0b01bceb0..6aa006fd9 100644 --- a/material/java/com/google/android/material/navigation/res/values/attrs.xml +++ b/material/java/com/google/android/material/navigation/res/values/attrs.xml @@ -199,6 +199,12 @@ + + + + diff --git a/material/java/com/google/android/material/navigation/res/values/tokens.xml b/material/java/com/google/android/material/navigation/res/values/tokens.xml index f895dd903..8fa76c5b7 100644 --- a/material/java/com/google/android/material/navigation/res/values/tokens.xml +++ b/material/java/com/google/android/material/navigation/res/values/tokens.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> - + diff --git a/material/java/com/google/android/material/navigationrail/res/values/tokens.xml b/material/java/com/google/android/material/navigationrail/res/values/tokens.xml index aaae51f9d..7e971251a 100644 --- a/material/java/com/google/android/material/navigationrail/res/values/tokens.xml +++ b/material/java/com/google/android/material/navigationrail/res/values/tokens.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> - + diff --git a/material/java/com/google/android/material/progressindicator/res-public/values/public.xml b/material/java/com/google/android/material/progressindicator/res-public/values/public.xml index cb4773df1..0b2833987 100644 --- a/material/java/com/google/android/material/progressindicator/res-public/values/public.xml +++ b/material/java/com/google/android/material/progressindicator/res-public/values/public.xml @@ -22,7 +22,6 @@ - @@ -38,7 +37,6 @@ - diff --git a/material/java/com/google/android/material/progressindicator/res/values/attrs.xml b/material/java/com/google/android/material/progressindicator/res/values/attrs.xml index 71639dd2f..74bfb5eb6 100644 --- a/material/java/com/google/android/material/progressindicator/res/values/attrs.xml +++ b/material/java/com/google/android/material/progressindicator/res/values/attrs.xml @@ -31,7 +31,7 @@ The indicator color (or colors in an array). By default, it uses theme primary color. --> - + - + - + diff --git a/material/java/com/google/android/material/radiobutton/res/values/tokens.xml b/material/java/com/google/android/material/radiobutton/res/values/tokens.xml index 8949618cd..1051e1799 100644 --- a/material/java/com/google/android/material/radiobutton/res/values/tokens.xml +++ b/material/java/com/google/android/material/radiobutton/res/values/tokens.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> - + diff --git a/material/java/com/google/android/material/resources/res-public/values/public.xml b/material/java/com/google/android/material/resources/res-public/values/public.xml index 2ff60ad24..35ac2e434 100644 --- a/material/java/com/google/android/material/resources/res-public/values/public.xml +++ b/material/java/com/google/android/material/resources/res-public/values/public.xml @@ -40,4 +40,8 @@ + + + + diff --git a/material/java/com/google/android/material/resources/res/values-v21/tokens.xml b/material/java/com/google/android/material/resources/res/values-v21/tokens.xml index d3731a3e0..8a0a2b472 100644 --- a/material/java/com/google/android/material/resources/res/values-v21/tokens.xml +++ b/material/java/com/google/android/material/resources/res/values-v21/tokens.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> - + diff --git a/material/java/com/google/android/material/resources/res/values-v28/tokens.xml b/material/java/com/google/android/material/resources/res/values-v28/tokens.xml index 51835a666..e811da724 100644 --- a/material/java/com/google/android/material/resources/res/values-v28/tokens.xml +++ b/material/java/com/google/android/material/resources/res/values-v28/tokens.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> - + diff --git a/material/java/com/google/android/material/resources/res/values/attrs.xml b/material/java/com/google/android/material/resources/res/values/attrs.xml index c020a3200..cfabd77e0 100644 --- a/material/java/com/google/android/material/resources/res/values/attrs.xml +++ b/material/java/com/google/android/material/resources/res/values/attrs.xml @@ -40,6 +40,10 @@ + + + + diff --git a/material/java/com/google/android/material/resources/res/values/tokens.xml b/material/java/com/google/android/material/resources/res/values/tokens.xml index 99659a26d..78f885eac 100644 --- a/material/java/com/google/android/material/resources/res/values/tokens.xml +++ b/material/java/com/google/android/material/resources/res/values/tokens.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> - + diff --git a/material/java/com/google/android/material/search/res/values/tokens.xml b/material/java/com/google/android/material/search/res/values/tokens.xml index 7127b983b..369e53f73 100644 --- a/material/java/com/google/android/material/search/res/values/tokens.xml +++ b/material/java/com/google/android/material/search/res/values/tokens.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> - + diff --git a/material/java/com/google/android/material/shape/MaterialShapeDrawable.java b/material/java/com/google/android/material/shape/MaterialShapeDrawable.java index dbbac1b6d..4f96f09c5 100644 --- a/material/java/com/google/android/material/shape/MaterialShapeDrawable.java +++ b/material/java/com/google/android/material/shape/MaterialShapeDrawable.java @@ -186,6 +186,8 @@ public CornerSize apply(@NonNull CornerSize cornerSize) { private boolean shadowBitmapDrawingEnable = true; + // Variables for corner morph. + private boolean boundsIsEmpty = true; @NonNull private ShapeAppearanceModel strokeShapeAppearanceModel; @Nullable private SpringForce cornerSpringForce; @NonNull SpringAnimation[] cornerSpringAnimations = new SpringAnimation[NUM_CORNERS]; @@ -1083,8 +1085,10 @@ protected void onBoundsChange(Rect bounds) { strokePathDirty = true; super.onBoundsChange(bounds); if (drawableState.stateListShapeAppearanceModel != null && !bounds.isEmpty()) { - updateShape(getState(), true); + // We only want to skip the corner morph animation if this is the first non-empty bounds. + updateShape(getState(), /* skipAnimation= */ boundsIsEmpty); } + boundsIsEmpty = bounds.isEmpty(); } @Override diff --git a/material/java/com/google/android/material/shape/MaterialShapes.java b/material/java/com/google/android/material/shape/MaterialShapes.java new file mode 100644 index 000000000..5d96a5e4a --- /dev/null +++ b/material/java/com/google/android/material/shape/MaterialShapes.java @@ -0,0 +1,935 @@ +/* + * Copyright (C) 2024 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.google.android.material.shape; + +import static java.lang.Math.PI; +import static java.lang.Math.min; + +import android.graphics.Matrix; +import android.graphics.PointF; +import android.graphics.RectF; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.PathShape; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; +import androidx.graphics.shapes.CornerRounding; +import androidx.graphics.shapes.RoundedPolygon; +import androidx.graphics.shapes.RoundedPolygonKt; +import androidx.graphics.shapes.ShapesKt; +import androidx.graphics.shapes.Shapes_androidKt; +import java.util.Arrays; +import java.util.List; +import org.jetbrains.annotations.Contract; + +/** + * A utility class providing static methods for creating Material endorsed shapes using the {@code + * androidx.graphics.shapes} library. + * + * @hide + */ +@RestrictTo(Scope.LIBRARY_GROUP) +public final class MaterialShapes { + // Cache various roundings for use below + private static final CornerRounding CORNER_ROUND_10 = + new CornerRounding(/* radius= */ .1f, /* smoothing= */ 0f); + private static final CornerRounding CORNER_ROUND_15 = + new CornerRounding(/* radius= */ .15f, /* smoothing= */ 0f); + private static final CornerRounding CORNER_ROUND_20 = + new CornerRounding(/* radius= */ .2f, /* smoothing= */ 0f); + private static final CornerRounding CORNER_ROUND_30 = + new CornerRounding(/* radius= */ .3f, /* smoothing= */ 0f); + private static final CornerRounding CORNER_ROUND_40 = + new CornerRounding(/* radius= */ .4f, /* smoothing= */ 0f); + private static final CornerRounding CORNER_ROUND_50 = + new CornerRounding(/* radius= */ .5f, /* smoothing= */ 0f); + private static final CornerRounding CORNER_ROUND_100 = + new CornerRounding(/* radius= */ 1f, /* smoothing= */ 0f); + + /** A circle shape {@link RoundedPolygon} fitting in a unit circle. */ + public static final RoundedPolygon CIRCLE = normalize(getCircle(), /* radial= */ true); + + /** A rounded square shape {@link RoundedPolygon} fitting in a unit circle. */ + public static final RoundedPolygon SQUARE = normalize(getSquare(), /* radial= */ true); + + /** A slanted rounded square shape {@link RoundedPolygon} fitting in a unit circle. */ + public static final RoundedPolygon SLANTED_SQUARE = + normalize(getSlantedSquare(), /* radial= */ true); + + /** An arch shape {@link RoundedPolygon} fitting in a unit circle. */ + public static final RoundedPolygon ARCH = normalize(getArch(), /* radial= */ true); + + /** A fan shape {@link RoundedPolygon} fitting in a unit circle. */ + public static final RoundedPolygon FAN = + normalize(getFan(/* rotateDegrees= */ -45f), /* radial= */ true); + + /** An arrow shape {@link RoundedPolygon} fitting in a unit circle. */ + public static final RoundedPolygon ARROW = normalize(getArrow(), /* radial= */ true); + + /** A semi-circle shape {@link RoundedPolygon} fitting in a unit circle. */ + public static final RoundedPolygon SEMI_CIRCLE = normalize(getSemiCircle(), /* radial= */ true); + + /** An oval shape {@link RoundedPolygon} fitting in a unit circle. */ + public static final RoundedPolygon OVAL = + normalize(getOval(/* rotateDegrees= */ -45f), /* radial= */ true); + + /** A pill shape {@link RoundedPolygon} fitting in a unit circle. */ + public static final RoundedPolygon PILL = + normalize(getPill(/* rotateDegrees= */ -45f), /* radial= */ true); + + /** A triangle shape {@link RoundedPolygon} fitting in a unit circle. */ + public static final RoundedPolygon TRIANGLE = + normalize(getTriangle(/* rotateDegrees= */ -90f), /* radial= */ true); + + /** A diamond shape {@link RoundedPolygon} fitting in a unit circle. */ + public static final RoundedPolygon DIAMOND = normalize(getDiamond(), /* radial= */ true); + + /** A clam-shell shape {@link RoundedPolygon} fitting in a unit circle. */ + public static final RoundedPolygon CLAM_SHELL = normalize(getClamShell(), /* radial= */ true); + + /** A pentagon shape {@link RoundedPolygon} fitting in a unit circle. */ + public static final RoundedPolygon PENTAGON = + normalize(getPentagon(/* rotateDegrees= */ -18f), /* radial= */ true); + + /** A gem shape {@link RoundedPolygon} fitting in a unit circle. */ + public static final RoundedPolygon GEM = + normalize(getGem(/* rotateDegrees= */ -90f), /* radial= */ true); + + public static final RoundedPolygon SUNNY = normalize(getSunny(), /* radial= */ true); + public static final RoundedPolygon VERY_SUNNY = normalize(getVerySunny(), /* radial= */ true); + public static final RoundedPolygon COOKIE_4 = normalize(getCookie4(), /* radial= */ true); + public static final RoundedPolygon COOKIE_6 = normalize(getCookie6(), /* radial= */ true); + public static final RoundedPolygon COOKIE_7 = normalize(getCookie7(), /* radial= */ true); + public static final RoundedPolygon COOKIE_9 = normalize(getCookie9(), /* radial= */ true); + public static final RoundedPolygon COOKIE_12 = normalize(getCookie12(), /* radial= */ true); + public static final RoundedPolygon GHOSTISH = normalize(getGhostish(), /* radial= */ true); + public static final RoundedPolygon CLOVER_4 = normalize(getClover4(), /* radial= */ true); + public static final RoundedPolygon CLOVER_8 = normalize(getClover8(), /* radial= */ true); + public static final RoundedPolygon BURST = normalize(getBurst(), /* radial= */ true); + public static final RoundedPolygon SOFT_BURST = normalize(getSoftBurst(), /* radial= */ true); + public static final RoundedPolygon BOOM = normalize(getBoom(), /* radial= */ true); + public static final RoundedPolygon SOFT_BOOM = normalize(getSoftBoom(), /* radial= */ true); + public static final RoundedPolygon FLOWER = normalize(getFlower(), /* radial= */ true); + public static final RoundedPolygon PUFFY = normalize(getPuffy(), /* radial= */ true); + public static final RoundedPolygon PUFFY_DIAMOND = + normalize(getPuffyDiamond(), /* radial= */ true); + public static final RoundedPolygon PIXEL_CIRCLE = normalize(getPixelCircle(), /* radial= */ true); + public static final RoundedPolygon PIXEL_TRIANGLE = + normalize(getPixelTriangle(), /* radial= */ true); + public static final RoundedPolygon BUN = normalize(getBun(), /* radial= */ true); + public static final RoundedPolygon HEART = normalize(getHeart(), /* radial= */ true); + + @NonNull + private static RoundedPolygon getCircle() { + return ShapesKt.circle(RoundedPolygon.Companion, /* numVertices= */ 10); + } + + @NonNull + private static RoundedPolygon getSquare() { + return ShapesKt.rectangle( + RoundedPolygon.Companion, + /* width= */ 1f, + /* height= */ 1f, + /* rounding= */ CORNER_ROUND_30, + /* perVertexRounding= */ null, + /* centerX= */ 0f, + /* centerY= */ 0f); + } + + @NonNull + private static RoundedPolygon getSlantedSquare() { + return Shapes_androidKt.transformed(getSquare(), createSkewMatrix(-.1f, 0f)); + } + + @NonNull + private static RoundedPolygon getArch() { + return Shapes_androidKt.transformed( + RoundedPolygonKt.RoundedPolygon( + /* numVertices= */ 4, + /* radius= */ 1f, + /* centerX= */ 0f, + /* centerY= */ 0f, + /* rounding= */ CornerRounding.Unrounded, + /* perVertexRounding= */ Arrays.asList( + CORNER_ROUND_100, CORNER_ROUND_100, CORNER_ROUND_20, CORNER_ROUND_20)), + createRotationMatrix(-135)); + } + + @NonNull + private static RoundedPolygon getFan() { + return RoundedPolygonKt.RoundedPolygon( + /* numVertices= */ 4, + /* radius= */ 1f, + /* centerX= */ 0f, + /* centerY= */ 0f, + CornerRounding.Unrounded, + /* perVertexRounding= */ Arrays.asList( + CORNER_ROUND_100, CORNER_ROUND_20, CORNER_ROUND_20, CORNER_ROUND_20)); + } + + @NonNull + private static RoundedPolygon getFan(float rotateDegrees) { + return Shapes_androidKt.transformed(getFan(), createRotationMatrix(rotateDegrees)); + } + + @NonNull + private static RoundedPolygon getArrow() { + return triangleChip( + /* innerRadius= */ .3375f, + /* cornerRounding= */ new CornerRounding(/* radius= */ .25f, /* smoothing= */ .48f)); + } + + @NonNull + private static RoundedPolygon getSemiCircle() { + return ShapesKt.rectangle( + RoundedPolygon.Companion, + /* width= */ 1.6f, + /* height= */ 1f, + /* rounding= */ CornerRounding.Unrounded, + /* perVertexRounding= */ Arrays.asList( + CORNER_ROUND_20, CORNER_ROUND_20, CORNER_ROUND_100, CORNER_ROUND_100), + /* centerX= */ 0f, + /* centerY= */ 0f); + } + + @NonNull + private static RoundedPolygon getOval() { + return Shapes_androidKt.transformed( + ShapesKt.circle(RoundedPolygon.Companion), + createScaleMatrix(/* scaleX= */ 1f, /* scaleY= */ .7f)); + } + + @NonNull + private static RoundedPolygon getOval(float rotateDegrees) { + return Shapes_androidKt.transformed(getOval(), createRotationMatrix(rotateDegrees)); + } + + @NonNull + private static RoundedPolygon getPill() { + return ShapesKt.pill(RoundedPolygon.Companion, /* width= */ 1.25f, /* height= */ 1f); + } + + @NonNull + private static RoundedPolygon getPill(float rotateDegrees) { + return Shapes_androidKt.transformed(getPill(), createRotationMatrix(rotateDegrees)); + } + + @NonNull + private static RoundedPolygon getTriangle() { + return RoundedPolygonKt.RoundedPolygon( + /* numVertices= */ 3, + /* radius= */ 1f, + /* centerX= */ 0f, + /* centerY= */ 0f, + /* rounding= */ CORNER_ROUND_20); + } + + @NonNull + private static RoundedPolygon getTriangle(float rotateDegrees) { + return Shapes_androidKt.transformed(getTriangle(), createRotationMatrix(rotateDegrees)); + } + + @NonNull + private static RoundedPolygon getDiamond() { + return Shapes_androidKt.transformed( + RoundedPolygonKt.RoundedPolygon( + /* numVertices= */ 4, + /* radius= */ 1f, + /* centerX= */ 0f, + /* centerY= */ 0f, + /* rounding= */ CORNER_ROUND_30), + createScaleMatrix(/* scaleX= */ 1f, /* scaleY= */ 1.2f)); + } + + @NonNull + private static RoundedPolygon getClamShell() { + float cornerInset = .6f; + float edgeInset = .4f; + float height = .7f; + float[] hexPoints = + new float[] { + 1f, + 0f, + cornerInset, + height, + edgeInset, + height, + -edgeInset, + height, + -cornerInset, + height, + -1f, + 0f, + -cornerInset, + -height, + -edgeInset, + -height, + edgeInset, + -height, + cornerInset, + -height + }; + List perVertexRounding = + Arrays.asList( + CORNER_ROUND_30, + CORNER_ROUND_30, + CornerRounding.Unrounded, + CornerRounding.Unrounded, + CORNER_ROUND_30, + CORNER_ROUND_30, + CORNER_ROUND_30, + CornerRounding.Unrounded, + CornerRounding.Unrounded, + CORNER_ROUND_30); + return RoundedPolygonKt.RoundedPolygon( + hexPoints, /* rounding= */ CornerRounding.Unrounded, perVertexRounding); + } + + @NonNull + private static RoundedPolygon getPentagon() { + return RoundedPolygonKt.RoundedPolygon( + /* numVertices= */ 5, + /* radius= */ 1f, + /* centerX= */ 0f, + /* centerY= */ 0f, + /* rounding= */ CORNER_ROUND_30); + } + + @NonNull + private static RoundedPolygon getPentagon(float rotateDegrees) { + return Shapes_androidKt.transformed(getPentagon(), createRotationMatrix(rotateDegrees)); + } + + @NonNull + private static RoundedPolygon getGem() { + // This shape looks like a rounded hexagon with a small offset in the second and last vertices. + int numVertices = 6; + float radius = 1f; + float[] points = new float[numVertices * 2]; + for (int i = 0; i < numVertices; i++) { + PointF vertex = radialToCartesian(new PointF(radius, (float) (2 * PI / numVertices * i))); + points[i * 2] = vertex.x; + points[i * 2 + 1] = vertex.y; + } + // Offsets the second vertex (in the first quadrant) by (-0.1, -0.1) towards the center. + points[2] -= .1f; + points[3] -= .1f; + // Offsets the last vertex (in the fourth quadrant) by (-0.1, 0.1) towards the center. + points[10] -= .1f; + points[11] += .1f; + return RoundedPolygonKt.RoundedPolygon(points, CORNER_ROUND_40); + } + + @NonNull + private static RoundedPolygon getGem(float rotateDegrees) { + return Shapes_androidKt.transformed(getGem(), createRotationMatrix(rotateDegrees)); + } + + @NonNull + private static RoundedPolygon getSunny() { + return ShapesKt.star( + RoundedPolygon.Companion, + /* numVerticesPerRadius= */ 8, + /* radius= */ 1f, + /* innerRadius= */ 0.8f, + /* rounding= */ CORNER_ROUND_15); + } + + @NonNull + private static RoundedPolygon getVerySunny() { + return ShapesKt.star( + RoundedPolygon.Companion, + /* numVerticesPerRadius= */ 8, + /* radius= */ 1f, + /* innerRadius= */ 0.65f, + /* rounding= */ CORNER_ROUND_10); + } + + @NonNull + private static RoundedPolygon getCookie4() { + return Shapes_androidKt.transformed( + ShapesKt.star( + RoundedPolygon.Companion, + /* numVerticesPerRadius= */ 4, + /* radius= */ 1f, + /* innerRadius= */ 0.5f, + /* rounding= */ CORNER_ROUND_30), + createRotationMatrix(-45f)); + } + + @NonNull + private static RoundedPolygon getCookie6() { + return Shapes_androidKt.transformed( + ShapesKt.star( + RoundedPolygon.Companion, + /* numVerticesPerRadius= */ 6, + /* radius= */ 1f, + /* innerRadius= */ 0.75f, + /* rounding= */ CORNER_ROUND_30), + createRotationMatrix(-90f)); + } + + @NonNull + private static RoundedPolygon getCookie7() { + return Shapes_androidKt.transformed( + ShapesKt.star( + RoundedPolygon.Companion, + /* numVerticesPerRadius= */ 7, + /* radius= */ 1f, + /* innerRadius= */ 0.75f, + /* rounding= */ CORNER_ROUND_50), + createRotationMatrix(-90f)); + } + + @NonNull + private static RoundedPolygon getCookie9() { + return Shapes_androidKt.transformed( + ShapesKt.star( + RoundedPolygon.Companion, + /* numVerticesPerRadius= */ 9, + /* radius= */ 1f, + /* innerRadius= */ 0.8f, + /* rounding= */ CORNER_ROUND_50), + createRotationMatrix(-90f)); + } + + @NonNull + private static RoundedPolygon getCookie12() { + return Shapes_androidKt.transformed( + ShapesKt.star( + RoundedPolygon.Companion, + /* numVerticesPerRadius= */ 12, + /* radius= */ 1f, + /* innerRadius= */ 0.8f, + /* rounding= */ CORNER_ROUND_50), + createRotationMatrix(-90f)); + } + + @NonNull + private static RoundedPolygon getGhostish() { + float inset = .46f; + float h = 1.2f; + float[] points = new float[] {-1f, -h, 1f, -h, 1f, h, 0f, inset, -1f, h}; + List perVertexRounding = + Arrays.asList( + CORNER_ROUND_100, CORNER_ROUND_100, CORNER_ROUND_50, CORNER_ROUND_100, CORNER_ROUND_50); + return RoundedPolygonKt.RoundedPolygon( + points, /* rounding= */ CornerRounding.Unrounded, perVertexRounding); + } + + @NonNull + private static RoundedPolygon getClover4() { + // No inner rounding. + return Shapes_androidKt.transformed( + ShapesKt.star( + RoundedPolygon.Companion, + /* numVerticesPerRadius= */ 4, + /* radius= */ 1f, + /* innerRadius= */ 0.2f, + /* rounding= */ CORNER_ROUND_40, + /* innerRounding= */ CornerRounding.Unrounded), + createRotationMatrix(45f)); + } + + @NonNull + private static RoundedPolygon getClover8() { + return Shapes_androidKt.transformed( + ShapesKt.star( + RoundedPolygon.Companion, + /* numVerticesPerRadius= */ 8, + /* radius= */ 1f, + /* innerRadius= */ 0.65f, + /* rounding= */ CORNER_ROUND_30, + /* innerRounding= */ CornerRounding.Unrounded), + createRotationMatrix(360 / 16f)); + } + + @NonNull + private static RoundedPolygon getBurst() { + return ShapesKt.star( + RoundedPolygon.Companion, + /* numVerticesPerRadius= */ 12, + /* radius= */ 1f, + /* innerRadius= */ 0.7f); + } + + @NonNull + private static RoundedPolygon getSoftBurst() { + return Shapes_androidKt.transformed( + ShapesKt.star( + RoundedPolygon.Companion, + /* numVerticesPerRadius= */ 10, + /* radius= */ 1f, + /* innerRadius= */ 0.65f, + /* rounding= */ CORNER_ROUND_10, + /* innerRounding= */ CORNER_ROUND_10), + createRotationMatrix(360 / 20f)); + } + + @NonNull + private static RoundedPolygon getBoom() { + return Shapes_androidKt.transformed( + ShapesKt.star( + RoundedPolygon.Companion, + /* numVerticesPerRadius= */ 15, + /* radius= */ 1f, + /* innerRadius= */ 0.42f), + createRotationMatrix(360 / 60f)); + } + + @NonNull + private static RoundedPolygon getSoftBoom() { + float[] pointsXyTemplate = + new float[] { + 0.456f, 0.224f, + 0.460f, 0.170f, + 0.500f, 0.100f, + 0.540f, 0.170f, + 0.544f, 0.224f, + 0.538f, 0.308f + }; + CornerRounding[] roundingsTemplate = + new CornerRounding[] { + new CornerRounding(/* radius= */ 0.020f, /* smoothing= */ 0f), + new CornerRounding(/* radius= */ 0.143f, /* smoothing= */ 0f), + new CornerRounding(/* radius= */ 0.025f, /* smoothing= */ 0f), + new CornerRounding(/* radius= */ 0.143f, /* smoothing= */ 0f), + new CornerRounding(/* radius= */ 0.190f, /* smoothing= */ 0f), + new CornerRounding(/* radius= */ 0f, /* smoothing= */ 0f) + }; + + float[] pointsXy = new float[pointsXyTemplate.length * 16]; + CornerRounding[] roundings = new CornerRounding[roundingsTemplate.length * 16]; + + repeatAroundCenter( + pointsXyTemplate, + pointsXy, + roundingsTemplate, + roundings, + 16, + 0.5f, + 0.5f, + -360f / 32, + false); + + return RoundedPolygonKt.RoundedPolygon( + pointsXy, + /* rounding= */ CornerRounding.Unrounded, + Arrays.asList(roundings), + /* centerX= */ 0.5f, + /* centerY= */ 0.5f); + } + + @NonNull + private static RoundedPolygon getFlower() { + return ShapesKt.star( + RoundedPolygon.Companion, + /* numVerticesPerRadius= */ 8, + /* radius= */ 1f, + /* innerRadius= */ 0.588f, + /* rounding= */ new CornerRounding(/* radius= */ 0.12f, /* smoothing= */ 0.48f), + /* innerRounding= */ CornerRounding.Unrounded); + } + + @NonNull + private static RoundedPolygon getPuffy() { + float[] pointsXyTemplate = + new float[] { + 0.501f, 0.260f, + 0.526f, 0.188f, + 0.676f, 0.226f, + 0.660f, 0.300f, + 0.734f, 0.230f, + 0.838f, 0.350f, + 0.782f, 0.418f, + 0.874f, 0.414f + }; + CornerRounding[] roundingsTemplate = + new CornerRounding[] { + CornerRounding.Unrounded, + new CornerRounding(/* radius= */ 0.095f, /* smoothing= */ 0f), + new CornerRounding(/* radius= */ 0.095f, /* smoothing= */ 0f), + CornerRounding.Unrounded, + new CornerRounding(/* radius= */ 0.095f, /* smoothing= */ 0f), + new CornerRounding(/* radius= */ 0.095f, /* smoothing= */ 0f), + CornerRounding.Unrounded, + new CornerRounding(/* radius= */ 0.095f, /* smoothing= */ 0f) + }; + + float[] pointsXy = new float[pointsXyTemplate.length * 4]; + CornerRounding[] roundings = new CornerRounding[roundingsTemplate.length * 4]; + + repeatAroundCenter( + pointsXyTemplate, pointsXy, roundingsTemplate, roundings, 4, 0.5f, 0.5f, 0f, true); + + return RoundedPolygonKt.RoundedPolygon( + pointsXy, + /* rounding= */ CornerRounding.Unrounded, + Arrays.asList(roundings), + /* centerX= */ 0.5f, + /* centerY= */ 0.5f); + } + + @NonNull + private static RoundedPolygon getPuffyDiamond() { + float[] pointsXyTemplate = + new float[] { + 0.390f, 0.260f, + 0.390f, 0.130f, + 0.610f, 0.130f, + 0.610f, 0.260f, + 0.740f, 0.260f + }; + CornerRounding[] roundingsTemplate = + new CornerRounding[] { + new CornerRounding(/* radius= */ 0.000f, /* smoothing= */ 0f), + new CornerRounding(/* radius= */ 0.104f, /* smoothing= */ 0f), + new CornerRounding(/* radius= */ 0.104f, /* smoothing= */ 0f), + new CornerRounding(/* radius= */ 0.000f, /* smoothing= */ 0f), + new CornerRounding(/* radius= */ 0.104f, /* smoothing= */ 0f) + }; + + float[] pointsXy = new float[pointsXyTemplate.length * 4]; + CornerRounding[] roundings = new CornerRounding[roundingsTemplate.length * 4]; + + repeatAroundCenter( + pointsXyTemplate, pointsXy, roundingsTemplate, roundings, 4, 0.5f, 0.5f, -24.63f, false); + + return RoundedPolygonKt.RoundedPolygon( + pointsXy, + /* rounding= */ CornerRounding.Unrounded, + Arrays.asList(roundings), + /* centerX= */ 0.5f, + /* centerY= */ 0.5f); + } + + @NonNull + private static RoundedPolygon getPixelCircle() { + PointF start = new PointF(); + PointF[] offsets = + new PointF[] { + new PointF(0.4f, -1f), + new PointF(0f, 0.14f), + new PointF(0.28f, 0f), + new PointF(0f, 0.16f), + new PointF(0.16f, 0f), + new PointF(0f, 0.3f), + new PointF(0.16f, 0f) + }; + float[] pointsXyTemplate = new float[offsets.length * 2]; + for (int i = 0; i < offsets.length; i++) { + start.offset(offsets[i].x, offsets[i].y); + pointsXyTemplate[i * 2] = start.x; + pointsXyTemplate[i * 2 + 1] = start.y; + } + float[] pointsXy = new float[pointsXyTemplate.length * 4]; + repeatAroundCenter(pointsXyTemplate, pointsXy, null, null, 4, 0f, 0f, 0f, true); + return RoundedPolygonKt.RoundedPolygon(pointsXy, /* rounding= */ CornerRounding.Unrounded); + } + + @NonNull + private static RoundedPolygon getPixelTriangle() { + PointF start = new PointF(); + PointF[] offsets = + new PointF[] { + new PointF(), + new PointF(56f, 0f), + new PointF(0f, 28f), + new PointF(44f, 0f), + new PointF(0f, 26f), + new PointF(44f, 0f), + new PointF(0f, 32f), + new PointF(38f, 0f), + new PointF(0f, 26f), + new PointF(38f, 0f), + new PointF(0f, 32f), + new PointF(32f, 0f) + }; + float[] pointsXyTemplate = new float[offsets.length * 2]; + for (int i = 0; i < offsets.length; i++) { + start.offset(offsets[i].x, offsets[i].y); + pointsXyTemplate[i * 2] = start.x; + pointsXyTemplate[i * 2 + 1] = start.y; + } + // Moves start to the center of the shape. + start.offset(-start.x / 2f, 19f); + float[] pointsXy = new float[pointsXyTemplate.length * 2]; + repeatAroundCenter(pointsXyTemplate, pointsXy, null, null, 2, start.x, start.y, -90, true); + return RoundedPolygonKt.RoundedPolygon( + pointsXy, + /* rounding= */ CornerRounding.Unrounded, + /* perVertexRounding= */ null, + /* centerX= */ start.x, + /* centerY= */ start.y); + } + + @NonNull + private static RoundedPolygon getBun() { + float inset = .4f; + float[] sandwichPoints = + new float[] { + 1f, 1f, inset, 1f, -inset, 1f, -1f, 1f, -1f, 0f, -inset, 0f, -1f, 0f, -1f, -1f, -inset, + -1f, inset, -1f, 1f, -1f, 1f, 0f, inset, 0f, 1f, 0f + }; + CornerRounding[] roundings = + new CornerRounding[] { + CORNER_ROUND_100, + CornerRounding.Unrounded, + CornerRounding.Unrounded, + CORNER_ROUND_100, + CORNER_ROUND_100, + CornerRounding.Unrounded, + CORNER_ROUND_100, + CORNER_ROUND_100, + CornerRounding.Unrounded, + CornerRounding.Unrounded, + CORNER_ROUND_100, + CORNER_ROUND_100, + CornerRounding.Unrounded, + CORNER_ROUND_100 + }; + return RoundedPolygonKt.RoundedPolygon( + sandwichPoints, CornerRounding.Unrounded, Arrays.asList(roundings)); + } + + @NonNull + private static RoundedPolygon getHeart() { + float[] points = + new float[] { + .2f, 0f, -.4f, .5f, -1f, 1f, -1.5f, .5f, -1f, 0f, -1.5f, -.5f, -1f, -1f, -.4f, -.5f + }; + CornerRounding[] roundings = + new CornerRounding[] { + CornerRounding.Unrounded, + CornerRounding.Unrounded, + CORNER_ROUND_100, + CORNER_ROUND_100, + CornerRounding.Unrounded, + CORNER_ROUND_100, + CORNER_ROUND_100, + CornerRounding.Unrounded, + }; + return Shapes_androidKt.transformed( + RoundedPolygonKt.RoundedPolygon(points, CornerRounding.Unrounded, Arrays.asList(roundings)), + createRotationMatrix(90f)); + } + + @NonNull + private static RoundedPolygon triangleChip(float innerRadius, CornerRounding cornerRounding) { + PointF[] radialPoints = + new PointF[] { + new PointF(0.888f, (float) Math.toRadians(270f)), + new PointF(1f, (float) Math.toRadians(30f)), + new PointF(innerRadius, (float) Math.toRadians(90f)), + new PointF(1f, (float) Math.toRadians(150f)) + }; + PointF[] cartesianPoints = new PointF[radialPoints.length]; + for (int i = 0; i < radialPoints.length; i++) { + cartesianPoints[i] = radialToCartesian(radialPoints[i]); + } + float[] cartesianPointsXy = new float[cartesianPoints.length * 2]; + for (int i = 0; i < cartesianPoints.length; i++) { + cartesianPointsXy[2 * i] = cartesianPoints[i].x; + cartesianPointsXy[2 * i + 1] = cartesianPoints[i].y; + } + return RoundedPolygonKt.RoundedPolygon(cartesianPointsXy, cornerRounding); + } + + private static void repeatAroundCenter( + @NonNull float[] srcPointsXy, + @NonNull float[] dstPointsXy, + @Nullable CornerRounding[] srcRoundings, + @Nullable CornerRounding[] dstRoundings, + int reps, + float centerX, + float centerY, + float srcRotateOffset, + boolean alternate) { + float rotationOffset = 360f / reps; + int srcPointsCount = srcPointsXy.length / 2; + // Moves src points centered at (0, 0) and rotates src points to start from 0 degree. + Matrix preTransform = createRotationMatrix(-srcRotateOffset); + preTransform.preTranslate(-centerX, -centerY); + preTransform.mapPoints(srcPointsXy); + // Start repeat src points to dst points. + int dstRoundingsWriteIndex = 0; + int dstPointsXyWriteIndex = 0; + for (int i = 0; i < reps; i++) { + // Flips the odd indexed iteration direction, if alternate is true. + boolean flip = alternate && (i % 2 == 1); + for (int j = 0; j < srcPointsCount; j++) { + // Reverts the src points order in dst points, if flips. + int indexRounding = flip ? srcPointsCount - 1 - j : j; + int indexX = indexRounding * 2; + int indexY = indexX + 1; + if (srcRoundings != null && dstRoundings != null) { + dstRoundings[dstRoundingsWriteIndex++] = srcRoundings[indexRounding]; + } + // Flips the src points along y axis, if flips. + dstPointsXy[dstPointsXyWriteIndex++] = srcPointsXy[indexX] * (flip ? -1 : 1); + dstPointsXy[dstPointsXyWriteIndex++] = srcPointsXy[indexY]; + } + // Rotates 1 more offset rotation, if flips. + createRotationMatrix(rotationOffset * (flip ? i + 1 : i)) + .mapPoints( + dstPointsXy, + srcPointsXy.length * i, + dstPointsXy, + srcPointsXy.length * i, + srcPointsCount); + } + // Rotates dst points with the same offset and moves them back to the original center. + Matrix postTransform = createRotationMatrix(srcRotateOffset); + postTransform.postTranslate(centerX, centerY); + postTransform.mapPoints(dstPointsXy); + } + + @NonNull + @Contract("_ -> new") + private static PointF radialToCartesian(@NonNull PointF radialPoint) { + float radius = radialPoint.x; + float angle = radialPoint.y; + return new PointF((float) (radius * Math.cos(angle)), (float) (radius * Math.sin(angle))); + } + + private MaterialShapes() {} + + // ============== Utility methods. ================== + + /** + * Returns a {@link ShapeDrawable} with the shape's path. + * + *

The shape is always assumed to fit in (0, 0) to (1, 1) square. + * + * @param shape A {@link RoundedPolygon} object to be used in the drawable. + * @hide + */ + @NonNull + @RestrictTo(Scope.LIBRARY_GROUP) + public static ShapeDrawable createShapeDrawable(@NonNull RoundedPolygon shape) { + PathShape pathShape = new PathShape(Shapes_androidKt.toPath(shape), 1, 1); + return new ShapeDrawable(pathShape); + } + + /** + * Creates a new {@link RoundedPolygon}, moving and resizing this one, so it's completely inside + * the destination bounds. + * + *

If {@code radial} is true, the shape will be scaled to fit in the biggest circle centered in + * the destination bounds. This is useful when the shape is animated to rotate around its center. + * Otherwise, the shape will be scaled to fit in the destination bounds. With either option, the + * shape's original center will be aligned with the destination bounds center. + * + * @param shape The original {@link RoundedPolygon}. + * @param radial Whether to transform the shape to fit in the biggest circle centered in the + * destination bounds. + * @param dstBounds The destination bounds to fit. + * @return A new {@link RoundedPolygon} that fits in the destination bounds. + * @hide + */ + @NonNull + @RestrictTo(Scope.LIBRARY_GROUP) + public static RoundedPolygon normalize( + @NonNull RoundedPolygon shape, boolean radial, @NonNull RectF dstBounds) { + float[] srcBoundsArray = new float[4]; + if (radial) { + // This calculates the axis-aligned bounds of the shape and returns that rectangle. It + // determines the max dimension of the shape (by calculating the distance from its center to + // the start and midpoint of each curve) and returns a square which can be used to hold the + // object in any rotation. + shape.calculateMaxBounds(srcBoundsArray); + } else { + // This calculates the bounds of the shape without rotating the shape. + shape.calculateBounds(srcBoundsArray); + } + RectF srcBounds = + new RectF(srcBoundsArray[0], srcBoundsArray[1], srcBoundsArray[2], srcBoundsArray[3]); + float scale = + min(dstBounds.width() / srcBounds.width(), dstBounds.height() / srcBounds.height()); + // Scales the shape with pivot point at its original center then moves it to align its original + // center with the destination bounds center. + Matrix transform = createScaleMatrix(scale, scale); + transform.preTranslate(-srcBounds.centerX(), -srcBounds.centerY()); + transform.postTranslate(dstBounds.centerX(), dstBounds.centerY()); + return Shapes_androidKt.transformed(shape, transform); + } + + /** + * Creates a new {@link RoundedPolygon}, moving and resizing this one, so it's completely inside + * (0, 0) - (1, 1) square. + * + *

If {@code radial} is true, the shape will be scaled to fit in the circle centered at (0.5, + * 0.5) with a radius of 0.5. This is useful when the shape is animated to rotate around its + * center. Otherwise, the shape will be scaled to fit in the (0, 0) - (1, 1) square. With either + * option, the shape center will be (0.5, 0.5). + * + * @param shape The original {@link RoundedPolygon}. + * @param radial Whether to transform the shape to fit in the circle centered at (0.5, 0.5) with a + * radius of 0.5. + * @return A new {@link RoundedPolygon} that fits in (0, 0) - (1, 1) square. + * @hide + */ + @NonNull + @RestrictTo(Scope.LIBRARY_GROUP) + public static RoundedPolygon normalize(@NonNull RoundedPolygon shape, boolean radial) { + return normalize(shape, radial, new RectF(0, 0, 1, 1)); + } + + /** + * Returns a {@link Matrix} with the input scales. + * + * @param scaleX Scale in X axis. + * @param scaleY Scale in Y axis + * @hide + */ + @NonNull + @RestrictTo(Scope.LIBRARY_GROUP) + static Matrix createScaleMatrix(float scaleX, float scaleY) { + Matrix matrix = new Matrix(); + matrix.setScale(scaleX, scaleY); + return matrix; + } + + /** + * Returns a {@link Matrix} with the input rotation in degrees. + * + * @param degrees The rotation in degrees. + * @hide + */ + @NonNull + @RestrictTo(Scope.LIBRARY_GROUP) + static Matrix createRotationMatrix(float degrees) { + Matrix matrix = new Matrix(); + matrix.setRotate(degrees); + return matrix; + } + + /** + * Returns a {@link Matrix} with the input skews. + * + * @param kx The skew in X axis. + * @param ky The skew in Y axis. + * @hide + */ + @NonNull + @RestrictTo(Scope.LIBRARY_GROUP) + static Matrix createSkewMatrix(float kx, float ky) { + Matrix matrix = new Matrix(); + matrix.setSkew(kx, ky); + return matrix; + } +} diff --git a/material/java/com/google/android/material/shape/StateListSizeChange.java b/material/java/com/google/android/material/shape/StateListSizeChange.java index 2c3954884..da25498ac 100644 --- a/material/java/com/google/android/material/shape/StateListSizeChange.java +++ b/material/java/com/google/android/material/shape/StateListSizeChange.java @@ -19,6 +19,7 @@ import com.google.android.material.R; import static android.content.res.Resources.ID_NULL; +import static java.lang.Math.max; import android.content.Context; import android.content.res.Resources; @@ -115,6 +116,19 @@ public SizeChange getSizeChangeForState(@NonNull int[] stateSet) { return idx < 0 ? defaultSizeChange : sizeChanges[idx]; } + public int getMaxWidthChange(@Px int baseWidth) { + int maxWidthChange = -baseWidth; + for (int i = 0; i < stateCount; i++) { + SizeChange sizeChange = sizeChanges[i]; + if (sizeChange.widthChange.type == SizeChangeType.PIXELS) { + maxWidthChange = (int) max(maxWidthChange, sizeChange.widthChange.amount); + } else if (sizeChange.widthChange.type == SizeChangeType.PERCENT) { + maxWidthChange = (int) max(maxWidthChange, baseWidth * sizeChange.widthChange.amount); + } + } + return maxWidthChange; + } + private int indexOfStateSet(int[] stateSet) { final int[][] stateSpecs = this.stateSpecs; for (int i = 0; i < stateCount; i++) { @@ -211,11 +225,15 @@ private void growArray(int oldSize, int newSize) { /** A collection of all values needed in a size change. */ public static class SizeChange { - SizeChangeAmount widthChange; + @Nullable public SizeChangeAmount widthChange; SizeChange(@Nullable SizeChangeAmount widthChange) { this.widthChange = widthChange; } + + SizeChange(@NonNull SizeChange other) { + this.widthChange = new SizeChangeAmount(other.widthChange.type, other.widthChange.amount); + } } /** The size change of one dimension, including the type and amount. */ diff --git a/material/java/com/google/android/material/shape/res/values/tokens.xml b/material/java/com/google/android/material/shape/res/values/tokens.xml index 22f669ea2..1636c7851 100644 --- a/material/java/com/google/android/material/shape/res/values/tokens.xml +++ b/material/java/com/google/android/material/shape/res/values/tokens.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> - + diff --git a/material/java/com/google/android/material/sidesheet/res/values/tokens.xml b/material/java/com/google/android/material/sidesheet/res/values/tokens.xml index c7ac8b8ad..fed26f320 100644 --- a/material/java/com/google/android/material/sidesheet/res/values/tokens.xml +++ b/material/java/com/google/android/material/sidesheet/res/values/tokens.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> - + diff --git a/material/java/com/google/android/material/slider/BaseSlider.java b/material/java/com/google/android/material/slider/BaseSlider.java index 6ca6cd685..4996d68bd 100644 --- a/material/java/com/google/android/material/slider/BaseSlider.java +++ b/material/java/com/google/android/material/slider/BaseSlider.java @@ -349,6 +349,9 @@ abstract class BaseSlider< @NonNull private final ViewTreeObserver.OnScrollChangedListener onScrollChangedListener = this::updateLabels; + @NonNull + private final ViewTreeObserver.OnGlobalLayoutListener onGlobalLayoutListener = + this::updateLabels; /** * Determines the behavior of the label which can be any of the following. @@ -1883,6 +1886,7 @@ public void setEnabled(boolean enabled) { protected void onAttachedToWindow() { super.onAttachedToWindow(); getViewTreeObserver().addOnScrollChangedListener(onScrollChangedListener); + getViewTreeObserver().addOnGlobalLayoutListener(onGlobalLayoutListener); // The label is attached on the Overlay relative to the content. for (TooltipDrawable label : labels) { attachLabelToContentView(label); @@ -1904,6 +1908,7 @@ protected void onDetachedFromWindow() { detachLabelFromContentView(label); } getViewTreeObserver().removeOnScrollChangedListener(onScrollChangedListener); + getViewTreeObserver().removeOnGlobalLayoutListener(onGlobalLayoutListener); super.onDetachedFromWindow(); } diff --git a/material/java/com/google/android/material/slider/res/color/m3_slider_active_tick_marks_color.xml b/material/java/com/google/android/material/slider/res/color/m3_slider_active_tick_marks_color.xml index 938fd9c2d..dc0c91af6 100644 --- a/material/java/com/google/android/material/slider/res/color/m3_slider_active_tick_marks_color.xml +++ b/material/java/com/google/android/material/slider/res/color/m3_slider_active_tick_marks_color.xml @@ -15,8 +15,8 @@ ~ limitations under the License. --> - - - + + diff --git a/material/java/com/google/android/material/slider/res/color/m3_slider_inactive_tick_marks_color.xml b/material/java/com/google/android/material/slider/res/color/m3_slider_inactive_tick_marks_color.xml index e7a9e95f3..b44a5b934 100644 --- a/material/java/com/google/android/material/slider/res/color/m3_slider_inactive_tick_marks_color.xml +++ b/material/java/com/google/android/material/slider/res/color/m3_slider_inactive_tick_marks_color.xml @@ -15,8 +15,8 @@ ~ limitations under the License. --> - - - + + diff --git a/material/java/com/google/android/material/slider/res/values/tokens.xml b/material/java/com/google/android/material/slider/res/values/tokens.xml index 204ca9487..fe2d74ee0 100644 --- a/material/java/com/google/android/material/slider/res/values/tokens.xml +++ b/material/java/com/google/android/material/slider/res/values/tokens.xml @@ -15,23 +15,18 @@ ~ limitations under the License. --> - + - - ?attr/colorOnSurface - 0.38 - ?attr/colorOnSurface - 0.12 - - ?attr/colorOnSurface - 0.38 4dp - - ?attr/colorOnSurfaceInverse + + ?attr/colorOnPrimary + 1 + ?attr/colorOnSecondaryContainer + 1 16dp ?attr/colorPrimary @@ -41,6 +36,17 @@ 44dp 4dp 6dp + + ?attr/colorOnSurfaceInverse + ?attr/colorOnSurface + + ?attr/colorOnSurface + 0.38 + ?attr/colorOnSurface + 0.12 + + ?attr/colorOnSurface + 0.38 ?attr/colorSurfaceInverse ?attr/colorOnSurfaceInverse diff --git a/material/java/com/google/android/material/snackbar/BaseTransientBottomBar.java b/material/java/com/google/android/material/snackbar/BaseTransientBottomBar.java index 0eb64846d..3ca755097 100644 --- a/material/java/com/google/android/material/snackbar/BaseTransientBottomBar.java +++ b/material/java/com/google/android/material/snackbar/BaseTransientBottomBar.java @@ -388,8 +388,6 @@ protected BaseTransientBottomBar( view.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); - ViewCompat.setAccessibilityPaneTitle( - view, getContext().getString(R.string.snackbar_accessibility_pane_title)); // Make sure that we fit system windows and have a listener to apply any insets view.setFitsSystemWindows(true); @@ -831,6 +829,10 @@ void onLayoutChange() { } private void showViewImpl() { + if (ViewCompat.getAccessibilityPaneTitle(view) == null) { + ViewCompat.setAccessibilityPaneTitle( + view, getContext().getString(R.string.snackbar_accessibility_pane_title)); + } if (shouldAnimate()) { // If animations are enabled, animate it in animateViewIn(); diff --git a/material/java/com/google/android/material/snackbar/res/values/tokens.xml b/material/java/com/google/android/material/snackbar/res/values/tokens.xml index bdd48a912..e6b75305f 100644 --- a/material/java/com/google/android/material/snackbar/res/values/tokens.xml +++ b/material/java/com/google/android/material/snackbar/res/values/tokens.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> - + diff --git a/material/java/com/google/android/material/tabs/res/values/tokens.xml b/material/java/com/google/android/material/tabs/res/values/tokens.xml index ca99724b9..8d7492b77 100644 --- a/material/java/com/google/android/material/tabs/res/values/tokens.xml +++ b/material/java/com/google/android/material/tabs/res/values/tokens.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> - + diff --git a/material/java/com/google/android/material/textfield/res/values/styles.xml b/material/java/com/google/android/material/textfield/res/values/styles.xml index 556c47d87..b8a5e4e56 100644 --- a/material/java/com/google/android/material/textfield/res/values/styles.xml +++ b/material/java/com/google/android/material/textfield/res/values/styles.xml @@ -460,7 +460,7 @@ - - - - - - diff --git a/material/java/com/google/android/material/typography/res/values-v21/tokens.xml b/material/java/com/google/android/material/typography/res/values-v21/tokens.xml deleted file mode 100644 index d7cbab1bc..000000000 --- a/material/java/com/google/android/material/typography/res/values-v21/tokens.xml +++ /dev/null @@ -1,185 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sans-serif - sans-serif-medium - - - sans-serif - sans-serif-medium - - diff --git a/material/java/com/google/android/material/typography/res/values/attrs.xml b/material/java/com/google/android/material/typography/res/values/attrs.xml index 84707888c..ec475f4a0 100644 --- a/material/java/com/google/android/material/typography/res/values/attrs.xml +++ b/material/java/com/google/android/material/typography/res/values/attrs.xml @@ -47,6 +47,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/material/java/com/google/android/material/typography/res/values/styles.xml b/material/java/com/google/android/material/typography/res/values/styles.xml index 451dbfb2e..d6498f336 100644 --- a/material/java/com/google/android/material/typography/res/values/styles.xml +++ b/material/java/com/google/android/material/typography/res/values/styles.xml @@ -32,6 +32,166 @@ + + + + + + + + + + + + + + + @@ -35,6 +37,8 @@ @string/m3_ref_typeface_brand_regular 45sp 0 + 52sp + 52sp false @@ -43,6 +47,8 @@ @string/m3_ref_typeface_brand_regular 36sp 0 + 44sp + 44sp false @@ -51,6 +57,8 @@ @string/m3_ref_typeface_brand_regular 32sp 0 + 40sp + 40sp false @@ -59,6 +67,8 @@ @string/m3_ref_typeface_brand_regular 28sp 0 + 36sp + 36sp false @@ -67,6 +77,8 @@ @string/m3_ref_typeface_brand_regular 24sp 0 + 32sp + 32sp false @@ -75,26 +87,28 @@ @string/m3_ref_typeface_brand_regular 22sp 0 + 28sp + 28sp false @@ -103,6 +117,8 @@ @string/m3_ref_typeface_plain_regular 16sp 0.03125 + 24sp + 24sp false @@ -111,6 +127,8 @@ @string/m3_ref_typeface_plain_regular 14sp 0.01785714 + 20sp + 20sp false @@ -119,36 +137,38 @@ @string/m3_ref_typeface_plain_regular 12sp 0.03333333 + 16sp + 16sp false diff --git a/recyclerview/build.gradle b/recyclerview/build.gradle index e4919a482..176a0d321 100644 --- a/recyclerview/build.gradle +++ b/recyclerview/build.gradle @@ -4,8 +4,8 @@ plugins { id("kotlinx-serialization") } -//recyclerview 1.4.0-alpha01 -//viewpager 1.1.0-beta02 +//recyclerview 1.4.0-beta01 +//viewpager 1.1.0 android { namespace "androidx.recyclerview"

For more information, see the component + * developer guidance and design + * guidelines. + */ +public class MaterialButtonGroup extends LinearLayout { + + private static final String LOG_TAG = "MButtonGroup"; + private static final int DEF_STYLE_RES = R.style.Widget_Material3_MaterialButtonGroup; + + private final List originalChildShapeAppearanceModels = new ArrayList<>(); + private final List originalChildStateListShapeAppearanceModels = + new ArrayList<>(); + + private final PressedStateTracker pressedStateTracker = new PressedStateTracker(); + private final Comparator childOrderComparator = + (v1, v2) -> { + int checked = Boolean.valueOf(v1.isChecked()).compareTo(v2.isChecked()); + if (checked != 0) { + return checked; + } + + int stateful = Boolean.valueOf(v1.isPressed()).compareTo(v2.isPressed()); + if (stateful != 0) { + return stateful; + } + + // don't return 0s + return Integer.compare(indexOfChild(v1), indexOfChild(v2)); + }; + + private Integer[] childOrder; + + @Nullable StateListCornerSize innerCornerSize; + @Nullable private StateListShapeAppearanceModel groupStateListShapeAppearance; + @Px private int spacing; + @Nullable private StateListSizeChange buttonSizeChange; + + public MaterialButtonGroup(@NonNull Context context) { + this(context, null); + } + + public MaterialButtonGroup(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, R.attr.materialButtonGroupStyle); + } + + public MaterialButtonGroup( + @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(wrap(context, attrs, defStyleAttr, DEF_STYLE_RES), attrs, defStyleAttr); + // Ensure we are using the correctly themed context rather than the context that was passed in. + context = getContext(); + TypedArray attributes = + ThemeEnforcement.obtainStyledAttributes( + context, attrs, R.styleable.MaterialButtonGroup, defStyleAttr, DEF_STYLE_RES); + + if (attributes.hasValue(R.styleable.MaterialButtonGroup_buttonSizeChange)) { + buttonSizeChange = + StateListSizeChange.create( + context, attributes, R.styleable.MaterialButtonGroup_buttonSizeChange); + } + + if (attributes.hasValue(R.styleable.MaterialButtonGroup_shapeAppearance)) { + groupStateListShapeAppearance = + StateListShapeAppearanceModel.create( + context, attributes, R.styleable.MaterialButtonGroup_shapeAppearance); + if (groupStateListShapeAppearance == null) { + groupStateListShapeAppearance = + new StateListShapeAppearanceModel.Builder( + ShapeAppearanceModel.builder( + context, + attributes.getResourceId( + R.styleable.MaterialButtonGroup_shapeAppearance, 0), + attributes.getResourceId( + R.styleable.MaterialButtonGroup_shapeAppearanceOverlay, 0)) + .build()) + .build(); + } + } + if (attributes.hasValue(R.styleable.MaterialButtonGroup_innerCornerSize)) { + innerCornerSize = + StateListCornerSize.create( + context, + attributes, + R.styleable.MaterialButtonGroup_innerCornerSize, + new AbsoluteCornerSize(0)); + } + + spacing = attributes.getDimensionPixelSize(R.styleable.MaterialButtonGroup_android_spacing, 0); + + setChildrenDrawingOrderEnabled(true); + setEnabled(attributes.getBoolean(R.styleable.MaterialButtonGroup_android_enabled, true)); + attributes.recycle(); + } + + @Override + protected void dispatchDraw(@NonNull Canvas canvas) { + updateChildOrder(); + super.dispatchDraw(canvas); + } + + /** + * This override prohibits Views other than {@link MaterialButton} to be added. It also makes + * updates to the add button shape and margins. + */ + @Override + public void addView(@NonNull View child, int index, @Nullable ViewGroup.LayoutParams params) { + if (!(child instanceof MaterialButton)) { + Log.e(LOG_TAG, "Child views must be of type MaterialButton."); + return; + } + + super.addView(child, index, params); + MaterialButton buttonChild = (MaterialButton) child; + setGeneratedIdIfNeeded(buttonChild); + buttonChild.setOnPressedChangeListenerInternal(pressedStateTracker); + + // Saves original child shape appearance. + originalChildShapeAppearanceModels.add(buttonChild.getShapeAppearanceModel()); + originalChildStateListShapeAppearanceModels.add(buttonChild.getStateListShapeAppearanceModel()); + + // Enable children based on the MaterialButtonToggleGroup own isEnabled + buttonChild.setEnabled(isEnabled()); + } + + @Override + public void onViewRemoved(View child) { + super.onViewRemoved(child); + + if (child instanceof MaterialButton) { + ((MaterialButton) child).setOnPressedChangeListenerInternal(null); + } + + int indexOfChild = indexOfChild(child); + if (indexOfChild >= 0) { + originalChildShapeAppearanceModels.remove(indexOfChild); + originalChildStateListShapeAppearanceModels.remove(indexOfChild); + } + + updateChildShapes(); + adjustChildMarginsAndUpdateLayout(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + updateChildShapes(); + adjustChildMarginsAndUpdateLayout(); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + if (changed) { + adjustChildSizeChange(); + } + } + + /** + * Sets all corner radii override to inner corner size except for leftmost and rightmost corners. + */ + @VisibleForTesting + void updateChildShapes() { + // No need to update shape if no inside corners or outer corners are specified. + if (innerCornerSize == null && groupStateListShapeAppearance == null) { + return; + } + int childCount = getChildCount(); + int firstVisibleChildIndex = getFirstVisibleChildIndex(); + int lastVisibleChildIndex = getLastVisibleChildIndex(); + for (int i = 0; i < childCount; i++) { + MaterialButton button = getChildButton(i); + if (button.getVisibility() == GONE) { + continue; + } + boolean isFirstVisible = i == firstVisibleChildIndex; + boolean isLastVisible = i == lastVisibleChildIndex; + + StateListShapeAppearanceModel.Builder originalStateListShapeBuilder = + getOriginalStateListShapeBuilder(isFirstVisible, isLastVisible, i); + // Determines which corners of the original shape should be kept. + boolean isHorizontal = getOrientation() == HORIZONTAL; + boolean isRtl = ViewUtils.isLayoutRtl(this); + int cornerPositionBitsToKeep = 0; + if (isHorizontal) { + // When horizontal (ltr), keeps the left two original corners for the first button. + if (isFirstVisible) { + cornerPositionBitsToKeep |= + StateListShapeAppearanceModel.CORNER_TOP_LEFT + | StateListShapeAppearanceModel.CORNER_BOTTOM_LEFT; + } + // When horizontal (ltr), keeps the right two original corners for the last button. + if (isLastVisible) { + cornerPositionBitsToKeep |= + StateListShapeAppearanceModel.CORNER_TOP_RIGHT + | StateListShapeAppearanceModel.CORNER_BOTTOM_RIGHT; + } + // If rtl, swap the position bits of left corners and right corners. + if (isRtl) { + cornerPositionBitsToKeep = + StateListShapeAppearanceModel.swapCornerPositionRtl(cornerPositionBitsToKeep); + } + } else { + // When vertical, keeps the top two original corners for the first button. + if (isFirstVisible) { + cornerPositionBitsToKeep |= + StateListShapeAppearanceModel.CORNER_TOP_LEFT + | StateListShapeAppearanceModel.CORNER_TOP_RIGHT; + } + // When vertical, keeps the bottom two original corners for the last button. + if (isLastVisible) { + cornerPositionBitsToKeep |= + StateListShapeAppearanceModel.CORNER_BOTTOM_LEFT + | StateListShapeAppearanceModel.CORNER_BOTTOM_RIGHT; + } + } + // Overrides the corners that don't need to keep with unary operator. + int cornerPositionBitsToOverride = ~cornerPositionBitsToKeep; + StateListShapeAppearanceModel newStateListShape = + originalStateListShapeBuilder + .setCornerSizeOverride(innerCornerSize, cornerPositionBitsToOverride) + .build(); + if (newStateListShape.isStateful()) { + button.setStateListShapeAppearanceModel(newStateListShape); + } else { + button.setShapeAppearanceModel( + newStateListShape.getDefaultShape(/* withCornerSizeOverrides= */ true)); + } + } + } + + /** + * Returns a {@link StateListShapeAppearanceModel.Builder} as the original shape of a child + * button. + * + *