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 e70f070bd..033b9258a 100644 Binary files a/libfenrir/src/main/jni/animation/rlottie/rlottie-master.zip and b/libfenrir/src/main/jni/animation/rlottie/rlottie-master.zip differ diff --git a/libfenrir/src/main/jni/animation/rlottie/src/lottie/lottiefiltermodel.h b/libfenrir/src/main/jni/animation/rlottie/src/lottie/lottiefiltermodel.h index 7ca5d83dd..b4a3c245a 100644 --- a/libfenrir/src/main/jni/animation/rlottie/src/lottie/lottiefiltermodel.h +++ b/libfenrir/src/main/jni/animation/rlottie/src/lottie/lottiefiltermodel.h @@ -424,6 +424,25 @@ class Filter : 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}. + * + *

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. + * + *

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"