From 747ed83b765afc0cad1090fa409df2b661f56b47 Mon Sep 17 00:00:00 2001 From: Janne Koschinski <janne@kuschku.de> Date: Tue, 2 Apr 2019 11:12:12 +0200 Subject: [PATCH] UI changes - Avatars are square by default - Implement Speeddial Layout for debug - Prepare for Channel/Query Join/Create UI - Fix and improve Spinner UI --- app/build.gradle.kts | 1 + .../debug/res/values/strings_constants.xml | 1 + app/src/main/AndroidManifest.xml | 20 +- .../quasseldroid/dagger/ActivityModule.kt | 22 + .../quasseldroid/settings/MessageSettings.kt | 2 +- .../chat/add/create/ChannelCreateActivity.kt | 31 + .../chat/add/create/ChannelCreateFragment.kt | 39 + .../create/ChannelCreateFragmentProvider.kt | 34 + .../ui/chat/add/join/ChannelJoinActivity.kt | 31 + .../ui/chat/add/join/ChannelJoinFragment.kt | 39 + .../add/join/ChannelJoinFragmentProvider.kt | 34 + .../ui/chat/add/query/QueryCreateActivity.kt | 31 + .../ui/chat/add/query/QueryCreateFragment.kt | 39 + .../add/query/QueryCreateFragmentProvider.kt | 35 + .../chat/buffers/BufferViewConfigFragment.kt | 50 +- .../chatlist/MinimumActivityAdapter.kt | 2 +- .../coresettings/chatlist/NetworkAdapter.kt | 2 +- .../highlightlist/HighlightNickTypeAdapter.kt | 2 +- .../ignoreitem/IgnoreTypeAdapter.kt | 2 +- .../ignoreitem/ScopeTypeAdapter.kt | 2 +- .../ignoreitem/StrictnessTypeAdapter.kt | 2 +- .../coresettings/network/IdentityAdapter.kt | 2 +- .../networkserver/ProxyTypeAdapter.kt | 2 +- .../ui/info/user/UserInfoFragment.kt | 101 +- .../ui/setup/user/DefaultNetworkAdapter.kt | 2 +- app/src/main/res/color/color_indicator.xml | 6 + .../main/res/color/color_outlined_stroke.xml | 21 + app/src/main/res/drawable/bg_spinner.xml | 7 + app/src/main/res/layout/add_create.xml | 34 + app/src/main/res/layout/add_join.xml | 34 + app/src/main/res/layout/add_query.xml | 34 + app/src/main/res/layout/chat_channel_join.xml | 117 ++ app/src/main/res/layout/chat_chatlist.xml | 12 +- .../main/res/layout/settings_aliasitem.xml | 4 +- app/src/main/res/layout/settings_chatlist.xml | 36 +- .../res/layout/settings_highlightlist.xml | 17 +- .../res/layout/settings_highlightrule.xml | 6 +- app/src/main/res/layout/settings_identity.xml | 16 +- .../main/res/layout/settings_ignoreitem.xml | 65 +- app/src/main/res/layout/settings_network.xml | 37 +- .../res/layout/settings_networkconfig.xml | 10 +- .../res/layout/settings_networkserver.xml | 33 +- .../res/layout/settings_passwordchange.xml | 8 +- .../res/layout/setup_account_connection.xml | 4 +- .../main/res/layout/setup_account_edit.xml | 10 +- .../main/res/layout/setup_account_name.xml | 2 +- .../main/res/layout/setup_account_user.xml | 4 +- .../main/res/layout/setup_network_network.xml | 44 +- .../main/res/layout/setup_user_channels.xml | 2 +- .../main/res/layout/setup_user_identity.xml | 4 +- .../main/res/layout/setup_user_network.xml | 25 +- .../layout/widget_material_spinner_label.xml | 16 + .../res/layout/widget_quassel_setup_entry.xml | 2 +- ...e.xml => widget_spinner_item_material.xml} | 4 +- app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/dimens.xml | 2 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/values/strings_constants.xml | 1 + app/src/main/res/values/styles_widgets.xml | 18 +- app/src/main/res/values/themes_amoled.xml | 3 +- app/src/main/res/values/themes_dracula.xml | 1 + app/src/main/res/values/themes_gruvbox.xml | 2 + app/src/main/res/values/themes_material.xml | 2 + app/src/main/res/values/themes_quassel.xml | 2 + app/src/main/res/values/themes_solarized.xml | 2 + app/src/main/res/xml/preferences.xml | 2 +- gradle.properties | 4 +- settings.gradle | 5 +- ui_spinner/build.gradle.kts | 49 + ui_spinner/proguard-rules.pro | 21 + ui_spinner/src/main/AndroidManifest.xml | 20 + .../de/kuschku/ui/animation/AnimationUtils.kt | 28 + .../ui/animation/AnimatorSetCompat.java | 46 + .../de/kuschku/ui/color/MaterialColors.kt | 63 + .../ui/internal/CollapsingTextHelper.java | 341 ++++ .../ui/internal/DescendantOffsetUtils.java | 92 + .../kuschku/ui/internal/ThemeEnforcement.java | 101 ++ .../ui/resources/MaterialAttributes.java | 39 + .../ui/resources/MaterialResources.java | 69 + .../de/kuschku/ui/shape/CornerFamily.java | 43 + .../de/kuschku/ui/shape/CornerTreatment.java | 77 + .../kuschku/ui/shape/CutCornerTreatment.java | 47 + .../de/kuschku/ui/shape/EdgeTreatment.java | 73 + .../ui/shape/MaterialShapeDrawable.java | 551 ++++++ .../kuschku/ui/shape/MaterialShapeUtils.java | 42 + .../ui/shape/RoundedCornerTreatment.java | 40 + .../ui/shape/ShapeAppearanceModel.java | 637 +++++++ .../ui/shape/ShapeAppearancePathProvider.java | 268 +++ .../java/de/kuschku/ui/shape/ShapePath.kt | 193 +++ .../de/kuschku/ui/spinner/CutoutDrawable.java | 131 ++ .../ui/spinner/IndicatorViewController.java | 587 +++++++ .../ui/spinner/MaterialSpinnerLayout.java | 1544 +++++++++++++++++ .../res/color/md_design_box_stroke_color.xml | 19 + .../src/main/res/color/md_design_error.xml | 17 + .../src/main/res/color/md_mtrl_error.xml | 17 + .../color/md_mtrl_filled_background_color.xml | 21 + .../res/color/md_mtrl_filled_stroke_color.xml | 23 + .../color/md_mtrl_indicator_text_color.xml | 17 + .../color/md_mtrl_outlined_stroke_color.xml | 22 + ui_spinner/src/main/res/values/_attrs.xml | 21 + .../src/main/res/values/color_attrs.xml | 67 + .../src/main/res/values/internal_attrs.xml | 55 + .../src/main/res/values/internal_dimens.xml | 24 + .../src/main/res/values/shape_attrs.xml | 73 + .../src/main/res/values/shape_dimens.xml | 20 + .../src/main/res/values/spinner_attrs.xml | 91 + .../src/main/res/values/spinner_colors.xml | 30 + .../src/main/res/values/spinner_dimens.xml | 33 + .../src/main/res/values/spinner_ids.xml | 24 + .../src/main/res/values/spinner_styles.xml | 180 ++ .../src/main/res/values/typography_attrs.xml | 44 + 111 files changed, 6879 insertions(+), 209 deletions(-) create mode 100644 app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/create/ChannelCreateActivity.kt create mode 100644 app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/create/ChannelCreateFragment.kt create mode 100644 app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/create/ChannelCreateFragmentProvider.kt create mode 100644 app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/join/ChannelJoinActivity.kt create mode 100644 app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/join/ChannelJoinFragment.kt create mode 100644 app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/join/ChannelJoinFragmentProvider.kt create mode 100644 app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/query/QueryCreateActivity.kt create mode 100644 app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/query/QueryCreateFragment.kt create mode 100644 app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/query/QueryCreateFragmentProvider.kt create mode 100644 app/src/main/res/color/color_indicator.xml create mode 100644 app/src/main/res/color/color_outlined_stroke.xml create mode 100644 app/src/main/res/drawable/bg_spinner.xml create mode 100644 app/src/main/res/layout/add_create.xml create mode 100644 app/src/main/res/layout/add_join.xml create mode 100644 app/src/main/res/layout/add_query.xml create mode 100644 app/src/main/res/layout/chat_channel_join.xml create mode 100644 app/src/main/res/layout/widget_material_spinner_label.xml rename app/src/main/res/layout/{widget_spinner_item_inline.xml => widget_spinner_item_material.xml} (91%) create mode 100644 ui_spinner/build.gradle.kts create mode 100644 ui_spinner/proguard-rules.pro create mode 100644 ui_spinner/src/main/AndroidManifest.xml create mode 100644 ui_spinner/src/main/java/de/kuschku/ui/animation/AnimationUtils.kt create mode 100644 ui_spinner/src/main/java/de/kuschku/ui/animation/AnimatorSetCompat.java create mode 100644 ui_spinner/src/main/java/de/kuschku/ui/color/MaterialColors.kt create mode 100644 ui_spinner/src/main/java/de/kuschku/ui/internal/CollapsingTextHelper.java create mode 100644 ui_spinner/src/main/java/de/kuschku/ui/internal/DescendantOffsetUtils.java create mode 100644 ui_spinner/src/main/java/de/kuschku/ui/internal/ThemeEnforcement.java create mode 100644 ui_spinner/src/main/java/de/kuschku/ui/resources/MaterialAttributes.java create mode 100644 ui_spinner/src/main/java/de/kuschku/ui/resources/MaterialResources.java create mode 100644 ui_spinner/src/main/java/de/kuschku/ui/shape/CornerFamily.java create mode 100644 ui_spinner/src/main/java/de/kuschku/ui/shape/CornerTreatment.java create mode 100644 ui_spinner/src/main/java/de/kuschku/ui/shape/CutCornerTreatment.java create mode 100644 ui_spinner/src/main/java/de/kuschku/ui/shape/EdgeTreatment.java create mode 100644 ui_spinner/src/main/java/de/kuschku/ui/shape/MaterialShapeDrawable.java create mode 100644 ui_spinner/src/main/java/de/kuschku/ui/shape/MaterialShapeUtils.java create mode 100644 ui_spinner/src/main/java/de/kuschku/ui/shape/RoundedCornerTreatment.java create mode 100644 ui_spinner/src/main/java/de/kuschku/ui/shape/ShapeAppearanceModel.java create mode 100644 ui_spinner/src/main/java/de/kuschku/ui/shape/ShapeAppearancePathProvider.java create mode 100644 ui_spinner/src/main/java/de/kuschku/ui/shape/ShapePath.kt create mode 100644 ui_spinner/src/main/java/de/kuschku/ui/spinner/CutoutDrawable.java create mode 100644 ui_spinner/src/main/java/de/kuschku/ui/spinner/IndicatorViewController.java create mode 100644 ui_spinner/src/main/java/de/kuschku/ui/spinner/MaterialSpinnerLayout.java create mode 100644 ui_spinner/src/main/res/color/md_design_box_stroke_color.xml create mode 100644 ui_spinner/src/main/res/color/md_design_error.xml create mode 100644 ui_spinner/src/main/res/color/md_mtrl_error.xml create mode 100644 ui_spinner/src/main/res/color/md_mtrl_filled_background_color.xml create mode 100644 ui_spinner/src/main/res/color/md_mtrl_filled_stroke_color.xml create mode 100644 ui_spinner/src/main/res/color/md_mtrl_indicator_text_color.xml create mode 100644 ui_spinner/src/main/res/color/md_mtrl_outlined_stroke_color.xml create mode 100644 ui_spinner/src/main/res/values/_attrs.xml create mode 100644 ui_spinner/src/main/res/values/color_attrs.xml create mode 100644 ui_spinner/src/main/res/values/internal_attrs.xml create mode 100644 ui_spinner/src/main/res/values/internal_dimens.xml create mode 100644 ui_spinner/src/main/res/values/shape_attrs.xml create mode 100644 ui_spinner/src/main/res/values/shape_dimens.xml create mode 100644 ui_spinner/src/main/res/values/spinner_attrs.xml create mode 100644 ui_spinner/src/main/res/values/spinner_colors.xml create mode 100644 ui_spinner/src/main/res/values/spinner_dimens.xml create mode 100644 ui_spinner/src/main/res/values/spinner_ids.xml create mode 100644 ui_spinner/src/main/res/values/spinner_styles.xml create mode 100644 ui_spinner/src/main/res/values/typography_attrs.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2b1e91e03..3a9348b0d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -154,6 +154,7 @@ dependencies { // UI implementation("com.leinardi.android", "speed-dial", "2.0.1") implementation("me.zhanghai.android.materialprogressbar", "library", "1.6.1") + implementation(project(":ui_spinner")) withVersion("0.9.6.0") { implementation("com.afollestad.material-dialogs", "core", version) implementation("com.afollestad.material-dialogs", "commons", version) diff --git a/app/src/debug/res/values/strings_constants.xml b/app/src/debug/res/values/strings_constants.xml index fb13b0aa1..2c87f87db 100644 --- a/app/src/debug/res/values/strings_constants.xml +++ b/app/src/debug/res/values/strings_constants.xml @@ -19,4 +19,5 @@ <resources> <string name="package_name" translatable="false">com.iskrembilen.quasseldroid.debug</string> + <string name="speedial_behavior" translatable="false">com.leinardi.android.speeddial.SpeedDialView$ScrollingViewSnackbarBehavior</string> </resources> diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6caf75e7f..96c17e9a8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -78,7 +78,6 @@ </activity> <!-- Info --> - <activity android:name="de.kuschku.quasseldroid.ui.info.user.UserInfoActivity" android:exported="false" @@ -113,6 +112,23 @@ android:label="@string/label_info_certificate" android:windowSoftInputMode="adjustResize" /> + <!-- Add --> + <activity + android:name="de.kuschku.quasseldroid.ui.chat.add.create.ChannelCreateActivity" + android:exported="false" + android:label="@string/label_create_channel" + android:windowSoftInputMode="adjustResize" /> + <activity + android:name="de.kuschku.quasseldroid.ui.chat.add.join.ChannelJoinActivity" + android:exported="false" + android:label="@string/label_join_long" + android:windowSoftInputMode="adjustResize" /> + <activity + android:name="de.kuschku.quasseldroid.ui.chat.add.query.QueryCreateActivity" + android:exported="false" + android:label="@string/label_query_medium" + android:windowSoftInputMode="adjustResize" /> + <!-- Core Settings --> <activity android:name=".ui.coresettings.CoreSettingsActivity" @@ -264,6 +280,7 @@ android:label="@string/setup_core_title" android:parentActivityName=".ui.chat.ChatActivity" android:windowSoftInputMode="adjustResize" /> + <!-- Core User Setup Flow --> <activity android:name=".ui.setup.user.UserSetupActivity" @@ -271,6 +288,7 @@ android:label="@string/setup_user_title" android:parentActivityName=".ui.chat.ChatActivity" android:windowSoftInputMode="adjustResize" /> + <!-- Network Setup Flow --> <activity android:name=".ui.setup.network.NetworkSetupActivity" diff --git a/app/src/main/java/de/kuschku/quasseldroid/dagger/ActivityModule.kt b/app/src/main/java/de/kuschku/quasseldroid/dagger/ActivityModule.kt index 3feea1e69..845a4fc90 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/dagger/ActivityModule.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/dagger/ActivityModule.kt @@ -26,6 +26,12 @@ import de.kuschku.quasseldroid.service.QuasselServiceModule import de.kuschku.quasseldroid.ui.chat.ChatActivity import de.kuschku.quasseldroid.ui.chat.ChatActivityModule import de.kuschku.quasseldroid.ui.chat.ChatFragmentProvider +import de.kuschku.quasseldroid.ui.chat.add.create.ChannelCreateActivity +import de.kuschku.quasseldroid.ui.chat.add.create.ChannelCreateFragmentProvider +import de.kuschku.quasseldroid.ui.chat.add.join.ChannelJoinActivity +import de.kuschku.quasseldroid.ui.chat.add.join.ChannelJoinFragmentProvider +import de.kuschku.quasseldroid.ui.chat.add.query.QueryCreateActivity +import de.kuschku.quasseldroid.ui.chat.add.query.QueryCreateFragmentProvider import de.kuschku.quasseldroid.ui.chat.topic.TopicFragmentProvider import de.kuschku.quasseldroid.ui.clientsettings.about.AboutActivity import de.kuschku.quasseldroid.ui.clientsettings.about.AboutFragmentProvider @@ -99,6 +105,8 @@ abstract class ActivityModule { @ContributesAndroidInjector(modules = [ChatActivityModule::class, ChatFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class]) abstract fun bindChatActivity(): ChatActivity + // Info + @ActivityScope @ContributesAndroidInjector(modules = [UserInfoFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class]) abstract fun bindUserInfoActivity(): UserInfoActivity @@ -123,6 +131,20 @@ abstract class ActivityModule { @ContributesAndroidInjector(modules = [CertificateInfoFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class]) abstract fun bindCertificateInfoActivity(): CertificateInfoActivity + // Add + + @ActivityScope + @ContributesAndroidInjector(modules = [ChannelCreateFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class]) + abstract fun bindChannelCreateActivity(): ChannelCreateActivity + + @ActivityScope + @ContributesAndroidInjector(modules = [ChannelJoinFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class]) + abstract fun bindChannelJoinActivity(): ChannelJoinActivity + + @ActivityScope + @ContributesAndroidInjector(modules = [QueryCreateFragmentProvider::class, SettingsModule::class, DatabaseModule::class, ActivityBaseModule::class]) + abstract fun bindQueryCreateActivity(): QueryCreateActivity + // Client Settings @ActivityScope diff --git a/app/src/main/java/de/kuschku/quasseldroid/settings/MessageSettings.kt b/app/src/main/java/de/kuschku/quasseldroid/settings/MessageSettings.kt index 56b54be0b..762bf8fe1 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/settings/MessageSettings.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/settings/MessageSettings.kt @@ -32,7 +32,7 @@ data class MessageSettings( val timeAtEnd: Boolean = false, val showRealNames: Boolean = false, val showAvatars: Boolean = true, - val squareAvatars: Boolean = false, + val squareAvatars: Boolean = true, val showIRCCloudAvatars: Boolean = false, val showGravatarAvatars: Boolean = false, val showMatrixAvatars: Boolean = false, diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/create/ChannelCreateActivity.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/create/ChannelCreateActivity.kt new file mode 100644 index 000000000..b7a982e4e --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/create/ChannelCreateActivity.kt @@ -0,0 +1,31 @@ +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2019 Janne Koschinski + * Copyright (c) 2019 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.kuschku.quasseldroid.ui.chat.add.create + +import android.content.Context +import android.content.Intent +import de.kuschku.quasseldroid.util.ui.settings.ServiceBoundSettingsActivity + +class ChannelCreateActivity : ServiceBoundSettingsActivity(ChannelCreateFragment()) { + companion object { + fun launch(context: Context) = context.startActivity(intent(context)) + fun intent(context: Context) = Intent(context, ChannelCreateActivity::class.java) + } +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/create/ChannelCreateFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/create/ChannelCreateFragment.kt new file mode 100644 index 000000000..6b1075521 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/create/ChannelCreateFragment.kt @@ -0,0 +1,39 @@ +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2019 Janne Koschinski + * Copyright (c) 2019 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.kuschku.quasseldroid.ui.chat.add.create + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import butterknife.ButterKnife +import de.kuschku.quasseldroid.R +import de.kuschku.quasseldroid.util.service.ServiceBoundFragment + +class ChannelCreateFragment : ServiceBoundFragment() { + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.add_create, container, false) + ButterKnife.bind(this, view) + + return view + } +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/create/ChannelCreateFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/create/ChannelCreateFragmentProvider.kt new file mode 100644 index 000000000..f81371ccb --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/create/ChannelCreateFragmentProvider.kt @@ -0,0 +1,34 @@ +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2019 Janne Koschinski + * Copyright (c) 2019 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.kuschku.quasseldroid.ui.chat.add.create + +import androidx.fragment.app.FragmentActivity +import dagger.Binds +import dagger.Module +import dagger.android.ContributesAndroidInjector + +@Module +abstract class ChannelCreateFragmentProvider { + @Binds + abstract fun bindFragmentActivity(activity: ChannelCreateActivity): FragmentActivity + + @ContributesAndroidInjector + abstract fun bindChannelCreateFragment(): ChannelCreateFragment +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/join/ChannelJoinActivity.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/join/ChannelJoinActivity.kt new file mode 100644 index 000000000..22a5fb7cb --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/join/ChannelJoinActivity.kt @@ -0,0 +1,31 @@ +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2019 Janne Koschinski + * Copyright (c) 2019 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.kuschku.quasseldroid.ui.chat.add.join + +import android.content.Context +import android.content.Intent +import de.kuschku.quasseldroid.util.ui.settings.ServiceBoundSettingsActivity + +class ChannelJoinActivity : ServiceBoundSettingsActivity(ChannelJoinFragment()) { + companion object { + fun launch(context: Context) = context.startActivity(intent(context)) + fun intent(context: Context) = Intent(context, ChannelJoinActivity::class.java) + } +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/join/ChannelJoinFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/join/ChannelJoinFragment.kt new file mode 100644 index 000000000..0785d1c61 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/join/ChannelJoinFragment.kt @@ -0,0 +1,39 @@ +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2019 Janne Koschinski + * Copyright (c) 2019 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.kuschku.quasseldroid.ui.chat.add.join + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import butterknife.ButterKnife +import de.kuschku.quasseldroid.R +import de.kuschku.quasseldroid.util.service.ServiceBoundFragment + +class ChannelJoinFragment : ServiceBoundFragment() { + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.add_join, container, false) + ButterKnife.bind(this, view) + + return view + } +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/join/ChannelJoinFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/join/ChannelJoinFragmentProvider.kt new file mode 100644 index 000000000..cd44ae8d5 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/join/ChannelJoinFragmentProvider.kt @@ -0,0 +1,34 @@ +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2019 Janne Koschinski + * Copyright (c) 2019 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.kuschku.quasseldroid.ui.chat.add.join + +import androidx.fragment.app.FragmentActivity +import dagger.Binds +import dagger.Module +import dagger.android.ContributesAndroidInjector + +@Module +abstract class ChannelJoinFragmentProvider { + @Binds + abstract fun bindFragmentActivity(activity: ChannelJoinActivity): FragmentActivity + + @ContributesAndroidInjector + abstract fun bindChannelJoinFragment(): ChannelJoinFragment +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/query/QueryCreateActivity.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/query/QueryCreateActivity.kt new file mode 100644 index 000000000..7c20d2d07 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/query/QueryCreateActivity.kt @@ -0,0 +1,31 @@ +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2019 Janne Koschinski + * Copyright (c) 2019 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.kuschku.quasseldroid.ui.chat.add.query + +import android.content.Context +import android.content.Intent +import de.kuschku.quasseldroid.util.ui.settings.ServiceBoundSettingsActivity + +class QueryCreateActivity : ServiceBoundSettingsActivity(QueryCreateFragment()) { + companion object { + fun launch(context: Context) = context.startActivity(intent(context)) + fun intent(context: Context) = Intent(context, QueryCreateActivity::class.java) + } +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/query/QueryCreateFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/query/QueryCreateFragment.kt new file mode 100644 index 000000000..6c8b37e52 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/query/QueryCreateFragment.kt @@ -0,0 +1,39 @@ +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2019 Janne Koschinski + * Copyright (c) 2019 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.kuschku.quasseldroid.ui.chat.add.query + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import butterknife.ButterKnife +import de.kuschku.quasseldroid.R +import de.kuschku.quasseldroid.util.service.ServiceBoundFragment + +class QueryCreateFragment : ServiceBoundFragment() { + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.add_query, container, false) + ButterKnife.bind(this, view) + + return view + } +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/query/QueryCreateFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/query/QueryCreateFragmentProvider.kt new file mode 100644 index 000000000..3d1536183 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/query/QueryCreateFragmentProvider.kt @@ -0,0 +1,35 @@ +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2019 Janne Koschinski + * Copyright (c) 2019 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.kuschku.quasseldroid.ui.chat.add.query + +import androidx.fragment.app.FragmentActivity +import dagger.Binds +import dagger.Module +import dagger.android.ContributesAndroidInjector +import de.kuschku.quasseldroid.ui.chat.add.query.QueryCreateActivity + +@Module +abstract class QueryCreateFragmentProvider { + @Binds + abstract fun bindFragmentActivity(activity: QueryCreateActivity): FragmentActivity + + @ContributesAndroidInjector + abstract fun bindQueryCreateFragment(): QueryCreateFragment +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferViewConfigFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferViewConfigFragment.kt index 916cdbf17..7705bd346 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferViewConfigFragment.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferViewConfigFragment.kt @@ -26,6 +26,7 @@ import android.text.TextWatcher import android.view.* import android.widget.AdapterView import android.widget.EditText +import androidx.annotation.ColorInt import androidx.appcompat.widget.AppCompatImageButton import androidx.appcompat.widget.AppCompatSpinner import androidx.appcompat.widget.Toolbar @@ -36,6 +37,8 @@ import androidx.recyclerview.widget.RecyclerView import butterknife.BindView import butterknife.ButterKnife import com.afollestad.materialdialogs.MaterialDialog +import com.leinardi.android.speeddial.SpeedDialActionItem +import com.leinardi.android.speeddial.SpeedDialView import de.kuschku.libquassel.protocol.BufferId import de.kuschku.libquassel.protocol.Buffer_Activity import de.kuschku.libquassel.protocol.Buffer_Type @@ -50,12 +53,16 @@ import de.kuschku.libquassel.util.helpers.mapMap import de.kuschku.libquassel.util.helpers.mapOrElse import de.kuschku.libquassel.util.helpers.nullIf import de.kuschku.libquassel.util.helpers.value +import de.kuschku.quasseldroid.BuildConfig import de.kuschku.quasseldroid.R import de.kuschku.quasseldroid.persistence.db.AccountDatabase import de.kuschku.quasseldroid.persistence.db.QuasselDatabase import de.kuschku.quasseldroid.settings.AppearanceSettings import de.kuschku.quasseldroid.settings.MessageSettings import de.kuschku.quasseldroid.ui.chat.ChatActivity +import de.kuschku.quasseldroid.ui.chat.add.create.ChannelCreateActivity +import de.kuschku.quasseldroid.ui.chat.add.join.ChannelJoinActivity +import de.kuschku.quasseldroid.ui.chat.add.query.QueryCreateActivity import de.kuschku.quasseldroid.ui.coresettings.network.NetworkEditActivity import de.kuschku.quasseldroid.ui.info.channellist.ChannelListActivity import de.kuschku.quasseldroid.util.ColorContext @@ -91,10 +98,9 @@ class BufferViewConfigFragment : ServiceBoundFragment() { @BindView(R.id.buffer_search_container) lateinit var bufferSearchContainer: ViewGroup -/* - @BindView(R.id.fab) + + @BindView(R.id.fab_chatlist) lateinit var fab: SpeedDialView - */ @Inject lateinit var appearanceSettings: AppearanceSettings @@ -535,16 +541,27 @@ class BufferViewConfigFragment : ServiceBoundFragment() { bufferSearch.setText("") } - /* @ColorInt var colorLabel = 0 @ColorInt var colorLabelBackground = 0 - view.context.theme.styledAttributes(R.attr.colorTextPrimary, R.attr.colorBackgroundCard) { + + @ColorInt var fabBackground0 = 0 + @ColorInt var fabBackground1 = 0 + @ColorInt var fabBackground2 = 0 + view.context.theme.styledAttributes( + R.attr.colorTextPrimary, R.attr.colorBackgroundCard, + R.attr.senderColorF, R.attr.senderColorE, R.attr.senderColorD + ) { colorLabel = getColor(0, 0) colorLabelBackground = getColor(1, 0) + + fabBackground0 = getColor(2, 0) + fabBackground1 = getColor(3, 0) + fabBackground2 = getColor(4, 0) } fab.addActionItem( SpeedDialActionItem.Builder(R.id.fab_create, R.drawable.ic_add) + .setFabBackgroundColor(fabBackground0) .setFabImageTintColor(0xffffffffu.toInt()) .setLabel(R.string.label_create_channel) .setLabelBackgroundColor(colorLabelBackground) @@ -554,6 +571,7 @@ class BufferViewConfigFragment : ServiceBoundFragment() { fab.addActionItem( SpeedDialActionItem.Builder(R.id.fab_join, R.drawable.ic_channel) + .setFabBackgroundColor(fabBackground1) .setFabImageTintColor(0xffffffffu.toInt()) .setLabel(R.string.label_join_long) .setLabelBackgroundColor(colorLabelBackground) @@ -563,13 +581,33 @@ class BufferViewConfigFragment : ServiceBoundFragment() { fab.addActionItem( SpeedDialActionItem.Builder(R.id.fab_query, R.drawable.ic_account) + .setFabBackgroundColor(fabBackground2) .setFabImageTintColor(0xffffffffu.toInt()) .setLabel(R.string.label_query_medium) .setLabelBackgroundColor(colorLabelBackground) .setLabelColor(colorLabel) .create() ) - */ + + fab.setOnActionSelectedListener { + when (it.id) { + R.id.fab_query -> { + context?.let(QueryCreateActivity.Companion::launch) + true + } + R.id.fab_join -> { + context?.let(ChannelJoinActivity.Companion::launch) + true + } + R.id.fab_create -> { + context?.let(ChannelCreateActivity.Companion::launch) + true + } + else -> false + } + } + + fab.visibleIf(BuildConfig.DEBUG) return view } diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/chatlist/MinimumActivityAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/chatlist/MinimumActivityAdapter.kt index bb9fe01f0..3976983a7 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/chatlist/MinimumActivityAdapter.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/chatlist/MinimumActivityAdapter.kt @@ -49,7 +49,7 @@ class MinimumActivityAdapter(val data: List<MinimumActivityItem>) : else parent.context ) - val view = inflater.inflate(R.layout.widget_spinner_item_inline, parent, false) + val view = inflater.inflate(R.layout.widget_spinner_item_material, parent, false) return MinimumActivityViewHolder( view ) diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/chatlist/NetworkAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/chatlist/NetworkAdapter.kt index 3d14d42d8..716e77072 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/chatlist/NetworkAdapter.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/chatlist/NetworkAdapter.kt @@ -57,7 +57,7 @@ class NetworkAdapter(@StringRes private val fallbackName: Int) : else parent.context ) - val view = inflater.inflate(R.layout.widget_spinner_item_inline, parent, false) + val view = inflater.inflate(R.layout.widget_spinner_item_material, parent, false) return NetworkViewHolder(fallbackName, view) } diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/highlightlist/HighlightNickTypeAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/highlightlist/HighlightNickTypeAdapter.kt index 6be35ad76..d50d0fbd1 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/highlightlist/HighlightNickTypeAdapter.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/highlightlist/HighlightNickTypeAdapter.kt @@ -49,7 +49,7 @@ class HighlightNickTypeAdapter(val data: List<HighlightNickTypeItem>) : else parent.context ) - val view = inflater.inflate(R.layout.widget_spinner_item_inline, parent, false) + val view = inflater.inflate(R.layout.widget_spinner_item_material, parent, false) return HighlightNickTypeViewHolder( view ) diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/ignoreitem/IgnoreTypeAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/ignoreitem/IgnoreTypeAdapter.kt index 627be7c79..8a6585ff6 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/ignoreitem/IgnoreTypeAdapter.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/ignoreitem/IgnoreTypeAdapter.kt @@ -49,7 +49,7 @@ class IgnoreTypeAdapter(val data: List<IgnoreTypeItem>) : else parent.context ) - val view = inflater.inflate(R.layout.widget_spinner_item_inline, parent, false) + val view = inflater.inflate(R.layout.widget_spinner_item_material, parent, false) return IgnoreTypeViewHolder( view ) diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/ignoreitem/ScopeTypeAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/ignoreitem/ScopeTypeAdapter.kt index 0b98f098b..e6343fee9 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/ignoreitem/ScopeTypeAdapter.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/ignoreitem/ScopeTypeAdapter.kt @@ -49,7 +49,7 @@ class ScopeTypeAdapter(val data: List<ScopeTypeItem>) : else parent.context ) - val view = inflater.inflate(R.layout.widget_spinner_item_inline, parent, false) + val view = inflater.inflate(R.layout.widget_spinner_item_material, parent, false) return ScopeTypeViewHolder( view ) diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/ignoreitem/StrictnessTypeAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/ignoreitem/StrictnessTypeAdapter.kt index 9fbe0fc4d..0004cdb5b 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/ignoreitem/StrictnessTypeAdapter.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/ignoreitem/StrictnessTypeAdapter.kt @@ -49,7 +49,7 @@ class StrictnessTypeAdapter(val data: List<StrictnessTypeItem>) : else parent.context ) - val view = inflater.inflate(R.layout.widget_spinner_item_inline, parent, false) + val view = inflater.inflate(R.layout.widget_spinner_item_material, parent, false) return StrictnessTypeViewHolder( view ) diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/network/IdentityAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/network/IdentityAdapter.kt index 3bbfaf988..0d9bec9c6 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/network/IdentityAdapter.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/network/IdentityAdapter.kt @@ -52,7 +52,7 @@ class IdentityAdapter : RecyclerSpinnerAdapter<IdentityAdapter.NetworkViewHolder val inflater = LayoutInflater.from( if (dropDown) ContextThemeWrapper(parent.context, dropDownViewTheme) else parent.context ) - return NetworkViewHolder(inflater.inflate(R.layout.widget_spinner_item_inline, parent, false)) + return NetworkViewHolder(inflater.inflate(R.layout.widget_spinner_item_material, parent, false)) } fun indexOf(id: IdentityId): Int? { diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/networkserver/ProxyTypeAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/networkserver/ProxyTypeAdapter.kt index 7a61e90c6..53d96b2bd 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/networkserver/ProxyTypeAdapter.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/networkserver/ProxyTypeAdapter.kt @@ -47,7 +47,7 @@ class ProxyTypeAdapter(val data: List<ProxyTypeItem>) : else parent.context ) return ProxyTypeViewHolder( - inflater.inflate(R.layout.widget_spinner_item_inline, parent, false) + inflater.inflate(R.layout.widget_spinner_item_material, parent, false) ) } diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/info/user/UserInfoFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/info/user/UserInfoFragment.kt index 1b6ed5b76..adb6a8c3c 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/info/user/UserInfoFragment.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/info/user/UserInfoFragment.kt @@ -48,6 +48,7 @@ import de.kuschku.libquassel.util.Optional import de.kuschku.libquassel.util.helpers.nullIf import de.kuschku.libquassel.util.helpers.value import de.kuschku.libquassel.util.irc.HostmaskHelper +import de.kuschku.libquassel.util.irc.IrcCaseMappers import de.kuschku.quasseldroid.R import de.kuschku.quasseldroid.settings.MessageSettings import de.kuschku.quasseldroid.ui.chat.ChatActivity @@ -185,53 +186,67 @@ class UserInfoFragment : ServiceBoundFragment() { ))) user == IrcUser.NULL -> Observable.just(Optional.empty()) else -> { - combineLatest(user.channels().map { channelName -> - user.network().liveIrcChannel( - channelName - ).switchMap { channel -> - channel.updates().map { - bufferSyncer?.find( - bufferName = channelName, - networkId = user.network().networkId() - )?.let { info -> - val bufferStatus = - if (it == IrcChannel.NULL) BufferStatus.OFFLINE - else BufferStatus.ONLINE - val color = - if (bufferStatus == BufferStatus.ONLINE) colorAccent - else colorAway - val fallbackDrawable = colorContext.buildTextDrawable("#", color) - - BufferProps( - info = info, - network = user.network().networkInfo(), - description = it.topic(), - activity = Message_Type.of(), - bufferStatus = bufferStatus, - hiddenState = BufferHiddenState.VISIBLE, - networkConnectionState = user.network().connectionState(), - fallbackDrawable = fallbackDrawable + fun buildUserInfo(channels: List<BufferProps>) = IrcUserInfo( + networkId = user.network().networkId(), + nick = user.nick(), + user = user.user(), + host = user.host(), + account = user.account(), + server = user.server(), + realName = user.realName(), + isAway = user.isAway(), + awayMessage = user.awayMessage(), + network = user.network(), + knownToCore = true, + info = info, + ircUser = user, + channels = channels.sortedBy { + IrcCaseMappers.unicode.toLowerCaseNullable(it.info.bufferName) + } + ) + + if (user.channels().isEmpty()) { + Observable.just(Optional.of( + buildUserInfo(emptyList()) + )) + } else { + combineLatest(user.channels().map { channelName -> + user.network().liveIrcChannel( + channelName + ).switchMap { channel -> + channel.updates().map { + Optional.ofNullable( + bufferSyncer?.find( + bufferName = channelName, + networkId = user.network().networkId() + )?.let { info -> + val bufferStatus = + if (it == IrcChannel.NULL) BufferStatus.OFFLINE + else BufferStatus.ONLINE + val color = + if (bufferStatus == BufferStatus.ONLINE) colorAccent + else colorAway + val fallbackDrawable = colorContext.buildTextDrawable("#", color) + + BufferProps( + info = info, + network = user.network().networkInfo(), + description = it.topic(), + activity = Message_Type.of(), + bufferStatus = bufferStatus, + hiddenState = BufferHiddenState.VISIBLE, + networkConnectionState = user.network().connectionState(), + fallbackDrawable = fallbackDrawable + ) + } ) } } + }).map { + it.mapNotNull(Optional<BufferProps>::orNull) + }.map { + Optional.of(buildUserInfo(it)) } - }).map { - Optional.of(IrcUserInfo( - networkId = user.network().networkId(), - nick = user.nick(), - user = user.user(), - host = user.host(), - account = user.account(), - server = user.server(), - realName = user.realName(), - isAway = user.isAway(), - awayMessage = user.awayMessage(), - network = user.network(), - knownToCore = true, - info = info, - ircUser = user, - channels = it.filterNotNull() - )) } } } diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/setup/user/DefaultNetworkAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/setup/user/DefaultNetworkAdapter.kt index 06f8256d3..67a510b41 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/setup/user/DefaultNetworkAdapter.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/setup/user/DefaultNetworkAdapter.kt @@ -51,7 +51,7 @@ class DefaultNetworkAdapter @Inject constructor(defaultNetworks: DefaultNetworks else parent.context ) - val view = inflater.inflate(R.layout.widget_spinner_item_inline, parent, false) + val view = inflater.inflate(R.layout.widget_spinner_item_material, parent, false) return DefaultNetworkViewHolder(view) } diff --git a/app/src/main/res/color/color_indicator.xml b/app/src/main/res/color/color_indicator.xml new file mode 100644 index 000000000..7ade179a9 --- /dev/null +++ b/app/src/main/res/color/color_indicator.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="?attr/colorAccent" android:state_activated="true" /> + <item android:alpha="0.38" android:color="?attr/colorOnSurface" android:state_enabled="false" /> + <item android:alpha="0.6" android:color="?attr/colorOnSurface" /> +</selector> diff --git a/app/src/main/res/color/color_outlined_stroke.xml b/app/src/main/res/color/color_outlined_stroke.xml new file mode 100644 index 000000000..4d6f9b841 --- /dev/null +++ b/app/src/main/res/color/color_outlined_stroke.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + Copyright (C) 2018 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. +--> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="?colorAccent" android:state_focused="true" /> + <item android:alpha="0.87" android:color="?colorTextSecondary" android:state_hovered="true" /> + <item android:alpha="0.12" android:color="?colorTextSecondary" android:state_enabled="false" /> + <item android:alpha="0.38" android:color="?colorTextSecondary" /> +</selector> diff --git a/app/src/main/res/drawable/bg_spinner.xml b/app/src/main/res/drawable/bg_spinner.xml new file mode 100644 index 000000000..60a856a1d --- /dev/null +++ b/app/src/main/res/drawable/bg_spinner.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <corners android:radius="4dp" /> + <stroke + android:width="1dp" + android:color="@color/color_indicator" /> +</shape> diff --git a/app/src/main/res/layout/add_create.xml b/app/src/main/res/layout/add_create.xml new file mode 100644 index 000000000..4aa14e116 --- /dev/null +++ b/app/src/main/res/layout/add_create.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:padding="16dp"> + + <de.kuschku.ui.spinner.MaterialSpinnerLayout + style="@style/Widget.CustomSpinnerLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/label_network"> + + <androidx.appcompat.widget.AppCompatSpinner + android:id="@+id/network" + style="@style/Widget.MaterialSpinner" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:listitem="@layout/widget_spinner_item_material" /> + </de.kuschku.ui.spinner.MaterialSpinnerLayout> + + <com.google.android.material.textfield.TextInputLayout + style="@style/Widget.CustomTextInput" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/settings_aliasitem_name"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/name" + style="@style/Widget.CoreSettings.EditText" + tools:text="back" /> + </com.google.android.material.textfield.TextInputLayout> +</LinearLayout> diff --git a/app/src/main/res/layout/add_join.xml b/app/src/main/res/layout/add_join.xml new file mode 100644 index 000000000..4aa14e116 --- /dev/null +++ b/app/src/main/res/layout/add_join.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:padding="16dp"> + + <de.kuschku.ui.spinner.MaterialSpinnerLayout + style="@style/Widget.CustomSpinnerLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/label_network"> + + <androidx.appcompat.widget.AppCompatSpinner + android:id="@+id/network" + style="@style/Widget.MaterialSpinner" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:listitem="@layout/widget_spinner_item_material" /> + </de.kuschku.ui.spinner.MaterialSpinnerLayout> + + <com.google.android.material.textfield.TextInputLayout + style="@style/Widget.CustomTextInput" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/settings_aliasitem_name"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/name" + style="@style/Widget.CoreSettings.EditText" + tools:text="back" /> + </com.google.android.material.textfield.TextInputLayout> +</LinearLayout> diff --git a/app/src/main/res/layout/add_query.xml b/app/src/main/res/layout/add_query.xml new file mode 100644 index 000000000..4aa14e116 --- /dev/null +++ b/app/src/main/res/layout/add_query.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:padding="16dp"> + + <de.kuschku.ui.spinner.MaterialSpinnerLayout + style="@style/Widget.CustomSpinnerLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/label_network"> + + <androidx.appcompat.widget.AppCompatSpinner + android:id="@+id/network" + style="@style/Widget.MaterialSpinner" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:listitem="@layout/widget_spinner_item_material" /> + </de.kuschku.ui.spinner.MaterialSpinnerLayout> + + <com.google.android.material.textfield.TextInputLayout + style="@style/Widget.CustomTextInput" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/settings_aliasitem_name"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/name" + style="@style/Widget.CoreSettings.EditText" + tools:text="back" /> + </com.google.android.material.textfield.TextInputLayout> +</LinearLayout> diff --git a/app/src/main/res/layout/chat_channel_join.xml b/app/src/main/res/layout/chat_channel_join.xml new file mode 100644 index 000000000..f635d3d9d --- /dev/null +++ b/app/src/main/res/layout/chat_channel_join.xml @@ -0,0 +1,117 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + Quasseldroid - Quassel client for Android + + Copyright (c) 2019 Janne Koschinski + Copyright (c) 2019 The Quassel Project + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU General Public License version 3 as published + by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program. If not, see <http://www.gnu.org/licenses/>. + --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <de.kuschku.ui.spinner.MaterialSpinnerLayout + style="@style/Widget.CustomSpinnerLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/label_network"> + + <androidx.appcompat.widget.AppCompatSpinner + android:id="@+id/network" + style="@style/Widget.MaterialSpinner" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:listitem="@layout/widget_spinner_item_material" /> + </de.kuschku.ui.spinner.MaterialSpinnerLayout> + + <androidx.cardview.widget.CardView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="6dp" + app:cardBackgroundColor="?colorBackgroundCard" + app:cardElevation="2dp"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <EditText + android:id="@+id/search_input" + android:layout_width="0dip" + android:layout_height="wrap_content" + android:layout_weight="1" + android:background="@android:color/transparent" + android:hint="@string/label_search_channels" + android:imeOptions="actionSearch" + android:importantForAutofill="no" + android:inputType="textNoSuggestions" + android:lines="1" + android:minHeight="40dp" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:textColor="?colorTextPrimary" + android:textColorHint="?colorTextSecondary" + android:textSize="16sp" /> + + <androidx.appcompat.widget.AppCompatImageButton + android:id="@+id/search_button" + android:layout_width="40dp" + android:layout_height="match_parent" + android:background="?selectableItemBackgroundBorderless" + android:contentDescription="@string/label_search" + app:srcCompat="@drawable/ic_search" + app:tint="?colorTextSecondary" /> + + </LinearLayout> + + <de.kuschku.quasseldroid.util.ui.view.MaterialContentLoadingProgressBar + android:id="@+id/progress" + style="@style/Widget.MaterialProgressBar.ProgressBar.Horizontal.NoPadding" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="bottom" + android:visibility="gone" + app:mpb_progressStyle="horizontal" + app:mpb_setBothDrawables="true" + app:mpb_useIntrinsicPadding="false" + tools:indeterminate="true" /> + + </LinearLayout> + + </androidx.cardview.widget.CardView> + + <de.kuschku.quasseldroid.util.ui.view.WarningBarView + android:id="@+id/error" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:icon="@drawable/ic_alert" + app:mode="none" /> + + <de.kuschku.quasseldroid.util.ui.fastscroll.views.FastScrollRecyclerView + android:id="@+id/search_results" + style="@style/Widget.FastScroller" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:listitem="@layout/widget_channel_search" /> + +</LinearLayout> diff --git a/app/src/main/res/layout/chat_chatlist.xml b/app/src/main/res/layout/chat_chatlist.xml index b5136c4f4..2e499427e 100644 --- a/app/src/main/res/layout/chat_chatlist.xml +++ b/app/src/main/res/layout/chat_chatlist.xml @@ -73,20 +73,20 @@ app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:listitem="@layout/widget_buffer" /> - <!-- <com.leinardi.android.speeddial.SpeedDialOverlayLayout - android:id="@+id/overlay" + android:id="@+id/fab_chatlist_overlay" android:layout_width="match_parent" android:layout_height="match_parent" + android:background="?colorBackgroundAlpha" app:layout_behavior="@string/appbar_scrolling_view_behavior" /> <com.leinardi.android.speeddial.SpeedDialView - android:id="@+id/fab" + android:id="@+id/fab_chatlist" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" - app:layout_behavior="@string/speeddial_scrolling_view_snackbar_behavior" + app:layout_behavior="@string/speedial_behavior" app:sdMainFabClosedSrc="@drawable/ic_add" - app:sdOverlayLayout="@id/overlay" /> - --> + app:sdOverlayLayout="@id/fab_chatlist_overlay" /> + </androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/app/src/main/res/layout/settings_aliasitem.xml b/app/src/main/res/layout/settings_aliasitem.xml index 58d1a9064..e0324bf52 100644 --- a/app/src/main/res/layout/settings_aliasitem.xml +++ b/app/src/main/res/layout/settings_aliasitem.xml @@ -47,7 +47,7 @@ android:minHeight="48dp"> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_aliasitem_name"> @@ -63,7 +63,7 @@ <LinearLayout style="@style/Widget.CoreSettings.Wrapper"> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_aliasitem_expansion"> diff --git a/app/src/main/res/layout/settings_chatlist.xml b/app/src/main/res/layout/settings_chatlist.xml index 62db4198b..bea2ca3bf 100644 --- a/app/src/main/res/layout/settings_chatlist.xml +++ b/app/src/main/res/layout/settings_chatlist.xml @@ -45,7 +45,7 @@ android:visibility="visible"> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_chatlist_buffer_view_name"> @@ -93,10 +93,19 @@ style="@style/Widget.CoreSettings.DependentGroup" android:visibility="visible"> - <Spinner - android:id="@+id/network_id" - style="@style/Widget.FullWidthSpinner" - tools:listitem="@layout/widget_spinner_item_inline" /> + <de.kuschku.ui.spinner.MaterialSpinnerLayout + style="@style/Widget.CustomSpinnerLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/settings_chatlist_network"> + + <androidx.appcompat.widget.AppCompatSpinner + android:id="@+id/network_id" + style="@style/Widget.MaterialSpinner" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:listitem="@layout/widget_spinner_item_material" /> + </de.kuschku.ui.spinner.MaterialSpinnerLayout> <androidx.appcompat.widget.SwitchCompat android:id="@+id/show_status_buffer" @@ -154,10 +163,19 @@ style="@style/Widget.CoreSettings.DependentGroup" android:visibility="visible"> - <Spinner - android:id="@+id/minimum_activity" - style="@style/Widget.FullWidthSpinner" - tools:listitem="@layout/widget_spinner_item_inline" /> + <de.kuschku.ui.spinner.MaterialSpinnerLayout + style="@style/Widget.CustomSpinnerLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/settings_chatlist_activity"> + + <androidx.appcompat.widget.AppCompatSpinner + android:id="@+id/minimum_activity" + style="@style/Widget.MaterialSpinner" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:listitem="@layout/widget_spinner_item_material" /> + </de.kuschku.ui.spinner.MaterialSpinnerLayout> <androidx.appcompat.widget.SwitchCompat android:id="@+id/hide_inactive_buffers" diff --git a/app/src/main/res/layout/settings_highlightlist.xml b/app/src/main/res/layout/settings_highlightlist.xml index 6ae568f3a..90baabbda 100644 --- a/app/src/main/res/layout/settings_highlightlist.xml +++ b/app/src/main/res/layout/settings_highlightlist.xml @@ -57,10 +57,19 @@ style="@style/Widget.CoreSettings.DependentGroup" android:visibility="visible"> - <Spinner - android:id="@+id/highlight_nick_type" - style="@style/Widget.FullWidthSpinner" - tools:listitem="@layout/widget_spinner_item_inline" /> + <de.kuschku.ui.spinner.MaterialSpinnerLayout + style="@style/Widget.CustomSpinnerLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/settings_highlightlist_highlight_nick"> + + <androidx.appcompat.widget.AppCompatSpinner + android:id="@+id/highlight_nick_type" + style="@style/Widget.MaterialSpinner" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:listitem="@layout/widget_spinner_item_material" /> + </de.kuschku.ui.spinner.MaterialSpinnerLayout> <androidx.appcompat.widget.SwitchCompat android:id="@+id/is_case_sensitive" diff --git a/app/src/main/res/layout/settings_highlightrule.xml b/app/src/main/res/layout/settings_highlightrule.xml index 2636be27b..7823f5d31 100644 --- a/app/src/main/res/layout/settings_highlightrule.xml +++ b/app/src/main/res/layout/settings_highlightrule.xml @@ -47,7 +47,7 @@ android:visibility="visible"> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_highlightrule_name"> @@ -69,7 +69,7 @@ android:text="@string/settings_highlightrule_case_sensitive" /> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_highlightrule_sender"> @@ -81,7 +81,7 @@ </com.google.android.material.textfield.TextInputLayout> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_highlightrule_channel"> diff --git a/app/src/main/res/layout/settings_identity.xml b/app/src/main/res/layout/settings_identity.xml index 193c403f4..6583c7bdd 100644 --- a/app/src/main/res/layout/settings_identity.xml +++ b/app/src/main/res/layout/settings_identity.xml @@ -45,7 +45,7 @@ android:visibility="visible"> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_identity_identity_name"> @@ -57,7 +57,7 @@ </com.google.android.material.textfield.TextInputLayout> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_identity_real_name"> @@ -69,7 +69,7 @@ </com.google.android.material.textfield.TextInputLayout> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_identity_ident"> @@ -138,7 +138,7 @@ android:visibility="visible"> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_identity_kick_reason"> @@ -150,7 +150,7 @@ </com.google.android.material.textfield.TextInputLayout> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_identity_part_reason"> @@ -162,7 +162,7 @@ </com.google.android.material.textfield.TextInputLayout> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_identity_quit_reason"> @@ -193,7 +193,7 @@ android:visibility="visible"> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_identity_away_reason"> @@ -215,7 +215,7 @@ android:layout_height="wrap_content"> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_identity_detach_away_reason"> diff --git a/app/src/main/res/layout/settings_ignoreitem.xml b/app/src/main/res/layout/settings_ignoreitem.xml index 65108129c..f2b3c0cee 100644 --- a/app/src/main/res/layout/settings_ignoreitem.xml +++ b/app/src/main/res/layout/settings_ignoreitem.xml @@ -47,7 +47,7 @@ android:visibility="visible"> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_ignoreitem_ignorerule"> @@ -63,23 +63,33 @@ style="@style/Widget.CoreSettings.PrimaryItemSwitch" android:text="@string/settings_ignoreitem_isregex" /> - <TextView - style="@style/Widget.CoreSettings.EditTextHeader" - android:text="@string/settings_ignoreitem_type" /> - - <Spinner - android:id="@+id/type" - style="@style/Widget.FullWidthSpinner" - tools:listitem="@layout/widget_spinner_item_inline" /> - - <TextView - style="@style/Widget.CoreSettings.EditTextHeader" - android:text="@string/settings_ignoreitem_strictness" /> - - <Spinner - android:id="@+id/strictness" - style="@style/Widget.FullWidthSpinner" - tools:listitem="@layout/widget_spinner_item_inline" /> + <de.kuschku.ui.spinner.MaterialSpinnerLayout + style="@style/Widget.CustomSpinnerLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/settings_ignoreitem_type"> + + <androidx.appcompat.widget.AppCompatSpinner + android:id="@+id/type" + style="@style/Widget.MaterialSpinner" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:listitem="@layout/widget_spinner_item_material" /> + </de.kuschku.ui.spinner.MaterialSpinnerLayout> + + <de.kuschku.ui.spinner.MaterialSpinnerLayout + style="@style/Widget.CustomSpinnerLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/settings_ignoreitem_strictness"> + + <androidx.appcompat.widget.AppCompatSpinner + android:id="@+id/strictness" + style="@style/Widget.MaterialSpinner" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:listitem="@layout/widget_spinner_item_material" /> + </de.kuschku.ui.spinner.MaterialSpinnerLayout> </LinearLayout> <LinearLayout @@ -100,10 +110,19 @@ style="@style/Widget.CoreSettings.DependentGroup" android:visibility="visible"> - <Spinner - android:id="@+id/scope" - style="@style/Widget.FullWidthSpinner" - tools:listitem="@layout/widget_spinner_item_inline" /> + <de.kuschku.ui.spinner.MaterialSpinnerLayout + style="@style/Widget.CustomSpinnerLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/settings_ignoreitem_scope"> + + <androidx.appcompat.widget.AppCompatSpinner + android:id="@+id/scope" + style="@style/Widget.MaterialSpinner" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:listitem="@layout/widget_spinner_item_material" /> + </de.kuschku.ui.spinner.MaterialSpinnerLayout> </LinearLayout> <LinearLayout @@ -112,7 +131,7 @@ android:visibility="visible"> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_ignoreitem_scoperule"> diff --git a/app/src/main/res/layout/settings_network.xml b/app/src/main/res/layout/settings_network.xml index 587a9a732..547d838c1 100644 --- a/app/src/main/res/layout/settings_network.xml +++ b/app/src/main/res/layout/settings_network.xml @@ -45,7 +45,7 @@ android:visibility="visible"> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_network_network_name"> @@ -102,16 +102,19 @@ style="@style/Widget.CoreSettings.DependentGroup" android:visibility="visible"> - <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + <de.kuschku.ui.spinner.MaterialSpinnerLayout android:layout_width="match_parent" - android:layout_height="wrap_content"> + style="@style/Widget.CustomSpinnerLayout" + android:layout_height="wrap_content" + android:hint="@string/settings_network_identity"> - <Spinner + <androidx.appcompat.widget.AppCompatSpinner android:id="@+id/identity" - style="@style/Widget.FullWidthSpinner" - tools:listitem="@layout/widget_spinner_item_inline" /> - </com.google.android.material.textfield.TextInputLayout> + style="@style/Widget.MaterialSpinner" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:listitem="@layout/widget_spinner_item_material" /> + </de.kuschku.ui.spinner.MaterialSpinnerLayout> </LinearLayout> <LinearLayout @@ -135,7 +138,7 @@ tools:visibility="gone"> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_network_sasl_account"> @@ -147,7 +150,7 @@ </com.google.android.material.textfield.TextInputLayout> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_network_sasl_password" @@ -184,7 +187,7 @@ tools:visibility="gone"> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_network_autoidentify_service"> @@ -196,7 +199,7 @@ </com.google.android.material.textfield.TextInputLayout> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_network_autoidentify_password" @@ -229,7 +232,7 @@ android:visibility="visible"> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_network_perform"> @@ -272,7 +275,7 @@ android:layout_height="wrap_content"> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_network_autoreconnect_interval"> @@ -289,7 +292,7 @@ </FrameLayout> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_network_autoreconnect_attempts"> @@ -332,7 +335,7 @@ android:text="@string/settings_network_customratelimits_unlimited" /> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_network_customratelimits_burstsize"> @@ -348,7 +351,7 @@ android:layout_height="wrap_content"> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_network_customratelimits_delay"> diff --git a/app/src/main/res/layout/settings_networkconfig.xml b/app/src/main/res/layout/settings_networkconfig.xml index b60501388..683927ab1 100644 --- a/app/src/main/res/layout/settings_networkconfig.xml +++ b/app/src/main/res/layout/settings_networkconfig.xml @@ -51,7 +51,7 @@ android:layout_height="wrap_content"> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_networkconfig_ping_interval"> @@ -73,7 +73,7 @@ android:layout_height="wrap_content"> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_networkconfig_max_ping_count"> @@ -116,7 +116,7 @@ android:layout_height="wrap_content"> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_networkconfig_auto_who_interval"> @@ -138,7 +138,7 @@ android:layout_height="wrap_content"> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_networkconfig_auto_who_nick_limit"> @@ -160,7 +160,7 @@ android:layout_height="wrap_content"> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_networkconfig_auto_who_delay"> diff --git a/app/src/main/res/layout/settings_networkserver.xml b/app/src/main/res/layout/settings_networkserver.xml index 0f2516fed..4f7b96b25 100644 --- a/app/src/main/res/layout/settings_networkserver.xml +++ b/app/src/main/res/layout/settings_networkserver.xml @@ -45,7 +45,7 @@ android:visibility="visible"> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_networkserver_host"> @@ -57,7 +57,7 @@ </com.google.android.material.textfield.TextInputLayout> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_networkserver_port"> @@ -80,7 +80,7 @@ android:text="@string/settings_networkserver_ssl_verify" /> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_networkserver_password" @@ -114,17 +114,22 @@ style="@style/Widget.CoreSettings.DependentGroup" tools:visibility="visible"> - <TextView - style="@style/Widget.CoreSettings.EditTextHeader" - android:text="@string/settings_networkserver_proxy_type" /> + <de.kuschku.ui.spinner.MaterialSpinnerLayout + style="@style/Widget.CustomSpinnerLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/settings_networkserver_proxy_type"> - <Spinner - android:id="@+id/proxy_type" - style="@style/Widget.FullWidthSpinner" - tools:listitem="@layout/widget_spinner_item_inline" /> + <androidx.appcompat.widget.AppCompatSpinner + android:id="@+id/proxy_type" + style="@style/Widget.MaterialSpinner" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:listitem="@layout/widget_spinner_item_material" /> + </de.kuschku.ui.spinner.MaterialSpinnerLayout> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_networkserver_proxy_host"> @@ -136,7 +141,7 @@ </com.google.android.material.textfield.TextInputLayout> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_networkserver_proxy_port"> @@ -149,7 +154,7 @@ </com.google.android.material.textfield.TextInputLayout> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_networkserver_proxy_user"> @@ -161,7 +166,7 @@ </com.google.android.material.textfield.TextInputLayout> <com.google.android.material.textfield.TextInputLayout - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_networkserver_proxy_pass" diff --git a/app/src/main/res/layout/settings_passwordchange.xml b/app/src/main/res/layout/settings_passwordchange.xml index 3cbbdf6ab..7cc1fb441 100644 --- a/app/src/main/res/layout/settings_passwordchange.xml +++ b/app/src/main/res/layout/settings_passwordchange.xml @@ -32,7 +32,7 @@ <com.google.android.material.textfield.TextInputLayout android:id="@+id/userWrapper" - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="16dp" @@ -54,7 +54,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="16dp" - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:hint="@string/label_password_old" app:passwordToggleEnabled="true" tools:ignore="LabelFor"> @@ -71,7 +71,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/label_password_new" - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" app:passwordToggleEnabled="true" tools:ignore="LabelFor"> @@ -88,7 +88,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="16dp" - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:hint="@string/label_password_repeat" app:passwordToggleEnabled="true" tools:ignore="LabelFor"> diff --git a/app/src/main/res/layout/setup_account_connection.xml b/app/src/main/res/layout/setup_account_connection.xml index ca8dc500a..776b92642 100644 --- a/app/src/main/res/layout/setup_account_connection.xml +++ b/app/src/main/res/layout/setup_account_connection.xml @@ -29,7 +29,7 @@ <com.google.android.material.textfield.TextInputLayout android:id="@+id/hostWrapper" - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/label_connection_host" @@ -48,7 +48,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/label_connection_port" - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" app:errorEnabled="true" tools:ignore="LabelFor"> diff --git a/app/src/main/res/layout/setup_account_edit.xml b/app/src/main/res/layout/setup_account_edit.xml index 8ef9ed22c..26535ff1a 100644 --- a/app/src/main/res/layout/setup_account_edit.xml +++ b/app/src/main/res/layout/setup_account_edit.xml @@ -53,7 +53,7 @@ <com.google.android.material.textfield.TextInputLayout android:id="@+id/nameWrapper" - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/label_account_name" @@ -98,7 +98,7 @@ <com.google.android.material.textfield.TextInputLayout android:id="@+id/hostWrapper" - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/label_connection_host" @@ -114,7 +114,7 @@ <com.google.android.material.textfield.TextInputLayout android:id="@+id/portWrapper" - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/label_connection_port" @@ -170,7 +170,7 @@ <com.google.android.material.textfield.TextInputLayout android:id="@+id/userWrapper" android:layout_width="match_parent" - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_height="wrap_content" android:hint="@string/label_account_user" tools:ignore="LabelFor"> @@ -186,7 +186,7 @@ <com.google.android.material.textfield.TextInputLayout android:id="@+id/passWrapper" android:layout_width="match_parent" - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_height="wrap_content" android:hint="@string/label_account_pass" app:passwordToggleEnabled="true" diff --git a/app/src/main/res/layout/setup_account_name.xml b/app/src/main/res/layout/setup_account_name.xml index d6a746daa..c7b4235a2 100644 --- a/app/src/main/res/layout/setup_account_name.xml +++ b/app/src/main/res/layout/setup_account_name.xml @@ -29,7 +29,7 @@ android:id="@+id/nameWrapper" android:layout_width="match_parent" android:layout_height="wrap_content" - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:hint="@string/label_account_name" app:errorEnabled="true" tools:ignore="LabelFor"> diff --git a/app/src/main/res/layout/setup_account_user.xml b/app/src/main/res/layout/setup_account_user.xml index e340c247b..8b9306e93 100644 --- a/app/src/main/res/layout/setup_account_user.xml +++ b/app/src/main/res/layout/setup_account_user.xml @@ -30,7 +30,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/label_account_user" - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" tools:ignore="LabelFor"> <com.google.android.material.textfield.TextInputEditText @@ -46,7 +46,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/label_account_pass" - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" app:passwordToggleEnabled="true" tools:ignore="LabelFor"> diff --git a/app/src/main/res/layout/setup_network_network.xml b/app/src/main/res/layout/setup_network_network.xml index c4366d386..820dd537d 100644 --- a/app/src/main/res/layout/setup_network_network.xml +++ b/app/src/main/res/layout/setup_network_network.xml @@ -25,16 +25,19 @@ android:orientation="vertical" android:padding="32dp"> - <TextView - style="@style/Widget.CoreSettings.EditTextHeader" - android:text="@string/settings_network_title" /> - - <Spinner - android:id="@+id/network" - style="@style/Widget.FullWidthSpinner" + <de.kuschku.ui.spinner.MaterialSpinnerLayout + style="@style/Widget.CustomSpinnerLayout" android:layout_width="match_parent" android:layout_height="wrap_content" - tools:listitem="@layout/widget_spinner_item_inline" /> + android:hint="@string/settings_network_title"> + + <androidx.appcompat.widget.AppCompatSpinner + android:id="@+id/network" + style="@style/Widget.MaterialSpinner" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:listitem="@layout/widget_spinner_item_material" /> + </de.kuschku.ui.spinner.MaterialSpinnerLayout> <LinearLayout android:id="@+id/network_group" @@ -42,20 +45,23 @@ android:layout_height="wrap_content" android:orientation="vertical"> - <TextView - style="@style/Widget.CoreSettings.EditTextHeader" - android:text="@string/settings_identity_title" /> - - <Spinner - android:id="@+id/identity" - style="@style/Widget.FullWidthSpinner" + <de.kuschku.ui.spinner.MaterialSpinnerLayout + style="@style/Widget.CustomSpinnerLayout" android:layout_width="match_parent" android:layout_height="wrap_content" - tools:listitem="@layout/widget_spinner_item_inline" /> + android:hint="@string/settings_identity_title"> + + <androidx.appcompat.widget.AppCompatSpinner + android:id="@+id/identity" + style="@style/Widget.MaterialSpinner" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:listitem="@layout/widget_spinner_item_material" /> + </de.kuschku.ui.spinner.MaterialSpinnerLayout> <com.google.android.material.textfield.TextInputLayout android:id="@+id/nameWrapper" - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_network_network_name" @@ -74,7 +80,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_networkserver_host" - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" tools:ignore="LabelFor"> <com.google.android.material.textfield.TextInputEditText @@ -90,7 +96,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_networkserver_port" - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" app:passwordToggleEnabled="true" tools:ignore="LabelFor"> diff --git a/app/src/main/res/layout/setup_user_channels.xml b/app/src/main/res/layout/setup_user_channels.xml index 2149436f0..bedf3c7d3 100644 --- a/app/src/main/res/layout/setup_user_channels.xml +++ b/app/src/main/res/layout/setup_user_channels.xml @@ -28,7 +28,7 @@ <com.google.android.material.textfield.TextInputLayout android:id="@+id/channelsWrapper" android:layout_width="match_parent" - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_height="wrap_content" android:hint="@string/label_channels" tools:ignore="LabelFor"> diff --git a/app/src/main/res/layout/setup_user_identity.xml b/app/src/main/res/layout/setup_user_identity.xml index 903898721..424d81f77 100644 --- a/app/src/main/res/layout/setup_user_identity.xml +++ b/app/src/main/res/layout/setup_user_identity.xml @@ -30,7 +30,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_identity_nick" - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" tools:ignore="LabelFor"> <com.google.android.material.textfield.TextInputEditText @@ -46,7 +46,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_identity_real_name" - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" app:passwordToggleEnabled="true" tools:ignore="LabelFor"> diff --git a/app/src/main/res/layout/setup_user_network.xml b/app/src/main/res/layout/setup_user_network.xml index f81841e36..5fae833a3 100644 --- a/app/src/main/res/layout/setup_user_network.xml +++ b/app/src/main/res/layout/setup_user_network.xml @@ -25,16 +25,19 @@ android:orientation="vertical" android:padding="32dp"> - <TextView - style="@style/Widget.CoreSettings.EditTextHeader" - android:text="@string/settings_network_title" /> - - <Spinner - android:id="@+id/network" - style="@style/Widget.FullWidthSpinner" + <de.kuschku.ui.spinner.MaterialSpinnerLayout + style="@style/Widget.CustomSpinnerLayout" android:layout_width="match_parent" android:layout_height="wrap_content" - tools:listitem="@layout/widget_spinner_item_inline" /> + android:hint="@string/settings_network_title"> + + <androidx.appcompat.widget.AppCompatSpinner + android:id="@+id/network" + style="@style/Widget.MaterialSpinner" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:listitem="@layout/widget_spinner_item_material" /> + </de.kuschku.ui.spinner.MaterialSpinnerLayout> <LinearLayout android:id="@+id/network_group" @@ -47,7 +50,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_network_network_name" - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" tools:ignore="LabelFor"> <com.google.android.material.textfield.TextInputEditText @@ -63,7 +66,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/settings_networkserver_host" - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" tools:ignore="LabelFor"> <com.google.android.material.textfield.TextInputEditText @@ -80,7 +83,7 @@ android:layout_height="wrap_content" android:hint="@string/settings_networkserver_port" app:passwordToggleEnabled="true" - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" tools:ignore="LabelFor"> <com.google.android.material.textfield.TextInputEditText diff --git a/app/src/main/res/layout/widget_material_spinner_label.xml b/app/src/main/res/layout/widget_material_spinner_label.xml new file mode 100644 index 000000000..5c6b555c9 --- /dev/null +++ b/app/src/main/res/layout/widget_material_spinner_label.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/spinner_label" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="start|top" + android:layout_marginStart="8dp" + android:layout_marginLeft="8dp" + android:letterSpacing="0.0333333333" + android:paddingLeft="4dp" + android:paddingRight="4dp" + android:textColor="@color/color_indicator" + android:textSize="12sp" + tools:ignore="UnusedAttribute" + tools:text="Name" /> diff --git a/app/src/main/res/layout/widget_quassel_setup_entry.xml b/app/src/main/res/layout/widget_quassel_setup_entry.xml index 00ce1d089..5fd97fb53 100644 --- a/app/src/main/res/layout/widget_quassel_setup_entry.xml +++ b/app/src/main/res/layout/widget_quassel_setup_entry.xml @@ -21,7 +21,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/wrapper" - style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + style="@style/Widget.CustomTextInput" android:layout_width="match_parent" android:layout_height="wrap_content" tools:ignore="LabelFor"> diff --git a/app/src/main/res/layout/widget_spinner_item_inline.xml b/app/src/main/res/layout/widget_spinner_item_material.xml similarity index 91% rename from app/src/main/res/layout/widget_spinner_item_inline.xml rename to app/src/main/res/layout/widget_spinner_item_material.xml index aaf0115f8..da0d69ec4 100644 --- a/app/src/main/res/layout/widget_spinner_item_inline.xml +++ b/app/src/main/res/layout/widget_spinner_item_material.xml @@ -20,12 +20,10 @@ <TextView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@android:id/text1" - style="@style/Widget.RtlConformTextView" + style="@style/Widget.MaterialSpinner.OutlinedBox" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_vertical" android:minHeight="?attr/listPreferredItemHeightSmall" - android:paddingLeft="16dp" - android:paddingRight="16dp" android:textAppearance="?android:attr/textAppearanceListItemSmall" tools:text="All Chats" /> diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index d5376574b..c57396b0b 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -58,6 +58,7 @@ <attr name="colorDivider" format="color" /> <attr name="colorBackground" format="color" /> + <attr name="colorBackgroundAlpha" format="color" /> <attr name="colorBackgroundHighlight" format="color" /> <attr name="colorBackgroundSecondary" format="color" /> <attr name="colorBackgroundCard" format="color" /> diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 605437ff6..f32b8bea2 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -47,4 +47,6 @@ <dimen name="drawer_toggle_size">24dp</dimen> <dimen name="drawer_toggle_thickness">2dp</dimen> + + <dimen name="hint_text_size">12sp</dimen> </resources> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 437744ba3..fd27e278d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -76,6 +76,7 @@ <string name="label_match_all">Matches all messages</string> <string name="label_mention">Mention</string> <string name="label_mention_long">Copy username into input line</string> + <string name="label_network">Network</string> <string name="label_new_account">New Account</string> <string name="label_new_chatlist">New Chatlist</string> <string name="label_new_highlight_ignore_rule">New Highlight Ignore Rule</string> diff --git a/app/src/main/res/values/strings_constants.xml b/app/src/main/res/values/strings_constants.xml index 92b980cb5..5b12ed27c 100644 --- a/app/src/main/res/values/strings_constants.xml +++ b/app/src/main/res/values/strings_constants.xml @@ -25,4 +25,5 @@ <string name="notification_channel_old_highlight" translatable="false">old_highlight</string> <string name="drag_intercept_bottom_sheet_behavior" translatable="false">de.kuschku.quasseldroid.util.ui.DragInterceptBottomSheetBehavior</string> + <string name="speedial_behavior" translatable="false">com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior</string> </resources> diff --git a/app/src/main/res/values/styles_widgets.xml b/app/src/main/res/values/styles_widgets.xml index b2f583a58..e7f21f7f8 100644 --- a/app/src/main/res/values/styles_widgets.xml +++ b/app/src/main/res/values/styles_widgets.xml @@ -18,7 +18,6 @@ --> <resources> - <style name="Widget" /> <style name="Widget.RtlConformTextView" parent="" /> @@ -111,6 +110,11 @@ <item name="android:background">?backgroundMenuItemRounded</item> </style> + <style name="Widget.MaterialSpinner" parent="Widget.AppCompat.Spinner"> + <item name="android:popupBackground">?colorBackground</item> + <item name="android:padding">0dip</item> + </style> + <style name="Widget.FullWidthSpinner" parent=""> <item name="android:layout_width">match_parent</item> <item name="android:layout_height">wrap_content</item> @@ -119,6 +123,18 @@ <item name="android:popupBackground">?colorBackground</item> </style> + <style name="Widget.CustomSpinnerLayout" parent="Widget.MaterialSpinnerLayout.OutlinedBox"> + <item name="android:textColorHint">?colorTextSecondary</item> + <item name="md_hintTextColor">?colorAccent</item> + <item name="md_boxStrokeColor">@color/color_outlined_stroke</item> + </style> + + <style name="Widget.CustomTextInput" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox"> + <item name="android:textColorHint">?colorTextSecondary</item> + <item name="hintTextColor">?colorAccent</item> + <item name="boxStrokeColor">@color/color_outlined_stroke</item> + </style> + <style name="Widget.CoreSettings.Wrapper" parent=""> <item name="android:layout_width">match_parent</item> <item name="android:layout_height">wrap_content</item> diff --git a/app/src/main/res/values/themes_amoled.xml b/app/src/main/res/values/themes_amoled.xml index ad82f60e1..92d3beafd 100644 --- a/app/src/main/res/values/themes_amoled.xml +++ b/app/src/main/res/values/themes_amoled.xml @@ -62,8 +62,9 @@ <item name="colorForegroundMirc">0x0</item> - <item name="colorBackground">#000000</item> <item name="android:windowBackground">@color/amoled_background</item> + <item name="colorBackground">#000000</item> + <item name="colorBackgroundAlpha">#aa000000</item> <item name="colorBackgroundHighlight">#40ffaf3b</item> <item name="colorBackgroundSecondary">#10ffaf3b</item> <item name="colorBackgroundCard">#000000</item> diff --git a/app/src/main/res/values/themes_dracula.xml b/app/src/main/res/values/themes_dracula.xml index 59b0adba4..344967a53 100644 --- a/app/src/main/res/values/themes_dracula.xml +++ b/app/src/main/res/values/themes_dracula.xml @@ -64,6 +64,7 @@ <item name="android:windowBackground">@color/dracula_dark_background</item> <item name="colorBackground">#282a36</item> + <item name="colorBackgroundAlpha">#aa282a36</item> <item name="colorBackgroundHighlight">#20ff79c6</item> <item name="colorBackgroundSecondary">#44475a</item> <item name="colorBackgroundCard">#44475a</item> diff --git a/app/src/main/res/values/themes_gruvbox.xml b/app/src/main/res/values/themes_gruvbox.xml index f107b8fb9..b2c65aa26 100644 --- a/app/src/main/res/values/themes_gruvbox.xml +++ b/app/src/main/res/values/themes_gruvbox.xml @@ -64,6 +64,7 @@ <item name="android:windowBackground">@color/gruvbox_light_background</item> <item name="colorBackground">#fbf1c7</item> + <item name="colorBackgroundAlpha">#aafbf1c7</item> <item name="colorBackgroundHighlight">#40d65d0e</item> <item name="colorBackgroundSecondary">#ebdbb2</item> <item name="colorBackgroundCard">#ebdbb2</item> @@ -128,6 +129,7 @@ <item name="android:windowBackground">@color/gruvbox_dark_background</item> <item name="colorBackground">#282828</item> + <item name="colorBackgroundAlpha">#aa282828</item> <item name="colorBackgroundHighlight">#40d65d0e</item> <item name="colorBackgroundSecondary">#504945</item> <item name="colorBackgroundCard">#504945</item> diff --git a/app/src/main/res/values/themes_material.xml b/app/src/main/res/values/themes_material.xml index 55511afe5..45dbed29b 100644 --- a/app/src/main/res/values/themes_material.xml +++ b/app/src/main/res/values/themes_material.xml @@ -58,6 +58,7 @@ <item name="android:windowBackground">@color/material_light_background</item> <item name="colorBackground">#FAFAFA</item> + <item name="colorBackgroundAlpha">#aaFAFAFA</item> <item name="colorBackgroundHighlight">#40ffaf3b</item> <item name="colorBackgroundSecondary">#F0F0F0</item> <item name="colorBackgroundCard">#FFFFFF</item> @@ -113,6 +114,7 @@ <item name="android:windowBackground">@color/material_dark_background</item> <item name="colorBackground">#303030</item> + <item name="colorBackgroundAlpha">#aa303030</item> <item name="colorBackgroundHighlight">#40ffaf3b</item> <item name="colorBackgroundSecondary">#424242</item> <item name="colorBackgroundCard">#424242</item> diff --git a/app/src/main/res/values/themes_quassel.xml b/app/src/main/res/values/themes_quassel.xml index 2fb569c03..7228a1ca8 100644 --- a/app/src/main/res/values/themes_quassel.xml +++ b/app/src/main/res/values/themes_quassel.xml @@ -60,6 +60,7 @@ <item name="android:windowBackground">@color/quassel_light_background</item> <item name="colorBackground">#FAFAFA</item> + <item name="colorBackgroundAlpha">#aaFAFAFA</item> <item name="colorBackgroundHighlight">#ffaf3b</item> <item name="colorBackgroundSecondary">#F0F0F0</item> <item name="colorBackgroundCard">#FFFFFF</item> @@ -117,6 +118,7 @@ <item name="android:windowBackground">@color/quassel_dark_background</item> <item name="colorBackground">#303030</item> + <item name="colorBackgroundAlpha">#aa303030</item> <item name="colorBackgroundHighlight">#ffaf3b</item> <item name="colorBackgroundSecondary">#424242</item> <item name="colorBackgroundCard">#424242</item> diff --git a/app/src/main/res/values/themes_solarized.xml b/app/src/main/res/values/themes_solarized.xml index 33383cc39..33b53c9a4 100644 --- a/app/src/main/res/values/themes_solarized.xml +++ b/app/src/main/res/values/themes_solarized.xml @@ -64,6 +64,7 @@ <item name="android:windowBackground">@color/solarized_light_background</item> <item name="colorBackground">#FDF6E3</item> + <item name="colorBackgroundAlpha">#aaFDF6E3</item> <item name="colorBackgroundHighlight">#40B58900</item> <item name="colorBackgroundSecondary">#EEE8D5</item> <item name="colorBackgroundCard">#EEE8D5</item> @@ -128,6 +129,7 @@ <item name="android:windowBackground">@color/solarized_dark_background</item> <item name="colorBackground">#002B36</item> + <item name="colorBackgroundAlpha">#aa002B36</item> <item name="colorBackgroundHighlight">#30268BD2</item> <item name="colorBackgroundSecondary">#073642</item> <item name="colorBackgroundCard">#073642</item> diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 5168d9b00..bb28d6f25 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -178,7 +178,7 @@ android:title="@string/preference_show_prefix_title" /> <SwitchPreference - android:defaultValue="false" + android:defaultValue="true" android:key="@string/preference_square_avatars_key" android:title="@string/preference_square_avatars_title" /> diff --git a/gradle.properties b/gradle.properties index 9b93d673b..9450fb27e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -33,9 +33,9 @@ android.enableD8=true # Enable new Android R8 Optimizer android.enableR8=true # Enable gradle build cache -org.gradle.caching=false +org.gradle.caching=true # Enable android build cache -android.enableBuildCache=false +android.enableBuildCache=true # Enable AndroidX android.useAndroidX=true android.enableJetifier=true diff --git a/settings.gradle b/settings.gradle index da7acbdfd..521e58970 100644 --- a/settings.gradle +++ b/settings.gradle @@ -26,5 +26,6 @@ include ':app', ':lib', ':lifecycle-ktx', ':malheur', - ":persistence", - ":viewmodel" + ':persistence', + ':viewmodel', + ':ui_spinner' diff --git a/ui_spinner/build.gradle.kts b/ui_spinner/build.gradle.kts new file mode 100644 index 000000000..ab7c81262 --- /dev/null +++ b/ui_spinner/build.gradle.kts @@ -0,0 +1,49 @@ +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2018 Janne Koschinski + * Copyright (c) 2018 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +plugins { + id("com.android.library") + kotlin("android") +} + +android { + compileSdkVersion(28) + + defaultConfig { + minSdkVersion(16) + targetSdkVersion(28) + + consumerProguardFiles("proguard-rules.pro") + + // Disable test runner analytics + testInstrumentationRunnerArguments = mapOf( + "disableAnalytics" to "true" + ) + } + + lintOptions { + isWarningsAsErrors = true + setLintConfig(file("../lint.xml")) + } +} + +dependencies { + implementation(kotlin("stdlib", "1.3.21")) + implementation("androidx.appcompat", "appcompat", "1.0.0") +} diff --git a/ui_spinner/proguard-rules.pro b/ui_spinner/proguard-rules.pro new file mode 100644 index 000000000..8c9db7a6d --- /dev/null +++ b/ui_spinner/proguard-rules.pro @@ -0,0 +1,21 @@ +# Gson uses generic type information stored in a class file when working with fields. Proguard +# removes such information by default, so configure it to keep all of it. +-keepattributes Signature + +# For using GSON @Expose annotation +-keepattributes *Annotation* + +# Gson specific classes +-dontwarn sun.misc.** +#-keep class com.google.gson.stream.** { *; } + +# Application classes that will be serialized/deserialized over Gson +-keep class com.google.gson.examples.android.model.** { *; } + +# Prevent proguard from stripping interface information from TypeAdapterFactory, +# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) +-keep class * implements com.google.gson.TypeAdapterFactory +-keep class * implements com.google.gson.JsonSerializer +-keep class * implements com.google.gson.JsonDeserializer + +-keep class **.BuildConfig { *; } diff --git a/ui_spinner/src/main/AndroidManifest.xml b/ui_spinner/src/main/AndroidManifest.xml new file mode 100644 index 000000000..6d39b721b --- /dev/null +++ b/ui_spinner/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + Quasseldroid - Quassel client for Android + + Copyright (c) 2019 Janne Koschinski + Copyright (c) 2019 The Quassel Project + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU General Public License version 3 as published + by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program. If not, see <http://www.gnu.org/licenses/>. + --> + +<manifest package="de.kuschku.ui.spinner" /> diff --git a/ui_spinner/src/main/java/de/kuschku/ui/animation/AnimationUtils.kt b/ui_spinner/src/main/java/de/kuschku/ui/animation/AnimationUtils.kt new file mode 100644 index 000000000..7186061c1 --- /dev/null +++ b/ui_spinner/src/main/java/de/kuschku/ui/animation/AnimationUtils.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2015 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 de.kuschku.ui.animation + +import android.view.animation.LinearInterpolator +import androidx.interpolator.view.animation.LinearOutSlowInInterpolator + +/** + * Utility class for animations containing Material interpolators. + */ +object AnimationUtils { + val LINEAR_INTERPOLATOR = LinearInterpolator() + val LINEAR_OUT_SLOW_IN_INTERPOLATOR = LinearOutSlowInInterpolator() +} diff --git a/ui_spinner/src/main/java/de/kuschku/ui/animation/AnimatorSetCompat.java b/ui_spinner/src/main/java/de/kuschku/ui/animation/AnimatorSetCompat.java new file mode 100644 index 000000000..93116c403 --- /dev/null +++ b/ui_spinner/src/main/java/de/kuschku/ui/animation/AnimatorSetCompat.java @@ -0,0 +1,46 @@ +/* + * Copyright 2017 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 de.kuschku.ui.animation; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ValueAnimator; + +import java.util.List; + +/** + * Compatibility utils for {@link android.animation.AnimatorSet}. + */ +public class AnimatorSetCompat { + + /** + * Sets up this AnimatorSet to play all of the supplied animations at the same time. + */ + public static void playTogether(AnimatorSet animatorSet, List<Animator> items) { + // Fix for pre-M bug where animators with start delay are not played correctly in an + // AnimatorSet. + long totalDuration = 0; + for (int i = 0, count = items.size(); i < count; i++) { + Animator animator = items.get(i); + totalDuration = Math.max(totalDuration, animator.getStartDelay() + animator.getDuration()); + } + Animator fix = ValueAnimator.ofInt(0, 0); + fix.setDuration(totalDuration); + items.add(0, fix); + + animatorSet.playTogether(items); + } +} diff --git a/ui_spinner/src/main/java/de/kuschku/ui/color/MaterialColors.kt b/ui_spinner/src/main/java/de/kuschku/ui/color/MaterialColors.kt new file mode 100644 index 000000000..cfeeabbd6 --- /dev/null +++ b/ui_spinner/src/main/java/de/kuschku/ui/color/MaterialColors.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2018 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 de.kuschku.ui.color + +import android.content.Context +import android.graphics.Color +import android.util.TypedValue +import android.view.View + +import de.kuschku.ui.resources.MaterialAttributes + +import androidx.annotation.AttrRes +import androidx.annotation.ColorInt +import androidx.annotation.FloatRange +import androidx.core.graphics.ColorUtils + +/** + * A utility class for common color variants used in Material themes. + */ +object MaterialColors { + /** + * Returns the color int for the provided theme color attribute, or the default value if the + * attribute is not set in the current theme, using the `view`'s [Context]. + */ + @ColorInt + fun getColor( + view: View, @AttrRes colorAttributeResId: Int, @ColorInt defaultValue: Int): Int { + return getColor(view.context, colorAttributeResId, defaultValue) + } + + /** + * Returns the color int for the provided theme color attribute, or the default value if the + * attribute is not set in the current theme. + */ + @ColorInt + private fun getColor( + context: Context, @AttrRes colorAttributeResId: Int, @ColorInt defaultValue: Int): Int { + val typedValue = MaterialAttributes.resolveAttribute(context, colorAttributeResId) + return typedValue?.data ?: defaultValue + } + + /** + * Calculates a color that represents the layering of the `overlayColor` on top of the + * `backgroundColor`. + */ + @ColorInt + fun layer(@ColorInt backgroundColor: Int, @ColorInt overlayColor: Int): Int { + return ColorUtils.compositeColors(overlayColor, backgroundColor) + } +} diff --git a/ui_spinner/src/main/java/de/kuschku/ui/internal/CollapsingTextHelper.java b/ui_spinner/src/main/java/de/kuschku/ui/internal/CollapsingTextHelper.java new file mode 100644 index 000000000..5c9314f0d --- /dev/null +++ b/ui_spinner/src/main/java/de/kuschku/ui/internal/CollapsingTextHelper.java @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2015 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 de.kuschku.ui.internal; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.text.TextPaint; +import android.text.TextUtils; +import android.view.Gravity; +import android.view.View; + +import de.kuschku.ui.resources.MaterialResources; + +import androidx.annotation.ColorInt; +import androidx.annotation.Nullable; +import androidx.core.math.MathUtils; +import androidx.core.text.TextDirectionHeuristicsCompat; +import androidx.core.view.GravityCompat; +import androidx.core.view.ViewCompat; +import de.kuschku.ui.spinner.R; + +/** + * Helper class for rendering and animating collapsed text. + */ +public final class CollapsingTextHelper { + private final View view; + private final Rect collapsedBounds; + private final RectF currentBounds; + private final TextPaint textPaint; + private final TextPaint tmpPaint; + private boolean drawTitle; + private int collapsedTextGravity = Gravity.CENTER_VERTICAL; + private float collapsedTextSize = 15; + private ColorStateList collapsedTextColor; + private float collapsedDrawY; + private float collapsedDrawX; + private float currentDrawX; + private float currentDrawY; + private Typeface collapsedTypeface; + private CharSequence text; + private CharSequence textToDraw; + private boolean isRtl; + private int[] state; + private boolean boundsChanged; + + public CollapsingTextHelper(View view) { + this.view = view; + + textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.SUBPIXEL_TEXT_FLAG); + tmpPaint = new TextPaint(textPaint); + + collapsedBounds = new Rect(); + currentBounds = new RectF(); + } + + private static boolean rectEquals(Rect r, int left, int top, int right, int bottom) { + return !(r.left != left || r.top != top || r.right != right || r.bottom != bottom); + } + + private void setCollapsedBounds(int left, int top, int right, int bottom) { + if (!rectEquals(collapsedBounds, left, top, right, bottom)) { + collapsedBounds.set(left, top, right, bottom); + boundsChanged = true; + onBoundsChanged(); + } + } + + public void setCollapsedBounds(Rect bounds) { + setCollapsedBounds(bounds.left, bounds.top, bounds.right, bounds.bottom); + } + + private float calculateCollapsedTextWidth() { + if (text == null) { + return 0; + } + getTextPaintCollapsed(tmpPaint); + return tmpPaint.measureText(text, 0, text.length()); + } + + public float getCollapsedTextHeight() { + getTextPaintCollapsed(tmpPaint); + // Return collapsed height measured from the baseline. + return -tmpPaint.ascent(); + } + + public void getCollapsedTextActualBounds(RectF bounds) { + boolean isRtl = calculateIsRtl(text); + + bounds.left = + !isRtl ? collapsedBounds.left : collapsedBounds.right - calculateCollapsedTextWidth(); + bounds.top = collapsedBounds.top; + bounds.right = !isRtl ? bounds.left + calculateCollapsedTextWidth() : collapsedBounds.right; + bounds.bottom = collapsedBounds.top + getCollapsedTextHeight(); + } + + private void getTextPaintCollapsed(TextPaint textPaint) { + textPaint.setTextSize(collapsedTextSize); + textPaint.setTypeface(collapsedTypeface); + } + + private void onBoundsChanged() { + drawTitle = + collapsedBounds.width() > 0 + && collapsedBounds.height() > 0; + } + + public void setCollapsedTextGravity(int gravity) { + if (collapsedTextGravity != gravity) { + collapsedTextGravity = gravity; + recalculate(); + } + } + + public void setCollapsedTextAppearance(int resId) { + Context context = view.getContext(); + TypedArray a = context.obtainStyledAttributes(resId, R.styleable.TextAppearance); + ColorStateList textColor = MaterialResources.getColorStateList(context, a, R.styleable.TextAppearance_android_textColor); + if (textColor != null) { + collapsedTextColor = textColor; + } + float textSize = a.getDimension(R.styleable.TextAppearance_android_textSize, 0f); + if (textSize != 0) { + collapsedTextSize = textSize; + } + a.recycle(); + + + recalculate(); + } + + public void setTypefaces(Typeface typeface) { + boolean collapsedFontChanged = setCollapsedTypefaceInternal(typeface); + if (collapsedFontChanged) { + recalculate(); + } + } + + @SuppressWarnings("ReferenceEquality") // Matches the Typeface comparison in TextView + private boolean setCollapsedTypefaceInternal(Typeface typeface) { + // Explicit Typeface setting cancels pending async fetch, if any, to avoid old font overriding + // already updated one when async op comes back after a while. + if (collapsedTypeface != typeface) { + collapsedTypeface = typeface; + return true; + } + return false; + } + + public final boolean setState(final int[] state) { + this.state = state; + + if (isStateful()) { + recalculate(); + return true; + } + + return false; + } + + private boolean isStateful() { + return (collapsedTextColor != null && collapsedTextColor.isStateful()); + } + + private void calculateOffsets() { + currentBounds.left = collapsedBounds.left; + currentBounds.top = collapsedDrawY; + currentBounds.right = collapsedBounds.right; + currentBounds.bottom = collapsedBounds.bottom; + currentDrawX = collapsedDrawX; + currentDrawY = collapsedDrawY; + + setInterpolatedTextSize(); + + textPaint.setColor(getCurrentCollapsedTextColor()); + + ViewCompat.postInvalidateOnAnimation(view); + } + + @ColorInt + public int getCurrentCollapsedTextColor() { + return getCurrentColor(collapsedTextColor); + } + + @ColorInt + private int getCurrentColor(@Nullable ColorStateList colorStateList) { + if (colorStateList == null) { + return 0; + } + if (state != null) { + return colorStateList.getColorForState(state, 0); + } + return colorStateList.getDefaultColor(); + } + + private void calculateBaseOffsets() { + // We then calculate the collapsed text size, using the same logic + calculateUsingTextSize(); + float width = + textToDraw != null ? textPaint.measureText(textToDraw, 0, textToDraw.length()) : 0; + final int collapsedAbsGravity = + GravityCompat.getAbsoluteGravity( + collapsedTextGravity, + isRtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR); + switch (collapsedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) { + case Gravity.BOTTOM: + collapsedDrawY = collapsedBounds.bottom; + break; + case Gravity.TOP: + collapsedDrawY = collapsedBounds.top - textPaint.ascent(); + break; + case Gravity.CENTER_VERTICAL: + default: + float textHeight = textPaint.descent() - textPaint.ascent(); + float textOffset = (textHeight / 2) - textPaint.descent(); + collapsedDrawY = collapsedBounds.centerY() + textOffset; + break; + } + switch (collapsedAbsGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK) { + case Gravity.CENTER_HORIZONTAL: + collapsedDrawX = collapsedBounds.centerX() - (width / 2); + break; + case Gravity.RIGHT: + collapsedDrawX = collapsedBounds.right - width; + break; + case Gravity.LEFT: + default: + collapsedDrawX = collapsedBounds.left; + break; + } + } + + public void draw(Canvas canvas) { + final int saveCount = canvas.save(); + + if (textToDraw != null && drawTitle) { + float x = currentDrawX; + float y = currentDrawY; + + canvas.drawText(textToDraw, 0, textToDraw.length(), x, y, textPaint); + } + + canvas.restoreToCount(saveCount); + } + + private boolean calculateIsRtl(CharSequence text) { + final boolean defaultIsRtl = + ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_RTL; + return (defaultIsRtl + ? TextDirectionHeuristicsCompat.FIRSTSTRONG_RTL + : TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR) + .isRtl(text, 0, text.length()); + } + + private void setInterpolatedTextSize() { + calculateUsingTextSize(); + ViewCompat.postInvalidateOnAnimation(view); + } + + @SuppressWarnings("ReferenceEquality") // Matches the Typeface comparison in TextView + private void calculateUsingTextSize() { + if (text == null) { + return; + } + + final float collapsedWidth = collapsedBounds.width(); + + boolean updateDrawText = false; + + final float availableWidth; + availableWidth = collapsedWidth; + + if (availableWidth > 0) { + updateDrawText = boundsChanged; + boundsChanged = false; + } + + if (textToDraw == null || updateDrawText) { + textPaint.setTextSize(collapsedTextSize); + textPaint.setTypeface(collapsedTypeface); + + // If we don't currently have text to draw, or the text size has changed, ellipsize... + final CharSequence title = + TextUtils.ellipsize(text, textPaint, availableWidth, TextUtils.TruncateAt.END); + if (!TextUtils.equals(title, textToDraw)) { + textToDraw = title; + isRtl = calculateIsRtl(textToDraw); + } + } + } + + public void recalculate() { + if (view.getHeight() > 0 && view.getWidth() > 0) { + // If we've already been laid out, calculate everything now otherwise we'll wait + // until a layout + calculateBaseOffsets(); + calculateOffsets(); + } + } + + /** + * Set the title to display + */ + public void setText(CharSequence text) { + if (text == null || !TextUtils.equals(this.text, text)) { + this.text = text; + textToDraw = null; + recalculate(); + } + } + + public ColorStateList getCollapsedTextColor() { + return collapsedTextColor; + } + + public void setCollapsedTextColor(ColorStateList textColor) { + if (collapsedTextColor != textColor) { + collapsedTextColor = textColor; + recalculate(); + } + } +} diff --git a/ui_spinner/src/main/java/de/kuschku/ui/internal/DescendantOffsetUtils.java b/ui_spinner/src/main/java/de/kuschku/ui/internal/DescendantOffsetUtils.java new file mode 100644 index 000000000..68e7de829 --- /dev/null +++ b/ui_spinner/src/main/java/de/kuschku/ui/internal/DescendantOffsetUtils.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2015 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 de.kuschku.ui.internal; + +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; + +/** + * Utility class for descendant {@link Rect} calculations. + */ +public class DescendantOffsetUtils { + private static final ThreadLocal<Matrix> matrix = new ThreadLocal<>(); + private static final ThreadLocal<RectF> rectF = new ThreadLocal<>(); + + /** + * This is a port of the common {@link ViewGroup#offsetDescendantRectToMyCoords(View, Rect)} from + * the framework, but adapted to take transformations into account. The result will be the + * bounding rect of the real transformed rect. + * + * @param descendant view defining the original coordinate system of rect + * @param rect (in/out) the rect to offset from descendant to this view's coordinate system + */ + private static void offsetDescendantRect(ViewGroup parent, View descendant, Rect rect) { + Matrix m = matrix.get(); + if (m == null) { + m = new Matrix(); + matrix.set(m); + } else { + m.reset(); + } + + offsetDescendantMatrix(parent, descendant, m); + + RectF rectF = DescendantOffsetUtils.rectF.get(); + if (rectF == null) { + rectF = new RectF(); + DescendantOffsetUtils.rectF.set(rectF); + } + rectF.set(rect); + m.mapRect(rectF); + rect.set( + (int) (rectF.left + 0.5f), + (int) (rectF.top + 0.5f), + (int) (rectF.right + 0.5f), + (int) (rectF.bottom + 0.5f)); + } + + /** + * Retrieve the transformed bounding rect of an arbitrary descendant view. This does not need to + * be a direct child. + * + * @param descendant descendant view to reference + * @param out rect to set to the bounds of the descendant view + */ + public static void getDescendantRect(ViewGroup parent, View descendant, Rect out) { + out.set(0, 0, descendant.getWidth(), descendant.getHeight()); + offsetDescendantRect(parent, descendant, out); + } + + private static void offsetDescendantMatrix(ViewParent target, View view, Matrix m) { + final ViewParent parent = view.getParent(); + if (parent instanceof View && parent != target) { + final View vp = (View) parent; + offsetDescendantMatrix(target, vp, m); + m.preTranslate(-vp.getScrollX(), -vp.getScrollY()); + } + + m.preTranslate(view.getLeft(), view.getTop()); + + if (!view.getMatrix().isIdentity()) { + m.preConcat(view.getMatrix()); + } + } +} diff --git a/ui_spinner/src/main/java/de/kuschku/ui/internal/ThemeEnforcement.java b/ui_spinner/src/main/java/de/kuschku/ui/internal/ThemeEnforcement.java new file mode 100644 index 000000000..dcf1882fe --- /dev/null +++ b/ui_spinner/src/main/java/de/kuschku/ui/internal/ThemeEnforcement.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2017 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 de.kuschku.ui.internal; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; + +import androidx.annotation.AttrRes; +import androidx.annotation.StyleRes; +import androidx.appcompat.view.ContextThemeWrapper; +import de.kuschku.ui.spinner.R; + +/** + * Utility methods to check Theme compatibility with components. + */ +public final class ThemeEnforcement { + + private static final int[] ANDROID_THEME_OVERLAY_ATTRS = + new int[]{android.R.attr.theme, R.attr.theme}; + private static final int[] MATERIAL_THEME_OVERLAY_ATTR = new int[]{R.attr.materialThemeOverlay}; + + private ThemeEnforcement() { + } + + + /** + * Uses the materialThemeOverlay attribute to create a themed context. This allows us to use + * ThemeOverlays with a default style, and gives us some protection against losing our + * ThemeOverlay by clients who set android:theme or app:theme. If android:theme or app:theme is + * specified by the client, any attributes defined there will take precedence over attributes + * defined in materialThemeOverlay. + */ + public static Context createThemedContext( + Context context, AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { + int materialThemeOverlayId = + obtainMaterialThemeOverlayId(context, attrs, defStyleAttr, defStyleRes); + if (materialThemeOverlayId != 0 + && (!(context instanceof ContextThemeWrapper) + || ((ContextThemeWrapper) context).getThemeResId() != materialThemeOverlayId)) { + // If the context isn't a ContextThemeWrapper, or it is but does not have the same theme as we + // need, wrap it in a new wrapper. + context = new ContextThemeWrapper(context, materialThemeOverlayId); + + // We want values set in android:theme or app:theme to always override values supplied by + // materialThemeOverlay, so we'll wrap the context again if either of those are set. + int androidThemeOverlayId = obtainAndroidThemeOverlayId(context, attrs); + if (androidThemeOverlayId != 0) { + context = new ContextThemeWrapper(context, androidThemeOverlayId); + } + } + return context; + } + + /** + * Retrieves the value of {@code android:theme} or {@code app:theme}, not taking into account + * {@code defStyleAttr} and {@code defStyleRes} because the Android theme overlays shouldn't work + * from default styles. + */ + @StyleRes + private static int obtainAndroidThemeOverlayId(Context context, AttributeSet attrs) { + TypedArray a = context.obtainStyledAttributes(attrs, ANDROID_THEME_OVERLAY_ATTRS); + int androidThemeId = a.getResourceId(0 /* index */, 0 /* defaultVal */); + int appThemeId = a.getResourceId(1 /* index */, 0 /* defaultVal */); + a.recycle(); + if (androidThemeId != 0) { + return androidThemeId; + } else { + return appThemeId; + } + } + + /** + * Retrieves the value of {@code materialThemeOverlay}, taking into account {@code defStyleAttr} + * and {@code defStyleRes} because the Material theme overlay should work from default styles. + */ + @StyleRes + private static int obtainMaterialThemeOverlayId( + Context context, AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { + TypedArray a = + context.obtainStyledAttributes( + attrs, MATERIAL_THEME_OVERLAY_ATTR, defStyleAttr, defStyleRes); + int materialThemeOverlayId = a.getResourceId(0 /* index */, 0 /* defaultVal */); + a.recycle(); + return materialThemeOverlayId; + } +} diff --git a/ui_spinner/src/main/java/de/kuschku/ui/resources/MaterialAttributes.java b/ui_spinner/src/main/java/de/kuschku/ui/resources/MaterialAttributes.java new file mode 100644 index 000000000..1ae84762d --- /dev/null +++ b/ui_spinner/src/main/java/de/kuschku/ui/resources/MaterialAttributes.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018 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 de.kuschku.ui.resources; + +import android.content.Context; +import android.util.TypedValue; + +import androidx.annotation.AttrRes; +import androidx.annotation.Nullable; + +/** + * Utility methods to work with attributes. + */ +public class MaterialAttributes { + /** + * Returns the {@link TypedValue} for the provided {@code attributeResId}. + */ + @Nullable + public static TypedValue resolveAttribute(Context context, @AttrRes int attributeResId) { + TypedValue typedValue = new TypedValue(); + if (context.getTheme().resolveAttribute(attributeResId, typedValue, true)) { + return typedValue; + } + return null; + } +} diff --git a/ui_spinner/src/main/java/de/kuschku/ui/resources/MaterialResources.java b/ui_spinner/src/main/java/de/kuschku/ui/resources/MaterialResources.java new file mode 100644 index 000000000..485d7f6ff --- /dev/null +++ b/ui_spinner/src/main/java/de/kuschku/ui/resources/MaterialResources.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2017 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 de.kuschku.ui.resources; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; + +import androidx.annotation.Nullable; +import androidx.annotation.StyleableRes; +import androidx.appcompat.content.res.AppCompatResources; + +/** + * Utility methods to resolve resources for components. + */ +public class MaterialResources { + + private MaterialResources() { + } + + /** + * Returns the {@link ColorStateList} from the given {@link TypedArray} attributes. The resource + * can include themeable attributes, regardless of API level. + */ + @Nullable + public static ColorStateList getColorStateList( + Context context, TypedArray attributes, @StyleableRes int index) { + if (attributes.hasValue(index)) { + int resourceId = attributes.getResourceId(index, 0); + if (resourceId != 0) { + ColorStateList value = AppCompatResources.getColorStateList(context, resourceId); + if (value != null) { + return value; + } + } + } + + // Reading a single color with getColorStateList() on API 15 and below doesn't always correctly + // read the value. Instead we'll first try to read the color directly here. + + return attributes.getColorStateList(index); + } + + /** + * Returns the @StyleableRes index that contains value in the attributes array. If both indices + * contain values, the first given index takes precedence and is returned. + */ + @StyleableRes + static int getIndexWithValue(TypedArray attributes, @StyleableRes int a, @StyleableRes int b) { + if (attributes.hasValue(a)) { + return a; + } + return b; + } +} diff --git a/ui_spinner/src/main/java/de/kuschku/ui/shape/CornerFamily.java b/ui_spinner/src/main/java/de/kuschku/ui/shape/CornerFamily.java new file mode 100644 index 000000000..e679b5113 --- /dev/null +++ b/ui_spinner/src/main/java/de/kuschku/ui/shape/CornerFamily.java @@ -0,0 +1,43 @@ +/* + * Copyright 2018 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 + * + * https://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 de.kuschku.ui.shape; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import androidx.annotation.IntDef; + +/** + * CornerFamily enum that holds which family to be used to create a {@link CornerTreatment} + * + * <p>The corner family determines which family to use to create a {@link CornerTreatment}. Setting + * the CornerFamily to {@link CornerFamily#ROUNDED} sets the corner treatment to {@link + * RoundedCornerTreatment}, and setting the CornerFamily to {@link CornerFamily#CUT} sets the corner + * treatment to a {@link CutCornerTreatment}. + */ +@IntDef({CornerFamily.ROUNDED, CornerFamily.CUT}) +@Retention(RetentionPolicy.SOURCE) +public @interface CornerFamily { + /** + * Corresponds to a {@link RoundedCornerTreatment}. + */ + int ROUNDED = 0; + /** + * Corresponds to a {@link CutCornerTreatment}. + */ + int CUT = 1; +} diff --git a/ui_spinner/src/main/java/de/kuschku/ui/shape/CornerTreatment.java b/ui_spinner/src/main/java/de/kuschku/ui/shape/CornerTreatment.java new file mode 100644 index 000000000..6e5f971d0 --- /dev/null +++ b/ui_spinner/src/main/java/de/kuschku/ui/shape/CornerTreatment.java @@ -0,0 +1,77 @@ +/* + * Copyright 2017 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 de.kuschku.ui.shape; + +/** + * A basic corner treatment (a single point which does not affect the shape). + * + * <p>Note: For corner treatments which result in a concave shape, the parent view must disable + * clipping of children by calling {@link android.view.ViewGroup#setClipChildren(boolean)}, or by + * setting `android:clipChildren="false"` in xml. `clipToPadding` may also need to be false if there + * is any padding on the parent that could intersect the shadow. + */ +public class CornerTreatment implements Cloneable { + + float cornerSize; + + CornerTreatment() { + // Default Constructor has no size. Using this treatment for all corners will draw a square + this.cornerSize = 0; + } + + CornerTreatment(float cornerSize) { + // Most CornerTreatments have a concept of corner size. This constructor is exposed for + // extending classes. + this.cornerSize = cornerSize; + } + + /** + * Generates a {@link ShapePath} for this corner treatment. + * + * <p>CornerTreatments are assumed to have an origin of (0, 0) (i.e. they represent the top-left + * corner), and are automatically rotated and scaled as necessary when applied to other corners. + * + * @param angle the angle of the corner, typically 90 degrees. + * @param interpolation the interpolation of the corner treatment. Ranges between 0 (none) and 1 + * (fully) interpolated. Custom corner treatments can implement interpolation to support shape + * transition between two arbitrary states. Typically, a value of 0 indicates that the custom + * corner treatment is not rendered (i.e. that it is a 90 degree angle), and a value of 1 + * indicates that the treatment is fully rendered. Animation between these two values can + * "heal" or "reveal" a corner treatment. + * @param shapePath the {@link ShapePath} that this treatment should write to. + */ + public void getCornerPath(float angle, float interpolation, ShapePath shapePath) { + } + + public float getCornerSize() { + return cornerSize; + } + + public void setCornerSize(float cornerSize) { + this.cornerSize = cornerSize; + } + + @Override + public CornerTreatment clone() { + try { + return (CornerTreatment) super.clone(); + } catch (CloneNotSupportedException e) { + throw new AssertionError(e); // This should never happen, because CornerTreatment handles the + // cloning, so all subclasses of CornerTreatment will support cloning. + } + } +} diff --git a/ui_spinner/src/main/java/de/kuschku/ui/shape/CutCornerTreatment.java b/ui_spinner/src/main/java/de/kuschku/ui/shape/CutCornerTreatment.java new file mode 100644 index 000000000..2a48ca9d0 --- /dev/null +++ b/ui_spinner/src/main/java/de/kuschku/ui/shape/CutCornerTreatment.java @@ -0,0 +1,47 @@ +/* + * Copyright 2017 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 de.kuschku.ui.shape; + +/** + * A corner treatment which cuts or clips the original corner of a shape with a straight line. + */ +public class CutCornerTreatment extends CornerTreatment implements Cloneable { + + /** + * Instantiates a cut corner treatment of a given size. A cut corner treatment introduces two new + * corners to a shape, produced by a straight line drawn between two points {@param size} pixels + * away, on the vertical and horizontal axes, from the rectilinear (original) corner of the shape. + * Stated another way, if the rectilinear (original) corner of the shape was at co-ordinates (0, + * 0), the new corners are at co-ordinates (size, 0) and (0, size), and a straight line is drawn + * between them. + * + * @param size the length in pixels that the new corners will be drawn away from the origin. + */ + public CutCornerTreatment(float size) { + super(size); + } + + @Override + public void getCornerPath(float angle, float interpolation, ShapePath shapePath) { + shapePath.reset(0, cornerSize * interpolation); + shapePath.lineTo( + (float) (Math.sin(Math.toRadians(angle)) * cornerSize * interpolation), + // Something about using cos() is causing rounding which prevents the path from being convex + // on api levels 21 and 22. Using sin() with 90 - angle is helping for now. + (float) (Math.sin(Math.toRadians(90 - angle)) * cornerSize * interpolation)); + } +} diff --git a/ui_spinner/src/main/java/de/kuschku/ui/shape/EdgeTreatment.java b/ui_spinner/src/main/java/de/kuschku/ui/shape/EdgeTreatment.java new file mode 100644 index 000000000..91edd7e13 --- /dev/null +++ b/ui_spinner/src/main/java/de/kuschku/ui/shape/EdgeTreatment.java @@ -0,0 +1,73 @@ +/* + * Copyright 2017 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 de.kuschku.ui.shape; + +/** + * A basic edge treatment (a single straight line). Sub-classed for custom edge treatments. + * + * <p>Note: For edge treatments which result in a concave shape, the parent view must disable + * clipping of children by calling {@link android.view.ViewGroup#setClipChildren(boolean)}, or by + * setting `android:clipChildren="false"` in xml. `clipToPadding` may also need to be false if there + * is any padding on the parent that could intersect the shadow. + */ +public class EdgeTreatment implements Cloneable { + + /** + * @deprecated Does not support interpolation. Use {@link #getEdgePath(float, float, float, + * ShapePath)} instead. + */ + @Deprecated + public void getEdgePath(float length, float interpolation, ShapePath shapePath) { + // Best guess at center since it could be offset by corners of different size. + float center = length / 2f; + getEdgePath(length, center, interpolation, shapePath); + } + + /** + * Generates a {@link ShapePath} for this edge treatment. + * + * <p>EdgeTreatments have an origin of (0, 0) and a destination of (0, length) (i.e. they + * represent the top edge), and are automatically rotated and scaled as necessary when applied to + * other edges. Only the horizontal, top EdgeTreatment needs to be defined in order to apply it to + * all four edges. + * + * @param length the length of the edge. + * @param center the distance to the center of the edge. This takes into account any offset added + * by the proceeding corner. Drawing anything at (center, 0) will be center aligned with the + * shape. Normally you'll want to use this instead of length / 2. + * @param interpolation the interpolation of the edge treatment. Ranges between 0 (none) and 1 + * (fully) interpolated. Custom edge treatments can implement interpolation to support shape + * transition between two arbitrary states. Typically, a value of 0 indicates that the custom + * edge treatment is not rendered (i.e. that it is a straight line), and a value of 1 + * indicates that the treatment is fully rendered. Animation between these two values can + * "heal" or "reveal" an edge treatment. + * @param shapePath the {@link ShapePath} that this treatment should write to. + */ + public void getEdgePath(float length, float center, float interpolation, ShapePath shapePath) { + shapePath.lineTo(length, 0); + } + + @Override + public EdgeTreatment clone() { + try { + return (EdgeTreatment) super.clone(); + } catch (CloneNotSupportedException e) { + throw new AssertionError(e); // This should never happen, because EdgeTreatment handles the + // cloning, so all subclasses of EdgeTreatment will support cloning. + } + } +} diff --git a/ui_spinner/src/main/java/de/kuschku/ui/shape/MaterialShapeDrawable.java b/ui_spinner/src/main/java/de/kuschku/ui/shape/MaterialShapeDrawable.java new file mode 100644 index 000000000..fe95b9534 --- /dev/null +++ b/ui_spinner/src/main/java/de/kuschku/ui/shape/MaterialShapeDrawable.java @@ -0,0 +1,551 @@ +/* + * Copyright 2017 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 de.kuschku.ui.shape; + +import android.content.res.ColorStateList; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.Path; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff; +import android.graphics.PorterDuff.Mode; +import android.graphics.PorterDuffColorFilter; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; +import android.graphics.Region.Op; +import android.graphics.drawable.Drawable; + +import org.jetbrains.annotations.NotNull; + +import androidx.annotation.ColorInt; +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.graphics.drawable.TintAwareDrawable; +import androidx.core.util.ObjectsCompat; + +/** + * Base drawable class for Material Shapes that handles shadows, elevation, scale and color for a + * generated path. + */ +public class MaterialShapeDrawable extends Drawable + implements TintAwareDrawable, ShapeAppearanceModel.OnChangedListener { + + private static final Paint clearPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + // Pre-allocated objects that are re-used several times during path computation and rendering. + private final Matrix matrix = new Matrix(); + private final Path path = new Path(); + private final Path pathInsetByStroke = new Path(); + private final RectF rectF = new RectF(); + private final RectF insetRectF = new RectF(); + private final Region transparentRegion = new Region(); + private final Region scratchRegion = new Region(); + private final Paint fillPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint strokePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final ShapeAppearancePathProvider pathProvider = new ShapeAppearancePathProvider(); + private MaterialShapeDrawableState drawableState; + // Inter-method state. + private boolean pathDirty; + private ShapeAppearanceModel strokeShapeAppearance; + @Nullable + private PorterDuffColorFilter tintFilter; + @Nullable + private PorterDuffColorFilter strokeTintFilter; + + public MaterialShapeDrawable() { + this(new ShapeAppearanceModel()); + } + + /** + * @param shapeAppearanceModel the {@link ShapeAppearanceModel} containing the path that will be + * rendered in this drawable. + */ + public MaterialShapeDrawable(ShapeAppearanceModel shapeAppearanceModel) { + this(new MaterialShapeDrawableState(shapeAppearanceModel)); + } + + private MaterialShapeDrawable(MaterialShapeDrawableState drawableState) { + this.drawableState = drawableState; + strokePaint.setStyle(Style.STROKE); + fillPaint.setStyle(Style.FILL); + clearPaint.setColor(Color.WHITE); + clearPaint.setXfermode(new PorterDuffXfermode(Mode.DST_OUT)); + updateTintFilter(); + updateColorsForState(getState()); + + // Listens for modifications made in the ShapeAppearanceModel, and requests a redraw if the + // ShapeAppearanceModel has changed. + drawableState.shapeAppearanceModel.addOnChangedListener(this); + } + + private static int modulateAlpha(int paintAlpha, int alpha) { + int scale = alpha + (alpha >>> 7); // convert to 0..256 + return (paintAlpha * scale) >>> 8; + } + + @Nullable + @Override + public ConstantState getConstantState() { + return drawableState; + } + + @NonNull + @Override + public Drawable mutate() { + drawableState = new MaterialShapeDrawableState(drawableState); + return this; + } + + /** + * Get the {@link ShapeAppearanceModel} containing the path that will be rendered in this + * drawable. + * + * @return the current model. + */ + @NonNull + private ShapeAppearanceModel getShapeAppearanceModel() { + return drawableState.shapeAppearanceModel; + } + + /** + * Set the {@link ShapeAppearanceModel} containing the path that will be rendered in this + * drawable. + * + * @param shapeAppearanceModel the desired model. + */ + public void setShapeAppearanceModel(@NonNull ShapeAppearanceModel shapeAppearanceModel) { + drawableState.shapeAppearanceModel.removeOnChangedListener(this); + drawableState.shapeAppearanceModel = shapeAppearanceModel; + shapeAppearanceModel.addOnChangedListener(this); + invalidateSelf(); + } + + /** + * Set the color used for the fill. + * + * @param fillColor the color set on the {@link Paint} object responsible for the fill. + */ + public void setFillColor(@Nullable ColorStateList fillColor) { + if (drawableState.fillColor != fillColor) { + drawableState.fillColor = fillColor; + onStateChange(getState()); + } + } + + /** + * Set the color used for the stroke. + * + * @param strokeColor the color set on the {@link Paint} object responsible for the stroke. + */ + private void setStrokeColor(@Nullable ColorStateList strokeColor) { + if (drawableState.strokeColor != strokeColor) { + drawableState.strokeColor = strokeColor; + onStateChange(getState()); + } + } + + @Override + public void setTintMode(@Nullable PorterDuff.Mode tintMode) { + if (drawableState.tintMode != tintMode) { + drawableState.tintMode = tintMode; + updateTintFilter(); + invalidateSelfIgnoreShape(); + } + } + + @Override + public void setTintList(@Nullable ColorStateList tintList) { + drawableState.tintList = tintList; + updateTintFilter(); + invalidateSelfIgnoreShape(); + } + + @Override + public void setTint(@ColorInt int tintColor) { + setTintList(ColorStateList.valueOf(tintColor)); + } + + /** + * Set the shape's stroke width and stroke color. + * + * @param strokeWidth a float for the width of the stroke. + * @param strokeColor an int representing the Color to use for the shape's stroke. + */ + public void setStroke(float strokeWidth, @ColorInt int strokeColor) { + setStrokeWidth(strokeWidth); + setStrokeColor(ColorStateList.valueOf(strokeColor)); + } + + /** + * Set the stroke width used by the shape's paint. + * + * @param strokeWidth desired stroke width. + */ + private void setStrokeWidth(float strokeWidth) { + drawableState.strokeWidth = strokeWidth; + invalidateSelf(); + } + + @Override + public int getOpacity() { + // OPAQUE or TRANSPARENT are possible, but the complexity of determining this based on the + // shape model outweighs the optimizations gained. + return PixelFormat.TRANSLUCENT; + } + + @Override + public void setAlpha(@IntRange(from = 0, to = 255) int alpha) { + if (drawableState.alpha != alpha) { + drawableState.alpha = alpha; + invalidateSelfIgnoreShape(); + } + } + + @Override + public void setColorFilter(@Nullable ColorFilter colorFilter) { + drawableState.colorFilter = colorFilter; + invalidateSelfIgnoreShape(); + } + + @Override + public Region getTransparentRegion() { + Rect bounds = getBounds(); + transparentRegion.set(bounds); + calculatePath(getBoundsAsRectF(), path); + scratchRegion.setPath(path, transparentRegion); + transparentRegion.op(scratchRegion, Op.DIFFERENCE); + return transparentRegion; + } + + private RectF getBoundsAsRectF() { + Rect bounds = getBounds(); + rectF.set(bounds.left, bounds.top, bounds.right, bounds.bottom); + return rectF; + } + + @Override + public void onShapeAppearanceModelChanged() { + invalidateSelf(); + } + + @Override + public void invalidateSelf() { + pathDirty = true; + super.invalidateSelf(); + } + + /** + * Invalidate without recalculating the path associated with this shape. This is useful if the + * shape has stayed the same but we still need to be redrawn, such as when the color has changed. + */ + private void invalidateSelfIgnoreShape() { + super.invalidateSelf(); + } + + /** + * Returns whether the shape has a fill. + */ + private boolean hasFill() { + return drawableState.paintStyle == Style.FILL_AND_STROKE + || drawableState.paintStyle == Style.FILL; + } + + /** + * Returns whether the shape has a stroke with a positive width. + */ + private boolean hasStroke() { + return (drawableState.paintStyle == Style.FILL_AND_STROKE + || drawableState.paintStyle == Style.STROKE) + && strokePaint.getStrokeWidth() > 0; + } + + @Override + protected void onBoundsChange(Rect bounds) { + pathDirty = true; + super.onBoundsChange(bounds); + } + + @Override + public void draw(@NotNull Canvas canvas) { + fillPaint.setColorFilter(tintFilter); + final int prevAlpha = fillPaint.getAlpha(); + fillPaint.setAlpha(modulateAlpha(prevAlpha, drawableState.alpha)); + + strokePaint.setColorFilter(strokeTintFilter); + strokePaint.setStrokeWidth(drawableState.strokeWidth); + + final int prevStrokeAlpha = strokePaint.getAlpha(); + strokePaint.setAlpha(modulateAlpha(prevStrokeAlpha, drawableState.alpha)); + + if (pathDirty) { + calculateStrokePath(); + calculatePath(getBoundsAsRectF(), path); + pathDirty = false; + } + + if (hasFill()) { + drawFillShape(canvas); + } + if (hasStroke()) { + drawStrokeShape(canvas); + } + + fillPaint.setAlpha(prevAlpha); + strokePaint.setAlpha(prevStrokeAlpha); + } + + /** + * Draw the path or try to draw a round rect if possible. + */ + private void drawShape( + Canvas canvas, + Paint paint, + Path path, + ShapeAppearanceModel shapeAppearanceModel, + RectF bounds) { + if (shapeAppearanceModel.isRoundRect()) { + float cornerSize = shapeAppearanceModel.getTopRightCorner().getCornerSize(); + canvas.drawRoundRect(bounds, cornerSize, cornerSize, paint); + } else { + canvas.drawPath(path, paint); + } + } + + private void drawFillShape(Canvas canvas) { + drawShape(canvas, fillPaint, path, drawableState.shapeAppearanceModel, getBoundsAsRectF()); + } + + private void drawStrokeShape(Canvas canvas) { + drawShape( + canvas, strokePaint, pathInsetByStroke, strokeShapeAppearance, getBoundsInsetByStroke()); + } + + /** + * @deprecated see {@link ShapeAppearancePathProvider} + */ + @Deprecated + public void getPathForSize(int width, int height, Path path) { + calculatePathForSize(new RectF(0, 0, width, height), path); + } + + /** + * @deprecated see {@link ShapeAppearancePathProvider} + */ + @Deprecated + public void getPathForSize(Rect bounds, Path path) { + calculatePathForSize(new RectF(bounds), path); + } + + private void calculatePathForSize(RectF bounds, Path path) { + pathProvider.calculatePath( + drawableState.shapeAppearanceModel, + drawableState.interpolation, + bounds, + null, + path); + } + + /** + * Calculates the path that can be used to draw the stroke entirely inside the shape + */ + private void calculateStrokePath() { + strokeShapeAppearance = new ShapeAppearanceModel(getShapeAppearanceModel()); + float cornerSizeTopLeft = strokeShapeAppearance.getTopLeftCorner().cornerSize; + float cornerSizeTopRight = strokeShapeAppearance.getTopRightCorner().cornerSize; + float cornerSizeBottomRight = strokeShapeAppearance.getBottomRightCorner().cornerSize; + float cornerSizeBottomLeft = strokeShapeAppearance.getBottomLeftCorner().cornerSize; + + // Adjust corner radius in order to draw the stroke so that the corners of the background are + // drawn on top of the edges. + strokeShapeAppearance.setCornerRadii( + adjustCornerSizeForStrokeSize(cornerSizeTopLeft), + adjustCornerSizeForStrokeSize(cornerSizeTopRight), + adjustCornerSizeForStrokeSize(cornerSizeBottomRight), + adjustCornerSizeForStrokeSize(cornerSizeBottomLeft)); + + pathProvider.calculatePath( + strokeShapeAppearance, + drawableState.interpolation, + getBoundsInsetByStroke(), + pathInsetByStroke); + } + + private float adjustCornerSizeForStrokeSize(float cornerSize) { + float adjustedCornerSize = cornerSize - getStrokeInsetLength(); + return Math.max(adjustedCornerSize, 0); + } + + private void calculatePath(RectF bounds, Path path) { + calculatePathForSize(bounds, path); + if (drawableState.scale == 1f) { + return; + } + matrix.reset(); + matrix.setScale( + drawableState.scale, drawableState.scale, bounds.width() / 2.0f, bounds.height() / 2.0f); + path.transform(matrix); + } + + private boolean updateTintFilter() { + PorterDuffColorFilter originalTintFilter = tintFilter; + PorterDuffColorFilter originalStrokeTintFilter = strokeTintFilter; + tintFilter = + calculateTintFilter( + drawableState.tintList, + drawableState.tintMode + ); + strokeTintFilter = + calculateTintFilter( + drawableState.strokeTintList, + drawableState.tintMode + ); + return !ObjectsCompat.equals(originalTintFilter, tintFilter) + || !ObjectsCompat.equals(originalStrokeTintFilter, strokeTintFilter); + } + + @Nullable + private PorterDuffColorFilter calculateTintFilter(ColorStateList tintList, PorterDuff.Mode tintMode) { + return tintList == null || tintMode == null + ? null + : new PorterDuffColorFilter(tintList.getColorForState(getState(), Color.TRANSPARENT), tintMode); + } + + @Override + public boolean isStateful() { + return super.isStateful() + || (drawableState.tintList != null && drawableState.tintList.isStateful()) + || (drawableState.strokeTintList != null && drawableState.strokeTintList.isStateful()) + || (drawableState.strokeColor != null && drawableState.strokeColor.isStateful()) + || (drawableState.fillColor != null && drawableState.fillColor.isStateful()); + } + + @Override + protected boolean onStateChange(int[] state) { + boolean paintColorChanged = updateColorsForState(state); + boolean tintFilterChanged = updateTintFilter(); + boolean invalidateSelf = paintColorChanged || tintFilterChanged; + if (invalidateSelf) { + invalidateSelf(); + } + return invalidateSelf; + } + + private boolean updateColorsForState(int[] state) { + boolean invalidateSelf = false; + + if (drawableState.fillColor != null) { + final int previousFillColor = fillPaint.getColor(); + final int newFillColor = drawableState.fillColor.getColorForState(state, previousFillColor); + if (previousFillColor != newFillColor) { + fillPaint.setColor(newFillColor); + invalidateSelf = true; + } + } + + if (drawableState.strokeColor != null) { + final int previousStrokeColor = strokePaint.getColor(); + final int newStrokeColor = + drawableState.strokeColor.getColorForState(state, previousStrokeColor); + if (previousStrokeColor != newStrokeColor) { + strokePaint.setColor(newStrokeColor); + invalidateSelf = true; + } + } + + return invalidateSelf; + } + + private float getStrokeInsetLength() { + if (hasStroke()) { + return strokePaint.getStrokeWidth() / 2.0f; + } + return 0f; + } + + private RectF getBoundsInsetByStroke() { + RectF rectF = getBoundsAsRectF(); + float inset = getStrokeInsetLength(); + insetRectF.set( + rectF.left + inset, rectF.top + inset, rectF.right - inset, rectF.bottom - inset); + return insetRectF; + } + + static final class MaterialShapeDrawableState extends ConstantState { + + @NonNull + ShapeAppearanceModel shapeAppearanceModel; + + @Nullable + ColorFilter colorFilter; + @Nullable + ColorStateList fillColor = null; + @Nullable + ColorStateList strokeColor = null; + @Nullable + ColorStateList strokeTintList = null; + @Nullable + ColorStateList tintList = null; + @Nullable + PorterDuff.Mode tintMode = PorterDuff.Mode.SRC_IN; + + float scale = 1f; + float interpolation = 1f; + float strokeWidth; + + int alpha = 255; + + boolean useTintColorForShadow = false; + + Style paintStyle = Style.FILL_AND_STROKE; + + MaterialShapeDrawableState(@NotNull ShapeAppearanceModel shapeAppearanceModel) { + this.shapeAppearanceModel = shapeAppearanceModel; + } + + MaterialShapeDrawableState(MaterialShapeDrawableState orig) { + shapeAppearanceModel = orig.shapeAppearanceModel; + strokeWidth = orig.strokeWidth; + colorFilter = orig.colorFilter; + fillColor = orig.fillColor; + strokeColor = orig.strokeColor; + tintMode = orig.tintMode; + tintList = orig.tintList; + alpha = orig.alpha; + scale = orig.scale; + useTintColorForShadow = orig.useTintColorForShadow; + interpolation = orig.interpolation; + strokeTintList = orig.strokeTintList; + paintStyle = orig.paintStyle; + } + + @Override + public Drawable newDrawable() { + return new MaterialShapeDrawable(this); + } + + @Override + public int getChangingConfigurations() { + return 0; + } + } +} diff --git a/ui_spinner/src/main/java/de/kuschku/ui/shape/MaterialShapeUtils.java b/ui_spinner/src/main/java/de/kuschku/ui/shape/MaterialShapeUtils.java new file mode 100644 index 000000000..e8043b8d4 --- /dev/null +++ b/ui_spinner/src/main/java/de/kuschku/ui/shape/MaterialShapeUtils.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018 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 + * + * https://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 de.kuschku.ui.shape; + +/** + * Utility methods for {@link MaterialShapeDrawable} and related classes. + */ +class MaterialShapeUtils { + + static CornerTreatment createCornerTreatment(@CornerFamily int cornerFamily, int cornerSize) { + switch (cornerFamily) { + case CornerFamily.ROUNDED: + return new RoundedCornerTreatment(cornerSize); + case CornerFamily.CUT: + return new CutCornerTreatment(cornerSize); + default: + return createDefaultCornerTreatment(); + } + } + + static CornerTreatment createDefaultCornerTreatment() { + return new RoundedCornerTreatment(0); + } + + static EdgeTreatment createDefaultEdgeTreatment() { + return new EdgeTreatment(); + } +} diff --git a/ui_spinner/src/main/java/de/kuschku/ui/shape/RoundedCornerTreatment.java b/ui_spinner/src/main/java/de/kuschku/ui/shape/RoundedCornerTreatment.java new file mode 100644 index 000000000..3cec841a0 --- /dev/null +++ b/ui_spinner/src/main/java/de/kuschku/ui/shape/RoundedCornerTreatment.java @@ -0,0 +1,40 @@ +/* + * Copyright 2017 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 de.kuschku.ui.shape; + +/** + * A corner treatment which rounds a corner of a shape. + */ +public class RoundedCornerTreatment extends CornerTreatment implements Cloneable { + + /** + * Instantiates a rounded corner treatment. + * + * @param radius the radius, in pixels, of the rounded corner, which is rendered as a quarter + * circle. + */ + public RoundedCornerTreatment(float radius) { + super(radius); + } + + @Override + public void getCornerPath(float angle, float interpolation, ShapePath shapePath) { + float radius = cornerSize; + shapePath.reset(0, radius * interpolation); + shapePath.addArc(0, 0, 2 * radius * interpolation, 2 * radius * interpolation, 180, angle); + } +} diff --git a/ui_spinner/src/main/java/de/kuschku/ui/shape/ShapeAppearanceModel.java b/ui_spinner/src/main/java/de/kuschku/ui/shape/ShapeAppearanceModel.java new file mode 100644 index 000000000..e36906a1a --- /dev/null +++ b/ui_spinner/src/main/java/de/kuschku/ui/shape/ShapeAppearanceModel.java @@ -0,0 +1,637 @@ +/* + * Copyright 2018 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 de.kuschku.ui.shape; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.ContextThemeWrapper; + +import java.util.LinkedHashSet; +import java.util.Set; + +import androidx.annotation.AttrRes; +import androidx.annotation.Dimension; +import androidx.annotation.Nullable; +import androidx.annotation.StyleRes; +import de.kuschku.ui.spinner.R; + +/** + * This class models the edges and corners of a shape, which are used by {@link + * MaterialShapeDrawable} to generate and render the shape for a view's background. + */ +public class ShapeAppearanceModel { + + private final Set<OnChangedListener> onChangedListeners = new LinkedHashSet<>(); + private CornerTreatment topLeftCorner; + private CornerTreatment topRightCorner; + private CornerTreatment bottomRightCorner; + private CornerTreatment bottomLeftCorner; + private EdgeTreatment topEdge; + private EdgeTreatment rightEdge; + private EdgeTreatment bottomEdge; + private EdgeTreatment leftEdge; + + /** + * Constructs a default path generator with default edge and corner treatments. + */ + public ShapeAppearanceModel() { + setTopLeftCornerInternal(MaterialShapeUtils.createDefaultCornerTreatment()); + setTopRightCornerInternal(MaterialShapeUtils.createDefaultCornerTreatment()); + setBottomRightCornerInternal(MaterialShapeUtils.createDefaultCornerTreatment()); + setBottomLeftCornerInternal(MaterialShapeUtils.createDefaultCornerTreatment()); + + setLeftEdgeInternal(MaterialShapeUtils.createDefaultEdgeTreatment()); + setTopEdgeInternal(MaterialShapeUtils.createDefaultEdgeTreatment()); + setRightEdgeInternal(MaterialShapeUtils.createDefaultEdgeTreatment()); + setBottomEdgeInternal(MaterialShapeUtils.createDefaultEdgeTreatment()); + + onShapeAppearanceModelChanged(); + } + + public ShapeAppearanceModel(ShapeAppearanceModel shapeAppearanceModel) { + setTopLeftCornerInternal(shapeAppearanceModel.getTopLeftCorner().clone()); + setTopRightCornerInternal(shapeAppearanceModel.getTopRightCorner().clone()); + setBottomRightCornerInternal(shapeAppearanceModel.getBottomRightCorner().clone()); + setBottomLeftCornerInternal(shapeAppearanceModel.getBottomLeftCorner().clone()); + + setLeftEdgeInternal(shapeAppearanceModel.getLeftEdge().clone()); + setTopEdgeInternal(shapeAppearanceModel.getTopEdge().clone()); + setRightEdgeInternal(shapeAppearanceModel.getRightEdge().clone()); + setBottomEdgeInternal(shapeAppearanceModel.getBottomEdge().clone()); + } + + public ShapeAppearanceModel( + Context context, AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { + this(context, attrs, defStyleAttr, defStyleRes, 0); + } + + private ShapeAppearanceModel( + Context context, + AttributeSet attrs, + @AttrRes int defStyleAttr, + @StyleRes int defStyleRes, + int defaultCornerSize) { + TypedArray a = + context.obtainStyledAttributes(attrs, R.styleable.MaterialShape, defStyleAttr, defStyleRes); + + int shapeAppearanceResId = a.getResourceId(R.styleable.MaterialShape_md_shapeAppearance, 0); + int shapeAppearanceOverlayResId = + a.getResourceId(R.styleable.MaterialShape_md_shapeAppearanceOverlay, 0); + a.recycle(); + + // The attributes in shapeAppearanceOverlay should be applied on top of shapeAppearance. + if (shapeAppearanceOverlayResId != 0) { + context = new ContextThemeWrapper(context, shapeAppearanceResId); + shapeAppearanceResId = shapeAppearanceOverlayResId; + } + + a = context.obtainStyledAttributes(shapeAppearanceResId, R.styleable.ShapeAppearance); + + int cornerFamily = a.getInt(R.styleable.ShapeAppearance_md_cornerFamily, CornerFamily.ROUNDED); + int cornerFamilyTopLeft = + a.getInt(R.styleable.ShapeAppearance_md_cornerFamilyTopLeft, cornerFamily); + int cornerFamilyTopRight = + a.getInt(R.styleable.ShapeAppearance_md_cornerFamilyTopRight, cornerFamily); + int cornerFamilyBottomRight = + a.getInt(R.styleable.ShapeAppearance_md_cornerFamilyBottomRight, cornerFamily); + int cornerFamilyBottomLeft = + a.getInt(R.styleable.ShapeAppearance_md_cornerFamilyBottomLeft, cornerFamily); + + int cornerSize = + a.getDimensionPixelSize(R.styleable.ShapeAppearance_md_cornerSize, defaultCornerSize); + int cornerSizeTopLeft = + a.getDimensionPixelSize(R.styleable.ShapeAppearance_md_cornerSizeTopLeft, cornerSize); + int cornerSizeTopRight = + a.getDimensionPixelSize(R.styleable.ShapeAppearance_md_cornerSizeTopRight, cornerSize); + int cornerSizeBottomRight = + a.getDimensionPixelSize(R.styleable.ShapeAppearance_md_cornerSizeBottomRight, cornerSize); + int cornerSizeBottomLeft = + a.getDimensionPixelSize(R.styleable.ShapeAppearance_md_cornerSizeBottomLeft, cornerSize); + + setTopLeftCornerInternal( + MaterialShapeUtils.createCornerTreatment(cornerFamilyTopLeft, cornerSizeTopLeft)); + setTopRightCornerInternal( + MaterialShapeUtils.createCornerTreatment(cornerFamilyTopRight, cornerSizeTopRight)); + setBottomRightCornerInternal( + MaterialShapeUtils.createCornerTreatment(cornerFamilyBottomRight, cornerSizeBottomRight)); + setBottomLeftCornerInternal( + MaterialShapeUtils.createCornerTreatment(cornerFamilyBottomLeft, cornerSizeBottomLeft)); + + setTopEdgeInternal(MaterialShapeUtils.createDefaultEdgeTreatment()); + setRightEdgeInternal(MaterialShapeUtils.createDefaultEdgeTreatment()); + setBottomEdgeInternal(MaterialShapeUtils.createDefaultEdgeTreatment()); + setLeftEdgeInternal(MaterialShapeUtils.createDefaultEdgeTreatment()); + + a.recycle(); + } + + /** + * Sets all corner treatments to {@link CornerTreatment}s generated from a {@code cornerFamily} + * and {@code cornerSize}. + * + * @param cornerFamily The family to be used to create the {@link CornerTreatment}s for all four + * corners. May be one of {@link CornerFamily#ROUNDED} or {@link CornerFamily#CUT}. + * @param cornerSize The size to be used to create the {@link CornerTreatment}s for all four + * corners. + */ + public void setAllCorners(@CornerFamily int cornerFamily, @Dimension int cornerSize) { + setAllCorners(MaterialShapeUtils.createCornerTreatment(cornerFamily, cornerSize)); + } + + /** + * Sets all corner treatments. + * + * @param cornerTreatment the corner treatment to use for all four corners. + */ + private void setAllCorners(CornerTreatment cornerTreatment) { + boolean changed = setTopLeftCornerInternal(cornerTreatment.clone()); + changed |= setTopRightCornerInternal(cornerTreatment.clone()); + changed |= setBottomRightCornerInternal(cornerTreatment.clone()); + changed |= setBottomLeftCornerInternal(cornerTreatment.clone()); + + if (changed) { + onShapeAppearanceModelChanged(); + } + } + + /** + * Sets the corner size of all four corner treatments to {@code cornerRadius}. This is a + * convenience method for {@link #setCornerRadii(float, float, float, float)})}. + * + * <p>Note: This method does not create new {@link CornerTreatment}s for all four corners. + * Instead, it directly modifies the corner size of each existing corner treatment. + * + * @see #setCornerRadii(float, float, float, float) + */ + public void setCornerRadius(float cornerRadius) { + setCornerRadii(cornerRadius, cornerRadius, cornerRadius, cornerRadius); + } + + /** + * Sets the corner size of all four corner treatments using the {@code topLeftCornerRadius}, + * {@code topRightCornerRadius}, {@code bottomRightCornerRadius}, and {@code + * bottomLeftCornerRadius}. + * + * <p>Note: This method does not create new {@link CornerTreatment}s for all four corners. + * Instead, it directly modifies the corner size of each existing corner treatment. + */ + public void setCornerRadii( + float topLeftCornerRadius, + float topRightCornerRadius, + float bottomRightCornerRadius, + float bottomLeftCornerRadius) { + boolean changed = setTopLeftCornerSizeInternal(topLeftCornerRadius); + changed |= setTopRightCornerSizeInternal(topRightCornerRadius); + changed |= setBottomRightCornerSizeInternal(bottomRightCornerRadius); + changed |= setBottomLeftCornerSizeInternal(bottomLeftCornerRadius); + + if (changed) { + onShapeAppearanceModelChanged(); + } + } + + private boolean setTopLeftCornerSizeInternal(float topLeftCornerSize) { + boolean changed = false; + if (this.topLeftCorner.cornerSize != topLeftCornerSize) { + this.topLeftCorner.cornerSize = topLeftCornerSize; + changed = true; + } + return changed; + } + + private boolean setTopRightCornerSizeInternal(float topRightCornerSize) { + boolean changed = false; + if (this.topRightCorner.cornerSize != topRightCornerSize) { + this.topRightCorner.cornerSize = topRightCornerSize; + changed = true; + } + return changed; + } + + private boolean setBottomRightCornerSizeInternal(float bottomRightCornerSize) { + boolean changed = false; + if (this.bottomRightCorner.cornerSize != bottomRightCornerSize) { + this.bottomRightCorner.cornerSize = bottomRightCornerSize; + changed = true; + } + return changed; + } + + private boolean setBottomLeftCornerSizeInternal(float bottomLeftCornerSize) { + boolean changed = false; + if (this.bottomLeftCorner.cornerSize != bottomLeftCornerSize) { + this.bottomLeftCorner.cornerSize = bottomLeftCornerSize; + changed = true; + } + return changed; + } + + /** + * Sets all edge treatments. + * + * @param edgeTreatment the edge treatment to use for all four edges. + */ + public void setAllEdges(EdgeTreatment edgeTreatment) { + boolean changed = setLeftEdgeInternal(edgeTreatment.clone()); + changed |= setTopEdgeInternal(edgeTreatment.clone()); + changed |= setRightEdgeInternal(edgeTreatment.clone()); + changed |= setBottomEdgeInternal(edgeTreatment.clone()); + + if (changed) { + onShapeAppearanceModelChanged(); + } + } + + /** + * Sets corner treatments. + * + * @param topLeftCorner the corner treatment to use in the top left corner. + * @param topRightCorner the corner treatment to use in the top right corner. + * @param bottomRightCorner the corner treatment to use in the bottom right corner. + * @param bottomLeftCorner the corner treatment to use in the bottom left corner. + */ + public void setCornerTreatments( + CornerTreatment topLeftCorner, + CornerTreatment topRightCorner, + CornerTreatment bottomRightCorner, + CornerTreatment bottomLeftCorner) { + boolean changed = setTopLeftCornerInternal(topLeftCorner); + changed |= setTopRightCornerInternal(topRightCorner); + changed |= setBottomRightCornerInternal(bottomRightCorner); + changed |= setBottomLeftCornerInternal(bottomLeftCorner); + + if (changed) { + onShapeAppearanceModelChanged(); + } + } + + /** + * Sets edge treatments. + * + * @param leftEdge the edge treatment to use on the left edge. + * @param topEdge the edge treatment to use on the top edge. + * @param rightEdge the edge treatment to use on the right edge. + * @param bottomEdge the edge treatment to use on the bottom edge. + */ + public void setEdgeTreatments( + EdgeTreatment leftEdge, + EdgeTreatment topEdge, + EdgeTreatment rightEdge, + EdgeTreatment bottomEdge) { + boolean changed = setLeftEdgeInternal(leftEdge); + changed |= setTopEdgeInternal(topEdge); + changed |= setRightEdgeInternal(rightEdge); + changed |= setBottomEdgeInternal(bottomEdge); + + if (changed) { + onShapeAppearanceModelChanged(); + } + } + + /** + * Sets the corner treatment for the top left corner. + * + * @param cornerFamily the family to use to create the corner treatment + * @param cornerSize the size to use to create the corner treatment + */ + public void setTopLeftCorner(@CornerFamily int cornerFamily, @Dimension int cornerSize) { + setTopLeftCorner(MaterialShapeUtils.createCornerTreatment(cornerFamily, cornerSize)); + } + + private boolean setTopLeftCornerInternal(CornerTreatment topLeftCorner) { + boolean changed = false; + if (this.topLeftCorner != topLeftCorner) { + this.topLeftCorner = topLeftCorner; + changed = true; + } + return changed; + } + + /** + * Gets the corner treatment for the top left corner. + * + * @return the corner treatment for the top left corner. + */ + public CornerTreatment getTopLeftCorner() { + return topLeftCorner; + } + + /** + * Sets the corner treatment for the top left corner. + * + * @param topLeftCorner the desired treatment. + */ + private void setTopLeftCorner(CornerTreatment topLeftCorner) { + if (setTopLeftCornerInternal(topLeftCorner)) { + onShapeAppearanceModelChanged(); + } + } + + /** + * Sets the corner treatment for the top right corner. + * + * @param cornerFamily the family to use to create the corner treatment + * @param cornerSize the size to use to create the corner treatment + */ + public void setTopRightCorner(@CornerFamily int cornerFamily, @Dimension int cornerSize) { + setTopRightCorner(MaterialShapeUtils.createCornerTreatment(cornerFamily, cornerSize)); + } + + private boolean setTopRightCornerInternal(CornerTreatment topRightCorner) { + boolean changed = false; + if (this.topRightCorner != topRightCorner) { + this.topRightCorner = topRightCorner; + changed = true; + } + return changed; + } + + /** + * Gets the corner treatment for the top right corner. + * + * @return the corner treatment for the top right corner. + */ + public CornerTreatment getTopRightCorner() { + return topRightCorner; + } + + /** + * Sets the corner treatment for the top right corner. + * + * @param topRightCorner the desired treatment. + */ + private void setTopRightCorner(CornerTreatment topRightCorner) { + if (setTopRightCornerInternal(topRightCorner)) { + onShapeAppearanceModelChanged(); + } + } + + /** + * Sets the corner treatment for the bottom right corner. + * + * @param cornerFamily the family to use to create the corner treatment + * @param cornerSize the size to use to create the corner treatment + */ + public void setBottomRightCorner(@CornerFamily int cornerFamily, @Dimension int cornerSize) { + setBottomRightCorner(MaterialShapeUtils.createCornerTreatment(cornerFamily, cornerSize)); + } + + private boolean setBottomRightCornerInternal(CornerTreatment bottomRightCorner) { + boolean changed = false; + if (this.bottomRightCorner != bottomRightCorner) { + this.bottomRightCorner = bottomRightCorner; + changed = true; + } + return changed; + } + + /** + * Gets the corner treatment for the bottom right corner. + * + * @return the corner treatment for the bottom right corner. + */ + public CornerTreatment getBottomRightCorner() { + return bottomRightCorner; + } + + /** + * Sets the corner treatment for the bottom right corner. + * + * @param bottomRightCorner the desired treatment. + */ + private void setBottomRightCorner(CornerTreatment bottomRightCorner) { + if (setBottomRightCornerInternal(bottomRightCorner)) { + onShapeAppearanceModelChanged(); + } + } + + /** + * Sets the corner treatment for the bottom left corner. + * + * @param cornerFamily the family to use to create the corner treatment + * @param cornerSize the size to use to create the corner treatment + */ + public void setBottomLeftCorner(@CornerFamily int cornerFamily, @Dimension int cornerSize) { + setBottomLeftCorner(MaterialShapeUtils.createCornerTreatment(cornerFamily, cornerSize)); + } + + private boolean setBottomLeftCornerInternal(CornerTreatment bottomLeftCorner) { + boolean changed = false; + if (this.bottomLeftCorner != bottomLeftCorner) { + this.bottomLeftCorner = bottomLeftCorner; + changed = true; + } + return changed; + } + + /** + * Gets the corner treatment for the bottom left corner. + * + * @return the corner treatment for the bottom left corner. + */ + public CornerTreatment getBottomLeftCorner() { + return bottomLeftCorner; + } + + /** + * Sets the corner treatment for the bottom left corner. + * + * @param bottomLeftCorner the desired treatment. + */ + private void setBottomLeftCorner(CornerTreatment bottomLeftCorner) { + if (setBottomLeftCornerInternal(bottomLeftCorner)) { + onShapeAppearanceModelChanged(); + } + } + + private boolean setLeftEdgeInternal(EdgeTreatment leftEdge) { + boolean changed = false; + if (this.leftEdge != leftEdge) { + this.leftEdge = leftEdge; + changed = true; + } + return changed; + } + + /** + * Gets the edge treatment for the left edge. + * + * @return the edge treatment for the left edge. + */ + public EdgeTreatment getLeftEdge() { + return leftEdge; + } + + /** + * Sets the edge treatment for the left edge. + * + * @param leftEdge the desired treatment. + */ + public void setLeftEdge(EdgeTreatment leftEdge) { + if (setLeftEdgeInternal(leftEdge)) { + onShapeAppearanceModelChanged(); + } + } + + private boolean setTopEdgeInternal(EdgeTreatment topEdge) { + boolean changed = false; + if (this.topEdge != topEdge) { + this.topEdge = topEdge; + changed = true; + } + return changed; + } + + /** + * Gets the edge treatment for the top edge. + * + * @return the edge treatment for the top edge. + */ + public EdgeTreatment getTopEdge() { + return topEdge; + } + + /** + * Sets the edge treatment for the top edge. + * + * @param topEdge the desired treatment. + */ + public void setTopEdge(EdgeTreatment topEdge) { + if (setTopEdgeInternal(topEdge)) { + onShapeAppearanceModelChanged(); + } + } + + private boolean setRightEdgeInternal(EdgeTreatment rightEdge) { + boolean changed = false; + if (this.rightEdge != rightEdge) { + this.rightEdge = rightEdge; + changed = true; + } + return changed; + } + + /** + * Gets the edge treatment for the right edge. + * + * @return the edge treatment for the right edge. + */ + public EdgeTreatment getRightEdge() { + return rightEdge; + } + + /** + * Sets the edge treatment for the right edge. + * + * @param rightEdge the desired treatment. + */ + public void setRightEdge(EdgeTreatment rightEdge) { + if (setRightEdgeInternal(rightEdge)) { + onShapeAppearanceModelChanged(); + } + } + + private boolean setBottomEdgeInternal(EdgeTreatment bottomEdge) { + boolean changed = false; + if (this.bottomEdge != bottomEdge) { + this.bottomEdge = bottomEdge; + changed = true; + } + return changed; + } + + /** + * Gets the edge treatment for the bottom edge. + * + * @return the edge treatment for the bottom edge. + */ + public EdgeTreatment getBottomEdge() { + return bottomEdge; + } + + /** + * Sets the edge treatment for the bottom edge. + * + * @param bottomEdge the desired treatment. + */ + public void setBottomEdge(EdgeTreatment bottomEdge) { + if (setBottomEdgeInternal(bottomEdge)) { + onShapeAppearanceModelChanged(); + } + } + + void addOnChangedListener(@Nullable OnChangedListener onChangedListener) { + onChangedListeners.add(onChangedListener); + } + + void removeOnChangedListener(@Nullable OnChangedListener onChangedListener) { + onChangedListeners.remove(onChangedListener); + } + + private void onShapeAppearanceModelChanged() { + for (OnChangedListener onChangedListener : onChangedListeners) { + if (onChangedListener != null) { + onChangedListener.onShapeAppearanceModelChanged(); + } + } + } + + /** + * Checks Corner and Edge treatments to see if we can use {@link Canvas#drawRoundRect(RectF, float, + * float, Paint)} "} to draw this model. + */ + public boolean isRoundRect() { + boolean hasDefaultEdges = + leftEdge.getClass().equals(EdgeTreatment.class) + && rightEdge.getClass().equals(EdgeTreatment.class) + && topEdge.getClass().equals(EdgeTreatment.class) + && bottomEdge.getClass().equals(EdgeTreatment.class); + + float cornerSize = topLeftCorner.getCornerSize(); + + boolean cornersHaveSameSize = + topRightCorner.getCornerSize() == cornerSize + && bottomLeftCorner.getCornerSize() == cornerSize + && bottomRightCorner.getCornerSize() == cornerSize; + + boolean hasRoundedCorners = + topRightCorner instanceof RoundedCornerTreatment + && topLeftCorner instanceof RoundedCornerTreatment + && bottomRightCorner instanceof RoundedCornerTreatment + && bottomLeftCorner instanceof RoundedCornerTreatment; + + return hasDefaultEdges && cornersHaveSameSize && hasRoundedCorners; + } + + /** + * Listener called every time a {@link ShapeAppearanceModel} corner or edge is modified and + * notifies the {@link MaterialShapeDrawable} that the shape has changed so that it can invalidate + * itself. Components that need to respond to shape changes can use this interface to get a + * callback to respond to shape changes. + */ + public interface OnChangedListener { + + /** + * Callback invoked when a corner or edge of the {@link ShapeAppearanceModel} changes. + */ + void onShapeAppearanceModelChanged(); + } +} diff --git a/ui_spinner/src/main/java/de/kuschku/ui/shape/ShapeAppearancePathProvider.java b/ui_spinner/src/main/java/de/kuschku/ui/shape/ShapeAppearancePathProvider.java new file mode 100644 index 000000000..5154bc820 --- /dev/null +++ b/ui_spinner/src/main/java/de/kuschku/ui/shape/ShapeAppearancePathProvider.java @@ -0,0 +1,268 @@ +/* + * Copyright 2018 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 de.kuschku.ui.shape; + +import android.graphics.Matrix; +import android.graphics.Path; +import android.graphics.PointF; +import android.graphics.RectF; + +import org.jetbrains.annotations.NotNull; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * A class to convert a {@link ShapeAppearanceModel to a {@link android.graphics.Path}}. + */ +class ShapeAppearancePathProvider { + + // Inter-method state. + private final ShapePath[] cornerPaths = new ShapePath[4]; + private final Matrix[] cornerTransforms = new Matrix[4]; + private final Matrix[] edgeTransforms = new Matrix[4]; + // Pre-allocated objects that are re-used several times during path computation and rendering. + private final PointF pointF = new PointF(); + private final ShapePath shapePath = new ShapePath(); + private final float[] scratch = new float[2]; + private final float[] scratch2 = new float[2]; + public ShapeAppearancePathProvider() { + for (int i = 0; i < 4; i++) { + cornerPaths[i] = new ShapePath(); + cornerTransforms[i] = new Matrix(); + edgeTransforms[i] = new Matrix(); + } + } + + /** + * Writes the given {@link ShapeAppearanceModel} to {@code path} + * + * @param shapeAppearanceModel The shape to be applied in the path. + * @param interpolation the desired interpolation. + * @param bounds the desired bounds for the path. + * @param path the returned path out-var. + */ + public void calculatePath( + ShapeAppearanceModel shapeAppearanceModel, float interpolation, RectF bounds, Path path) { + calculatePath(shapeAppearanceModel, interpolation, bounds, null, path); + } + + /** + * Writes the given {@link ShapeAppearanceModel} to {@code path} + * + * @param shapeAppearanceModel The shape to be applied in the path. + * @param interpolation the desired interpolation. + * @param bounds the desired bounds for the path. + * @param pathListener the path + * @param path the returned path out-var. + */ + public void calculatePath( + ShapeAppearanceModel shapeAppearanceModel, + float interpolation, + RectF bounds, + PathListener pathListener, + Path path) { + path.rewind(); + ShapeAppearancePathSpec spec = + new ShapeAppearancePathSpec( + shapeAppearanceModel, interpolation, bounds, pathListener, path); + + // Calculate the transformations (rotations and translations) necessary for each edge and + // corner treatment. + for (int index = 0; index < 4; index++) { + setCornerPathAndTransform(spec, index); + setEdgePathAndTransform(index); + } + + // Apply corners and edges to the path in clockwise interleaving sequence: top-right corner, + // right edge, bottom-right corner, bottom edge, bottom-left corner etc. We start from the top + // right corner rather than the top left to work around a bug in API level 21 and 22 in which + // rounding error causes the path to incorrectly be marked as concave. + for (int index = 0; index < 4; index++) { + appendCornerPath(spec, index); + appendEdgePath(spec, index); + } + + path.close(); + } + + private void setCornerPathAndTransform(ShapeAppearancePathSpec spec, int index) { + getCornerTreatmentForIndex(index, spec.shapeAppearanceModel) + .getCornerPath(90, spec.interpolation, cornerPaths[index]); + + float edgeAngle = angleOfEdge(index); + cornerTransforms[index].reset(); + getCoordinatesOfCorner(index, spec.bounds, pointF); + cornerTransforms[index].setTranslate(pointF.x, pointF.y); + cornerTransforms[index].preRotate(edgeAngle); + } + + private void setEdgePathAndTransform(int index) { + scratch[0] = cornerPaths[index].getEndX(); + scratch[1] = cornerPaths[index].getEndY(); + cornerTransforms[index].mapPoints(scratch); + float edgeAngle = angleOfEdge(index); + edgeTransforms[index].reset(); + edgeTransforms[index].setTranslate(scratch[0], scratch[1]); + edgeTransforms[index].preRotate(edgeAngle); + } + + private void appendCornerPath(ShapeAppearancePathSpec spec, int index) { + scratch[0] = cornerPaths[index].getStartX(); + scratch[1] = cornerPaths[index].getStartY(); + cornerTransforms[index].mapPoints(scratch); + if (index == 0) { + spec.path.moveTo(scratch[0], scratch[1]); + } else { + spec.path.lineTo(scratch[0], scratch[1]); + } + cornerPaths[index].applyToPath(cornerTransforms[index], spec.path); + if (spec.pathListener != null) { + spec.pathListener.onCornerPathCreated(cornerPaths[index], cornerTransforms[index], index); + } + } + + private void appendEdgePath(ShapeAppearancePathSpec spec, int index) { + int nextIndex = (index + 1) % 4; + scratch[0] = cornerPaths[index].getEndX(); + scratch[1] = cornerPaths[index].getEndY(); + cornerTransforms[index].mapPoints(scratch); + + scratch2[0] = cornerPaths[nextIndex].getStartX(); + scratch2[1] = cornerPaths[nextIndex].getStartY(); + cornerTransforms[nextIndex].mapPoints(scratch2); + + float edgeLength = (float) Math.hypot(scratch[0] - scratch2[0], scratch[1] - scratch2[1]); + // TODO: Remove this -.001f that is currently needed to handle rounding errors + edgeLength = Math.max(edgeLength - .001f, 0); + float center = getEdgeCenterForIndex(spec.bounds, index); + shapePath.reset(0, 0); + getEdgeTreatmentForIndex(index, spec.shapeAppearanceModel) + .getEdgePath(edgeLength, center, spec.interpolation, shapePath); + shapePath.applyToPath(edgeTransforms[index], spec.path); + if (spec.pathListener != null) { + spec.pathListener.onEdgePathCreated(shapePath, edgeTransforms[index], index); + } + } + + private float getEdgeCenterForIndex(RectF bounds, int index) { + scratch[0] = cornerPaths[index].getEndX(); + scratch[1] = cornerPaths[index].getEndY(); + cornerTransforms[index].mapPoints(scratch); + switch (index) { + case 1: + case 3: + return Math.abs(bounds.centerX() - scratch[0]); + case 2: + case 0: + default: + return Math.abs(bounds.centerY() - scratch[1]); + } + } + + private CornerTreatment getCornerTreatmentForIndex( + int index, ShapeAppearanceModel shapeAppearanceModel) { + switch (index) { + case 1: + return shapeAppearanceModel.getBottomRightCorner(); + case 2: + return shapeAppearanceModel.getBottomLeftCorner(); + case 3: + return shapeAppearanceModel.getTopLeftCorner(); + case 0: + default: + return shapeAppearanceModel.getTopRightCorner(); + } + } + + private EdgeTreatment getEdgeTreatmentForIndex( + int index, ShapeAppearanceModel shapeAppearanceModel) { + switch (index) { + case 1: + return shapeAppearanceModel.getBottomEdge(); + case 2: + return shapeAppearanceModel.getLeftEdge(); + case 3: + return shapeAppearanceModel.getTopEdge(); + case 0: + default: + return shapeAppearanceModel.getRightEdge(); + } + } + + private void getCoordinatesOfCorner(int index, RectF bounds, PointF pointF) { + switch (index) { + case 1: // bottom-right + pointF.set(bounds.right, bounds.bottom); + break; + case 2: // bottom-left + pointF.set(bounds.left, bounds.bottom); + break; + case 3: // top-left + pointF.set(bounds.left, bounds.top); + break; + case 0: // top-right + default: + pointF.set(bounds.right, bounds.top); + break; + } + } + + private float angleOfEdge(int index) { + return 90 * (index + 1 % 4); + } + + /** + * Listener called every time a {@link ShapePath} is created for a corner or an edge treatment. + */ + public interface PathListener { + void onCornerPathCreated(ShapePath cornerPath, Matrix transform, int count); + + void onEdgePathCreated(ShapePath edgePath, Matrix transform, int count); + } + + /** + * Necessary information to map a {@link ShapeAppearanceModel} into a Path. + */ + static final class ShapeAppearancePathSpec { + + @NonNull + final ShapeAppearanceModel shapeAppearanceModel; + @NonNull + final Path path; + @NonNull + final RectF bounds; + + @Nullable + final PathListener pathListener; + + final float interpolation; + + ShapeAppearancePathSpec( + @NonNull ShapeAppearanceModel shapeAppearanceModel, + float interpolation, + @NotNull RectF bounds, + @Nullable PathListener pathListener, + @NotNull Path path) { + this.pathListener = pathListener; + this.shapeAppearanceModel = shapeAppearanceModel; + this.interpolation = interpolation; + this.bounds = bounds; + this.path = path; + } + } +} diff --git a/ui_spinner/src/main/java/de/kuschku/ui/shape/ShapePath.kt b/ui_spinner/src/main/java/de/kuschku/ui/shape/ShapePath.kt new file mode 100644 index 000000000..99d48021b --- /dev/null +++ b/ui_spinner/src/main/java/de/kuschku/ui/shape/ShapePath.kt @@ -0,0 +1,193 @@ +/* + * Copyright 2017 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 de.kuschku.ui.shape + +import android.graphics.Matrix +import android.graphics.Path +import android.graphics.RectF +import kotlin.math.cos +import kotlin.math.sin + +/** + * Represents the descriptive path of a shape. Path segments are stored in sequence so that + * transformations can be applied to them when the [android.graphics.Path] is produced by the + * [MaterialShapeDrawable]. + */ +class ShapePath { + private val operations = mutableListOf<PathOperation>() + var startX = 0f + var startY = 0f + var endX = 0f + var endY = 0f + + constructor() { + reset(0f, 0f) + } + + constructor(startX: Float, startY: Float) { + reset(startX, startY) + } + + fun reset(startX: Float, startY: Float) { + this.startX = startX + this.startY = startY + this.endX = startX + this.endY = startY + this.operations.clear() + } + + /** + * Add a line to the ShapePath. + * + * @param x the x to which the line should be drawn. + * @param y the y to which the line should be drawn. + */ + fun lineTo(x: Float, y: Float) { + operations.add(PathLineOperation( + x = x, + y = y + )) + + endX = x + endY = y + } + + /** + * Add a quad to the ShapePath. + * + * @param controlX the control point x of the arc. + * @param controlY the control point y of the arc. + * @param toX the end x of the arc. + * @param toY the end y of the arc. + */ + fun quadToPoint(controlX: Float, controlY: Float, toX: Float, toY: Float) { + operations.add(PathQuadOperation( + controlX = controlX, + controlY = controlY, + endX = toX, + endY = toY + )) + + endX = toX + endY = toY + } + + /** + * Add an arc to the ShapePath. + * + * @param left the X coordinate of the left side of the rectangle containing the arc oval. + * @param top the Y coordinate of the top of the rectangle containing the arc oval. + * @param right the X coordinate of the right side of the rectangle containing the arc oval. + * @param bottom the Y coordinate of the bottom of the rectangle containing the arc oval. + * @param startAngle start angle of the arc. + * @param sweepAngle sweep angle of the arc. + */ + fun addArc(left: Float, top: Float, right: Float, bottom: Float, + startAngle: Float, sweepAngle: Float) { + operations.add(PathArcOperation( + left = left, + top = top, + right = right, + bottom = bottom, + startAngle = startAngle, + sweepAngle = sweepAngle + )) + + endX = (left + right) * 0.5f + (right - left) / 2 * cos(Math.toRadians((startAngle + sweepAngle).toDouble())).toFloat() + endY = (top + bottom) * 0.5f + (bottom - top) / 2 * sin(Math.toRadians((startAngle + sweepAngle).toDouble())).toFloat() + } + + /** + * Apply the ShapePath sequence to a [android.graphics.Path] under a matrix transform. + * + * @param transform the matrix transform under which this ShapePath is applied + * @param path the path to which this ShapePath is applied + */ + fun applyToPath(transform: Matrix, path: Path) { + for (operation in operations) { + operation.applyToPath(transform, path) + } + } + + /** + * Interface for a path operation to be appended to the operations list. + */ + abstract class PathOperation { + val matrix = Matrix() + + abstract fun applyToPath(transform: Matrix, path: Path) + } + + /** + * Straight line operation. + */ + class PathLineOperation( + val x: Float = 0f, + val y: Float = 0f + ) : PathOperation() { + override fun applyToPath(transform: Matrix, path: Path) { + val inverse = matrix + transform.invert(inverse) + path.transform(inverse) + path.lineTo(x, y) + path.transform(transform) + } + } + + /** + * Path quad operation. + */ + class PathQuadOperation( + val controlX: Float = 0f, + val controlY: Float = 0f, + val endX: Float = 0f, + val endY: Float = 0f + ) : PathOperation() { + override fun applyToPath(transform: Matrix, path: Path) { + val inverse = matrix + transform.invert(inverse) + path.transform(inverse) + path.quadTo(controlX, controlY, endX, endY) + path.transform(transform) + } + } + + /** + * Path arc operation. + */ + class PathArcOperation( + val left: Float, + val top: Float, + val right: Float, + val bottom: Float, + val startAngle: Float = 0f, + val sweepAngle: Float = 0f + ) : PathOperation() { + override fun applyToPath(transform: Matrix, path: Path) { + val inverse = matrix + transform.invert(inverse) + path.transform(inverse) + rectF.set(left, top, right, bottom) + path.arcTo(rectF, startAngle, sweepAngle, false) + path.transform(transform) + } + + companion object { + private val rectF = RectF() + } + } +} diff --git a/ui_spinner/src/main/java/de/kuschku/ui/spinner/CutoutDrawable.java b/ui_spinner/src/main/java/de/kuschku/ui/spinner/CutoutDrawable.java new file mode 100644 index 000000000..821e33868 --- /dev/null +++ b/ui_spinner/src/main/java/de/kuschku/ui/spinner/CutoutDrawable.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2017 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 de.kuschku.ui.spinner; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.PorterDuff.Mode; +import android.graphics.PorterDuffXfermode; +import android.graphics.RectF; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.view.View; + +import de.kuschku.ui.shape.MaterialShapeDrawable; +import de.kuschku.ui.shape.ShapeAppearanceModel; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * A {@link MaterialShapeDrawable} that can draw a cutout for the label in {@link MaterialSpinnerLayout}'s + * outline mode. + */ +class CutoutDrawable extends MaterialShapeDrawable { + private final Paint cutoutPaint; + private final RectF cutoutBounds; + private int savedLayer; + + CutoutDrawable() { + this(null); + } + + CutoutDrawable(@Nullable ShapeAppearanceModel shapeAppearanceModel) { + super(shapeAppearanceModel != null ? shapeAppearanceModel : new ShapeAppearanceModel()); + cutoutPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + setPaintStyles(); + cutoutBounds = new RectF(); + } + + private void setPaintStyles() { + cutoutPaint.setStyle(Style.FILL_AND_STROKE); + cutoutPaint.setColor(Color.WHITE); + cutoutPaint.setXfermode(new PorterDuffXfermode(Mode.DST_OUT)); + } + + boolean hasCutout() { + return !cutoutBounds.isEmpty(); + } + + private void setCutout(float left, float top, float right, float bottom) { + // Avoid expensive redraws by only calling invalidateSelf if one of the cutout's dimensions has + // changed. + if (left != cutoutBounds.left + || top != cutoutBounds.top + || right != cutoutBounds.right + || bottom != cutoutBounds.bottom) { + cutoutBounds.set(left, top, right, bottom); + invalidateSelf(); + } + } + + void setCutout(RectF bounds) { + setCutout(bounds.left, bounds.top, bounds.right, bounds.bottom); + } + + void removeCutout() { + // Call setCutout with empty bounds to remove the cutout. + setCutout(0, 0, 0, 0); + } + + @Override + public void draw(@NonNull Canvas canvas) { + preDraw(canvas); + super.draw(canvas); + + // Draw mask for the cutout. + canvas.drawRect(cutoutBounds, cutoutPaint); + + postDraw(canvas); + } + + private void preDraw(@NonNull Canvas canvas) { + Callback callback = getCallback(); + + if (useHardwareLayer(callback)) { + View viewCallback = (View) callback; + // Make sure we're using a hardware layer. + if (viewCallback.getLayerType() != View.LAYER_TYPE_HARDWARE) { + viewCallback.setLayerType(View.LAYER_TYPE_HARDWARE, null); + } + } else { + // If we're not using a hardware layer, save the canvas layer. + saveCanvasLayer(canvas); + } + } + + private void saveCanvasLayer(@NonNull Canvas canvas) { + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + savedLayer = canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), null); + } else { + savedLayer = + canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), null, Canvas.ALL_SAVE_FLAG); + } + } + + private void postDraw(@NonNull Canvas canvas) { + if (!useHardwareLayer(getCallback())) { + canvas.restoreToCount(savedLayer); + } + } + + private boolean useHardwareLayer(Callback callback) { + return callback instanceof View; + } +} diff --git a/ui_spinner/src/main/java/de/kuschku/ui/spinner/IndicatorViewController.java b/ui_spinner/src/main/java/de/kuschku/ui/spinner/IndicatorViewController.java new file mode 100644 index 000000000..2b413d5cb --- /dev/null +++ b/ui_spinner/src/main/java/de/kuschku/ui/spinner/IndicatorViewController.java @@ -0,0 +1,587 @@ +/* + * Copyright (C) 2017 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 de.kuschku.ui.spinner; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Typeface; +import android.text.TextUtils; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.Space; +import android.widget.TextView; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.ColorInt; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import androidx.annotation.StyleRes; +import androidx.appcompat.widget.AppCompatTextView; +import androidx.core.view.ViewCompat; +import androidx.core.widget.TextViewCompat; +import de.kuschku.ui.animation.AnimationUtils; +import de.kuschku.ui.animation.AnimatorSetCompat; + +import static android.view.View.TRANSLATION_Y; +import static android.view.View.VISIBLE; + +/** + * Controller for indicator views underneath the text input line in {@link + * de.kuschku.ui.spinner.spinnerLayout}. This class controls helper and error views. + */ +final class IndicatorViewController { + /* + * TODO: Update placeholder values for caption animation. + * + */ + + /** + * Duration for the caption's vertical translation animation. + */ + private static final int CAPTION_TRANSLATE_Y_ANIMATION_DURATION = 217; + + /** + * Duration for the caption's opacity fade animation. + */ + private static final int CAPTION_OPACITY_FADE_ANIMATION_DURATION = 167; + private static final int ERROR_INDEX = 0; + private static final int HELPER_INDEX = 1; + private static final int CAPTION_STATE_NONE = 0; + private static final int CAPTION_STATE_ERROR = 1; + private static final int CAPTION_STATE_HELPER_TEXT = 2; + private final Context context; + private final MaterialSpinnerLayout spinnerView; + private final float captionTranslationYPx; + private LinearLayout indicatorArea; + private int indicatorsAdded; + private FrameLayout captionArea; + private int captionViewsAdded; + @Nullable + private Animator captionAnimator; + private int captionDisplayed; + private int captionToShow; + private CharSequence errorText; + private boolean errorEnabled; + private TextView errorView; + private int errorTextAppearance; + @Nullable + private ColorStateList errorViewTextColor; + private CharSequence helperText; + private boolean helperTextEnabled; + private TextView helperTextView; + private int helperTextTextAppearance; + @Nullable + private ColorStateList helperTextViewTextColor; + private Typeface typeface; + + public IndicatorViewController(MaterialSpinnerLayout spinnerView) { + this.context = spinnerView.getContext(); + this.spinnerView = spinnerView; + this.captionTranslationYPx = + context.getResources().getDimensionPixelSize(R.dimen.md_design_spinner_caption_translate_y); + } + + void showHelper(final CharSequence helperText) { + cancelCaptionAnimator(); + this.helperText = helperText; + helperTextView.setText(helperText); + + // If helper is not already shown, show helper. + if (captionDisplayed != CAPTION_STATE_HELPER_TEXT) { + captionToShow = CAPTION_STATE_HELPER_TEXT; + } + updateCaptionViewsVisibility( + captionDisplayed, captionToShow, shouldAnimateCaptionView(helperTextView, helperText)); + } + + private void hideHelperText() { + cancelCaptionAnimator(); + + // Hide helper if it's shown. + if (captionDisplayed == CAPTION_STATE_HELPER_TEXT) { + captionToShow = CAPTION_STATE_NONE; + } + updateCaptionViewsVisibility( + captionDisplayed, captionToShow, shouldAnimateCaptionView(helperTextView, null)); + } + + void showError(final CharSequence errorText) { + cancelCaptionAnimator(); + this.errorText = errorText; + errorView.setText(errorText); + + // If error is not already shown, show error. + if (captionDisplayed != CAPTION_STATE_ERROR) { + captionToShow = CAPTION_STATE_ERROR; + } + updateCaptionViewsVisibility( + captionDisplayed, captionToShow, shouldAnimateCaptionView(errorView, errorText)); + } + + void hideError() { + errorText = null; + cancelCaptionAnimator(); + // Hide error if it's shown. + if (captionDisplayed == CAPTION_STATE_ERROR) { + // If helper text is enabled and not empty, show helper text in place of the error. + if (helperTextEnabled && !TextUtils.isEmpty(helperText)) { + captionToShow = CAPTION_STATE_HELPER_TEXT; + } else { + // Otherwise, just hide the error. + captionToShow = CAPTION_STATE_NONE; + } + } + updateCaptionViewsVisibility( + captionDisplayed, captionToShow, shouldAnimateCaptionView(errorView, null)); + } + + /** + * Check if the caption view should animate. Only animate the caption view if we're enabled, laid + * out, and have a different caption message. + * + * @param captionView The view that contains text for the caption underneath the text input area + * @param captionText The text for the caption view + * @return Whether the view should animate when setting the caption + */ + private boolean shouldAnimateCaptionView( + TextView captionView, @Nullable final CharSequence captionText) { + return ViewCompat.isLaidOut(spinnerView) + && spinnerView.isEnabled() + && (captionToShow != captionDisplayed + || captionView == null + || !TextUtils.equals(captionView.getText(), captionText)); + } + + private void updateCaptionViewsVisibility( + final @CaptionDisplayState int captionToHide, + final @CaptionDisplayState int captionToShow, + boolean animate) { + + if (animate) { + final AnimatorSet captionAnimator = new AnimatorSet(); + this.captionAnimator = captionAnimator; + List<Animator> captionAnimatorList = new ArrayList<>(); + + createCaptionAnimators( + captionAnimatorList, + helperTextEnabled, + helperTextView, + CAPTION_STATE_HELPER_TEXT, + captionToHide, + captionToShow); + + createCaptionAnimators( + captionAnimatorList, + errorEnabled, + errorView, + CAPTION_STATE_ERROR, + captionToHide, + captionToShow); + + AnimatorSetCompat.playTogether(captionAnimator, captionAnimatorList); + final TextView captionViewToHide = getCaptionViewFromDisplayState(captionToHide); + final TextView captionViewToShow = getCaptionViewFromDisplayState(captionToShow); + + captionAnimator.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animator) { + captionDisplayed = captionToShow; + IndicatorViewController.this.captionAnimator = null; + if (captionViewToHide != null) { + captionViewToHide.setVisibility(View.INVISIBLE); + if (captionToHide == CAPTION_STATE_ERROR && errorView != null) { + errorView.setText(null); + } + + if (captionViewToShow != null) { + captionViewToShow.setTranslationY(0f); + captionViewToShow.setAlpha(1f); + } + } + } + + @Override + public void onAnimationStart(Animator animator) { + if (captionViewToShow != null) { + captionViewToShow.setVisibility(VISIBLE); + } + } + }); + captionAnimator.start(); + } else { + setCaptionViewVisibilities(captionToHide, captionToShow); + } + spinnerView.updateSpinnerBackground(); + spinnerView.updateLabelState(animate); + spinnerView.updatespinnerBoxState(); + } + + private void setCaptionViewVisibilities( + @CaptionDisplayState int captionToHide, @CaptionDisplayState int captionToShow) { + if (captionToHide == captionToShow) { + return; + } + + if (captionToShow != CAPTION_STATE_NONE) { + TextView captionViewToShow = getCaptionViewFromDisplayState(captionToShow); + if (captionViewToShow != null) { + captionViewToShow.setVisibility(VISIBLE); + captionViewToShow.setAlpha(1f); + } + } + + if (captionToHide != CAPTION_STATE_NONE) { + TextView captionViewDisplayed = getCaptionViewFromDisplayState(captionToHide); + if (captionViewDisplayed != null) { + captionViewDisplayed.setVisibility(View.INVISIBLE); + // Only set the caption text to null if it's the error. + if (captionToHide == CAPTION_STATE_ERROR) { + captionViewDisplayed.setText(null); + } + } + } + captionDisplayed = captionToShow; + } + + private void createCaptionAnimators( + List<Animator> captionAnimatorList, + boolean captionEnabled, + TextView captionView, + @CaptionDisplayState int captionState, + @CaptionDisplayState int captionToHide, + @CaptionDisplayState int captionToShow) { + // If caption view is null or not enabled, do nothing. + if (captionView == null || !captionEnabled) { + return; + } + // If the caption view should be shown, set alpha to 1f. + if ((captionState == captionToShow) || (captionState == captionToHide)) { + captionAnimatorList.add( + createCaptionOpacityAnimator(captionView, captionToShow == captionState)); + if (captionToShow == captionState) { + captionAnimatorList.add(createCaptionTranslationYAnimator(captionView)); + } + } + } + + private ObjectAnimator createCaptionOpacityAnimator(TextView captionView, boolean display) { + float endValue = display ? 1f : 0f; + ObjectAnimator opacityAnimator = ObjectAnimator.ofFloat(captionView, View.ALPHA, endValue); + opacityAnimator.setDuration(CAPTION_OPACITY_FADE_ANIMATION_DURATION); + opacityAnimator.setInterpolator(AnimationUtils.INSTANCE.getLINEAR_INTERPOLATOR()); + return opacityAnimator; + } + + private ObjectAnimator createCaptionTranslationYAnimator(TextView captionView) { + ObjectAnimator translationYAnimator = + ObjectAnimator.ofFloat(captionView, TRANSLATION_Y, -captionTranslationYPx, 0f); + translationYAnimator.setDuration(CAPTION_TRANSLATE_Y_ANIMATION_DURATION); + translationYAnimator.setInterpolator(AnimationUtils.INSTANCE.getLINEAR_OUT_SLOW_IN_INTERPOLATOR()); + return translationYAnimator; + } + + private void cancelCaptionAnimator() { + if (captionAnimator != null) { + captionAnimator.cancel(); + } + } + + private boolean isCaptionView(@IndicatorIndex int index) { + return index == ERROR_INDEX || index == HELPER_INDEX; + } + + @Nullable + private TextView getCaptionViewFromDisplayState(@CaptionDisplayState int captionDisplayState) { + switch (captionDisplayState) { + case CAPTION_STATE_ERROR: + return errorView; + case CAPTION_STATE_HELPER_TEXT: + return helperTextView; + default: // No caption displayed, fall out and return null. + } + return null; + } + + void adjustIndicatorPadding() { + if (canAdjustIndicatorPadding()) { + // Add padding to the indicators so that they match the EditText + ViewCompat.setPaddingRelative( + indicatorArea, + ViewCompat.getPaddingStart(spinnerView.getSpinner()), + 0, + ViewCompat.getPaddingEnd(spinnerView.getSpinner()), + 0); + } + } + + private boolean canAdjustIndicatorPadding() { + return indicatorArea != null && spinnerView.getSpinner() != null; + } + + private void addIndicator(TextView indicator, @IndicatorIndex int index) { + if (indicatorArea == null && captionArea == null) { + indicatorArea = new LinearLayout(context); + indicatorArea.setOrientation(LinearLayout.HORIZONTAL); + spinnerView.addView(indicatorArea, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + + captionArea = new FrameLayout(context); + indicatorArea.addView( + captionArea, + -1, + new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); + + final Space spacer = new Space(context); + final LayoutParams spacerLp = new LinearLayout.LayoutParams(0, 0, 1f); + indicatorArea.addView(spacer, spacerLp); + + if (spinnerView.getSpinner() != null) { + adjustIndicatorPadding(); + } + } + + if (isCaptionView(index)) { + captionArea.setVisibility(VISIBLE); + captionArea.addView(indicator); + captionViewsAdded++; + } else { + indicatorArea.addView(indicator, index); + } + indicatorArea.setVisibility(VISIBLE); + indicatorsAdded++; + } + + private void removeIndicator(TextView indicator, @IndicatorIndex int index) { + if (indicatorArea == null) { + return; + } + + if (isCaptionView(index) && captionArea != null) { + captionViewsAdded--; + setViewGroupGoneIfEmpty(captionArea, captionViewsAdded); + captionArea.removeView(indicator); + } else { + indicatorArea.removeView(indicator); + } + indicatorsAdded--; + setViewGroupGoneIfEmpty(indicatorArea, indicatorsAdded); + } + + private void setViewGroupGoneIfEmpty(ViewGroup viewGroup, int indicatorsAdded) { + if (indicatorsAdded == 0) { + viewGroup.setVisibility(View.GONE); + } + } + + boolean isErrorEnabled() { + return errorEnabled; + } + + void setErrorEnabled(boolean enabled) { + // If the enabled state is the same as before, do nothing. + if (errorEnabled == enabled) { + return; + } + + // Otherwise, adjust enabled state. + cancelCaptionAnimator(); + + if (enabled) { + errorView = new AppCompatTextView(context); + errorView.setId(R.id.md_spinner_error); + if (typeface != null) { + errorView.setTypeface(typeface); + } + setErrorTextAppearance(errorTextAppearance); + setErrorViewTextColor(errorViewTextColor); + errorView.setVisibility(View.INVISIBLE); + ViewCompat.setAccessibilityLiveRegion(errorView, ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE); + addIndicator(errorView, ERROR_INDEX); + } else { + hideError(); + removeIndicator(errorView, ERROR_INDEX); + errorView = null; + spinnerView.updateSpinnerBackground(); + spinnerView.updatespinnerBoxState(); + } + errorEnabled = enabled; + } + + boolean isHelperTextEnabled() { + return helperTextEnabled; + } + + void setHelperTextEnabled(boolean enabled) { + // If the enabled state is the same as before, do nothing. + if (helperTextEnabled == enabled) { + return; + } + + // Otherwise, adjust enabled state. + cancelCaptionAnimator(); + + if (enabled) { + helperTextView = new AppCompatTextView(context); + helperTextView.setId(R.id.md_spinner_helper_text); + if (typeface != null) { + helperTextView.setTypeface(typeface); + } + helperTextView.setVisibility(View.INVISIBLE); + ViewCompat.setAccessibilityLiveRegion( + helperTextView, ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE); + setHelperTextAppearance(helperTextTextAppearance); + setHelperTextViewTextColor(helperTextViewTextColor); + addIndicator(helperTextView, HELPER_INDEX); + } else { + hideHelperText(); + removeIndicator(helperTextView, HELPER_INDEX); + helperTextView = null; + spinnerView.updateSpinnerBackground(); + spinnerView.updatespinnerBoxState(); + } + helperTextEnabled = enabled; + } + + boolean errorIsDisplayed() { + return isCaptionStateError(captionDisplayed); + } + + boolean errorShouldBeShown() { + return isCaptionStateError(captionToShow); + } + + private boolean isCaptionStateError(@CaptionDisplayState int captionState) { + return captionState == CAPTION_STATE_ERROR + && errorView != null + && !TextUtils.isEmpty(errorText); + } + + boolean helperTextIsDisplayed() { + return isCaptionStateHelperText(captionDisplayed); + } + + boolean helperTextShouldBeShown() { + return isCaptionStateHelperText(captionToShow); + } + + private boolean isCaptionStateHelperText(@CaptionDisplayState int captionState) { + return captionState == CAPTION_STATE_HELPER_TEXT + && helperTextView != null + && !TextUtils.isEmpty(helperText); + } + + CharSequence getErrorText() { + return errorText; + } + + CharSequence getHelperText() { + return helperText; + } + + @SuppressWarnings("ReferenceEquality") + // Matches the Typeface comparison in TextView + void setTypefaces(Typeface typeface) { + if (typeface != this.typeface) { + this.typeface = typeface; + setTextViewTypeface(errorView, typeface); + setTextViewTypeface(helperTextView, typeface); + } + } + + private void setTextViewTypeface(@Nullable TextView captionView, Typeface typeface) { + if (captionView != null) { + captionView.setTypeface(typeface); + } + } + + @ColorInt + int getErrorViewCurrentTextColor() { + return errorView != null ? errorView.getCurrentTextColor() : -1; + } + + @Nullable + ColorStateList getErrorViewTextColors() { + return errorView != null ? errorView.getTextColors() : null; + } + + void setErrorViewTextColor(ColorStateList errorViewTextColor) { + this.errorViewTextColor = errorViewTextColor; + if (errorView != null && errorViewTextColor != null) { + errorView.setTextColor(errorViewTextColor); + } + } + + void setErrorTextAppearance(@StyleRes int resId) { + this.errorTextAppearance = resId; + if (errorView != null) { + spinnerView.setTextAppearanceCompatWithErrorFallback(errorView, resId); + } + } + + @ColorInt + int getHelperTextViewCurrentTextColor() { + return helperTextView != null ? helperTextView.getCurrentTextColor() : -1; + } + + @Nullable + ColorStateList getHelperTextViewColors() { + return helperTextView != null ? helperTextView.getTextColors() : null; + } + + void setHelperTextViewTextColor(ColorStateList helperTextViewTextColor) { + this.helperTextViewTextColor = helperTextViewTextColor; + if (helperTextView != null && helperTextViewTextColor != null) { + helperTextView.setTextColor(helperTextViewTextColor); + } + } + + void setHelperTextAppearance(@StyleRes int resId) { + this.helperTextTextAppearance = resId; + if (helperTextView != null) { + TextViewCompat.setTextAppearance(helperTextView, resId); + } + } + + /** + * Values for indicator indices. Indicators are views below the text input area, like a caption + * (error text or helper text) or a character counter. + */ + @IntDef({ERROR_INDEX, HELPER_INDEX}) + @Retention(RetentionPolicy.SOURCE) + private @interface IndicatorIndex { + } + + /** + * Values for caption display state constants. There is either an error displayed, helper text + * displayed, or no caption. + */ + @IntDef({CAPTION_STATE_NONE, CAPTION_STATE_ERROR, CAPTION_STATE_HELPER_TEXT}) + @Retention(RetentionPolicy.SOURCE) + private @interface CaptionDisplayState { + } +} diff --git a/ui_spinner/src/main/java/de/kuschku/ui/spinner/MaterialSpinnerLayout.java b/ui_spinner/src/main/java/de/kuschku/ui/spinner/MaterialSpinnerLayout.java new file mode 100644 index 000000000..eff27d97c --- /dev/null +++ b/ui_spinner/src/main/java/de/kuschku/ui/spinner/MaterialSpinnerLayout.java @@ -0,0 +1,1544 @@ +/* + * Copyright (C) 2015 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 de.kuschku.ui.spinner; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.graphics.drawable.NinePatchDrawable; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.Spinner; +import android.widget.TextView; + +import org.jetbrains.annotations.NotNull; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.LinkedHashSet; + +import androidx.annotation.ColorInt; +import androidx.annotation.ColorRes; +import androidx.annotation.DimenRes; +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StyleRes; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.core.view.GravityCompat; +import androidx.core.view.ViewCompat; +import androidx.core.widget.TextViewCompat; +import androidx.customview.view.AbsSavedState; +import de.kuschku.ui.color.MaterialColors; +import de.kuschku.ui.internal.CollapsingTextHelper; +import de.kuschku.ui.internal.DescendantOffsetUtils; +import de.kuschku.ui.resources.MaterialResources; +import de.kuschku.ui.shape.MaterialShapeDrawable; +import de.kuschku.ui.shape.ShapeAppearanceModel; + +import static de.kuschku.ui.internal.ThemeEnforcement.createThemedContext; + +/** + * Layout which wraps a {@link android.widget.Spinner}, or descendant to show a floating label when + * the hint is hidden. + * + * <p>Also supports: + * + * <ul> + * <li>Showing an error via {@link #setErrorEnabled(boolean)} and {@link #setError(CharSequence)} + * <li>Showing helper text via {@link #setHelperTextEnabled(boolean)} and {@link + * #setHelperText(CharSequence)} + * </ul> + * + * <p>An example usage is as so:</p> + * + * <pre> + * <de.kuschku.ui.spinner.MaterialSpinnerLayout + * android:layout_width="match_parent" + * android:layout_height="wrap_content" + * android:hint="@string/form_username"> + * + * <android.widget.Spinner + * android:layout_width="match_parent" + * android:layout_height="wrap_content"/> + * + * </de.kuschku.ui.spinner.MaterialSpinnerLayout> + * </pre> + * + * <p><strong>Note:</strong> The actual view hierarchy present under MaterialSpinnerLayout is + * <strong>NOT</strong> guaranteed to match the view hierarchy as written in XML. As a result, calls + * to getParent() on children of the MaterialSpinnerLayout -- such as a Spinner -- may not return the + * MaterialSpinnerLayout itself, but rather an intermediate View. If you need to access a View + * directly, set an {@code android:id} and use {@link View#findViewById(int)}. + */ +public class MaterialSpinnerLayout extends LinearLayout { + + public static final int BOX_BACKGROUND_NONE = 0; + public static final int BOX_BACKGROUND_FILLED = 1; + public static final int BOX_BACKGROUND_OUTLINE = 2; + private static final int DEF_STYLE_RES = R.style.Widget_Design_MaterialSpinnerLayout; + final CollapsingTextHelper collapsingTextHelper = new CollapsingTextHelper(this); + private final FrameLayout inputFrame; + private final IndicatorViewController indicatorViewController = new IndicatorViewController(this); + private final ShapeAppearanceModel shapeAppearanceModel; + private final ShapeAppearanceModel cornerAdjustedShapeAppearanceModel; + private final int boxLabelCutoutPaddingPx; + private final int boxCollapsedPaddingTopPx; + private final int boxStrokeWidthDefaultPx; + private final int boxStrokeWidthFocusedPx; + private final Rect tmpRect = new Rect(); + private final Rect tmpBoundsRect = new Rect(); + private final RectF tmpRectF = new RectF(); + private final LinkedHashSet<OnSpinnerAttachedListener> spinnerAttachedListeners = + new LinkedHashSet<>(); + @ColorInt + private final int defaultStrokeColor; + @ColorInt + private final int hoveredStrokeColor; + @ColorInt + private final int disabledFilledBackgroundColor; + @ColorInt + private final int hoveredFilledBackgroundColor; + @ColorInt + private final int disabledColor; + Spinner spinner; + private boolean hintEnabled; + private CharSequence hint; + private boolean isProvidingHint; + private MaterialShapeDrawable boxBackground; + private MaterialShapeDrawable boxUnderline; + @BoxBackgroundMode + private int boxBackgroundMode; + private int boxStrokeWidthPx; + @ColorInt + private int boxStrokeColor; + @ColorInt + private int boxBackgroundColor; + private Typeface typeface; + private ColorStateList defaultHintTextColor; + private ColorStateList focusedTextColor; + @ColorInt + private int focusedStrokeColor; + @ColorInt + private int defaultFilledBackgroundColor; + private boolean hintAnimationEnabled; + private boolean inDrawableStateChanged; + + public MaterialSpinnerLayout(Context context) { + this(context, null); + } + + public MaterialSpinnerLayout(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, R.attr.md_materialSpinnerStyle); + } + + public MaterialSpinnerLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(createThemedContext(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(); + + setOrientation(VERTICAL); + setWillNotDraw(false); + setAddStatesFromChildren(true); + + inputFrame = new FrameLayout(context); + inputFrame.setAddStatesFromChildren(true); + addView(inputFrame); + + collapsingTextHelper.setCollapsedTextGravity(Gravity.TOP | GravityCompat.START); + + final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MaterialSpinnerLayout, defStyleAttr, DEF_STYLE_RES); + + hintEnabled = a.getBoolean(R.styleable.MaterialSpinnerLayout_md_hintEnabled, true); + setHint(a.getText(R.styleable.MaterialSpinnerLayout_android_hint)); + hintAnimationEnabled = a.getBoolean(R.styleable.MaterialSpinnerLayout_md_hintAnimationEnabled, true); + + shapeAppearanceModel = new ShapeAppearanceModel(context, attrs, defStyleAttr, DEF_STYLE_RES); + cornerAdjustedShapeAppearanceModel = new ShapeAppearanceModel(shapeAppearanceModel); + + boxLabelCutoutPaddingPx = + context + .getResources() + .getDimensionPixelOffset(R.dimen.md_mtrl_spinner_box_label_cutout_padding); + boxCollapsedPaddingTopPx = + a.getDimensionPixelOffset(R.styleable.MaterialSpinnerLayout_md_boxCollapsedPaddingTop, 0); + + boxStrokeWidthDefaultPx = + context + .getResources() + .getDimensionPixelSize(R.dimen.md_mtrl_spinner_box_stroke_width_default); + boxStrokeWidthFocusedPx = + context + .getResources() + .getDimensionPixelSize(R.dimen.md_mtrl_spinner_box_stroke_width_focused); + boxStrokeWidthPx = boxStrokeWidthDefaultPx; + + float boxCornerRadiusTopStart = + a.getDimension(R.styleable.MaterialSpinnerLayout_md_boxCornerRadiusTopStart, -1f); + float boxCornerRadiusTopEnd = + a.getDimension(R.styleable.MaterialSpinnerLayout_md_boxCornerRadiusTopEnd, -1f); + float boxCornerRadiusBottomEnd = + a.getDimension(R.styleable.MaterialSpinnerLayout_md_boxCornerRadiusBottomEnd, -1f); + float boxCornerRadiusBottomStart = + a.getDimension(R.styleable.MaterialSpinnerLayout_md_boxCornerRadiusBottomStart, -1f); + if (boxCornerRadiusTopStart >= 0) { + shapeAppearanceModel.getTopLeftCorner().setCornerSize(boxCornerRadiusTopStart); + } + if (boxCornerRadiusTopEnd >= 0) { + shapeAppearanceModel.getTopRightCorner().setCornerSize(boxCornerRadiusTopEnd); + } + if (boxCornerRadiusBottomEnd >= 0) { + shapeAppearanceModel.getBottomRightCorner().setCornerSize(boxCornerRadiusBottomEnd); + } + if (boxCornerRadiusBottomStart >= 0) { + shapeAppearanceModel.getBottomLeftCorner().setCornerSize(boxCornerRadiusBottomStart); + } + adjustCornerSizeForStrokeWidth(); + + ColorStateList filledBackgroundColorStateList = + MaterialResources.getColorStateList( + context, a, R.styleable.MaterialSpinnerLayout_md_boxBackgroundColor); + if (filledBackgroundColorStateList != null) { + defaultFilledBackgroundColor = filledBackgroundColorStateList.getDefaultColor(); + boxBackgroundColor = defaultFilledBackgroundColor; + if (filledBackgroundColorStateList.isStateful()) { + disabledFilledBackgroundColor = + filledBackgroundColorStateList.getColorForState( + new int[]{-android.R.attr.state_enabled}, -1); + hoveredFilledBackgroundColor = + filledBackgroundColorStateList.getColorForState( + new int[]{android.R.attr.state_hovered}, -1); + } else { + ColorStateList mtrlFilledBackgroundColorStateList = + AppCompatResources.getColorStateList(context, R.color.md_mtrl_filled_background_color); + disabledFilledBackgroundColor = + mtrlFilledBackgroundColorStateList.getColorForState( + new int[]{-android.R.attr.state_enabled}, -1); + hoveredFilledBackgroundColor = + mtrlFilledBackgroundColorStateList.getColorForState( + new int[]{android.R.attr.state_hovered}, -1); + } + } else { + boxBackgroundColor = Color.TRANSPARENT; + defaultFilledBackgroundColor = Color.TRANSPARENT; + disabledFilledBackgroundColor = Color.TRANSPARENT; + hoveredFilledBackgroundColor = Color.TRANSPARENT; + } + + if (a.hasValue(R.styleable.MaterialSpinnerLayout_android_textColorHint)) { + defaultHintTextColor = + focusedTextColor = a.getColorStateList(R.styleable.MaterialSpinnerLayout_android_textColorHint); + } + + ColorStateList boxStrokeColorStateList = + MaterialResources.getColorStateList(context, a, R.styleable.MaterialSpinnerLayout_md_boxStrokeColor); + if (boxStrokeColorStateList != null && boxStrokeColorStateList.isStateful()) { + defaultStrokeColor = boxStrokeColorStateList.getDefaultColor(); + disabledColor = + boxStrokeColorStateList.getColorForState(new int[]{-android.R.attr.state_enabled}, -1); + hoveredStrokeColor = + boxStrokeColorStateList.getColorForState(new int[]{android.R.attr.state_hovered}, -1); + focusedStrokeColor = + boxStrokeColorStateList.getColorForState(new int[]{android.R.attr.state_focused}, -1); + } else { + // If attribute boxStrokeColor is not a color state list but only a single value, its value + // will be applied to the box's focus state. + focusedStrokeColor = + a.getColor(R.styleable.MaterialSpinnerLayout_md_boxStrokeColor, Color.TRANSPARENT); + defaultStrokeColor = + ContextCompat.getColor(context, R.color.md_mtrl_spinner_default_box_stroke_color); + disabledColor = ContextCompat.getColor(context, R.color.md_mtrl_spinner_disabled_color); + hoveredStrokeColor = + ContextCompat.getColor(context, R.color.md_mtrl_spinner_hovered_box_stroke_color); + } + + final int hintAppearance = a.getResourceId(R.styleable.MaterialSpinnerLayout_md_hintTextAppearance, -1); + if (hintAppearance != -1) { + setHintTextAppearance(a.getResourceId(R.styleable.MaterialSpinnerLayout_md_hintTextAppearance, 0)); + } + + final int errorTextAppearance = + a.getResourceId(R.styleable.MaterialSpinnerLayout_md_errorTextAppearance, 0); + final boolean errorEnabled = a.getBoolean(R.styleable.MaterialSpinnerLayout_md_errorEnabled, false); + + final int helperTextTextAppearance = + a.getResourceId(R.styleable.MaterialSpinnerLayout_md_helperTextTextAppearance, 0); + final boolean helperTextEnabled = + a.getBoolean(R.styleable.MaterialSpinnerLayout_md_helperTextEnabled, false); + final CharSequence helperText = a.getText(R.styleable.MaterialSpinnerLayout_md_helperText); + + setHelperTextEnabled(helperTextEnabled); + setHelperText(helperText); + setHelperTextTextAppearance(helperTextTextAppearance); + setErrorEnabled(errorEnabled); + setErrorTextAppearance(errorTextAppearance); + + if (a.hasValue(R.styleable.MaterialSpinnerLayout_md_errorTextColor)) { + setErrorTextColor(a.getColorStateList(R.styleable.MaterialSpinnerLayout_md_errorTextColor)); + } + if (a.hasValue(R.styleable.MaterialSpinnerLayout_md_helperTextTextColor)) { + setHelperTextColor(a.getColorStateList(R.styleable.MaterialSpinnerLayout_md_helperTextTextColor)); + } + if (a.hasValue(R.styleable.MaterialSpinnerLayout_md_hintTextColor)) { + setHintTextColor(a.getColorStateList(R.styleable.MaterialSpinnerLayout_md_hintTextColor)); + } + + setBoxBackgroundMode( + a.getInt(R.styleable.MaterialSpinnerLayout_md_boxBackgroundMode, BOX_BACKGROUND_NONE)); + a.recycle(); + + // For accessibility, consider MaterialSpinnerLayout itself to be a simple container for a + // Spinner, and do not expose it to accessibility services. + ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO); + } + + private static void recursiveSetEnabled(final ViewGroup vg, final boolean enabled) { + for (int i = 0, count = vg.getChildCount(); i < count; i++) { + final View child = vg.getChildAt(i); + child.setEnabled(enabled); + if (child instanceof ViewGroup) { + recursiveSetEnabled((ViewGroup) child, enabled); + } + } + } + + @Override + public void addView(View child, int index, final ViewGroup.LayoutParams params) { + if (child instanceof Spinner) { + // Make sure that the Spinner is vertically at the bottom, so that it sits on the + // Spinner's underline + FrameLayout.LayoutParams flp = new FrameLayout.LayoutParams(params); + flp.gravity = Gravity.CENTER_VERTICAL | (flp.gravity & ~Gravity.VERTICAL_GRAVITY_MASK); + inputFrame.addView(child, flp); + + // Now use the Spinner's LayoutParams as our own and update them to make enough space + // for the label + inputFrame.setLayoutParams(params); + updateInputLayoutMargins(); + + setSpinner((Spinner) child); + } else { + // Carry on adding the View... + super.addView(child, index, params); + } + } + + @NonNull + private Drawable getBoxBackground() { + if (boxBackgroundMode == BOX_BACKGROUND_FILLED || boxBackgroundMode == BOX_BACKGROUND_OUTLINE) { + return boxBackground; + } + throw new IllegalStateException(); + } + + /** + * Get the box background mode (filled, outline, or none). + * + * <p>May be one of {@link #BOX_BACKGROUND_NONE}, {@link #BOX_BACKGROUND_FILLED}, or {@link + * #BOX_BACKGROUND_OUTLINE}. + */ + @BoxBackgroundMode + public int getBoxBackgroundMode() { + return boxBackgroundMode; + } + + /** + * Set the box background mode (filled, outline, or none). + * + * <p>May be one of {@link #BOX_BACKGROUND_NONE}, {@link #BOX_BACKGROUND_FILLED}, or {@link + * #BOX_BACKGROUND_OUTLINE}. + * + * <p>Note: This method defines MaterialSpinnerLayout's internal behavior (for example, it allows the + * hint to be displayed inline with the stroke in a cutout), but doesn't set all attributes that + * are set in the styles provided for the box background modes. To achieve the look of an outlined + * or filled text field, supplement this method with other methods that modify the box, such as + * {@link #setBoxStrokeColor(int)} and {@link #setBoxBackgroundColor(int)}. + * + * @param boxBackgroundMode box's background mode + * @throws IllegalArgumentException if boxBackgroundMode is not a @BoxBackgroundMode constant + */ + public void setBoxBackgroundMode(@BoxBackgroundMode int boxBackgroundMode) { + if (boxBackgroundMode == this.boxBackgroundMode) { + return; + } + this.boxBackgroundMode = boxBackgroundMode; + if (spinner != null) { + onApplyBoxBackgroundMode(); + } + } + + private void onApplyBoxBackgroundMode() { + assignBoxBackgroundByMode(); + setSpinnerBoxBackground(); + updatespinnerBoxState(); + if (boxBackgroundMode != BOX_BACKGROUND_NONE) { + updateInputLayoutMargins(); + } + } + + private void assignBoxBackgroundByMode() { + switch (boxBackgroundMode) { + case BOX_BACKGROUND_FILLED: + boxBackground = new MaterialShapeDrawable(shapeAppearanceModel); + boxUnderline = new MaterialShapeDrawable(); + break; + case BOX_BACKGROUND_OUTLINE: + if (hintEnabled && !(boxBackground instanceof CutoutDrawable)) { + boxBackground = new CutoutDrawable(shapeAppearanceModel); + } else { + boxBackground = new MaterialShapeDrawable(shapeAppearanceModel); + } + boxUnderline = null; + break; + case BOX_BACKGROUND_NONE: + boxBackground = null; + boxUnderline = null; + break; + default: + throw new IllegalArgumentException( + boxBackgroundMode + " is illegal; only @BoxBackgroundMode constants are supported."); + } + } + + private void setSpinnerBoxBackground() { + // Set the Spinner background to boxBackground if we should use that as the box background. + if (shouldUseSpinnerBackgroundForBoxBackground()) { + Object tag = spinner.getTag(R.id.md_spinner_background); + Drawable viewBackground; + if (tag instanceof Drawable) { + viewBackground = (Drawable) tag; + } else { + viewBackground = spinner.getBackground(); + spinner.setTag(R.id.md_spinner_background, viewBackground); + } + + Drawable finalBackground; + if (viewBackground != null) { + finalBackground = new LayerDrawable(new Drawable[]{ + boxBackground, + viewBackground + }); + } else { + finalBackground = boxBackground; + } + + ViewCompat.setBackground(spinner, finalBackground); + } + } + + private boolean shouldUseSpinnerBackgroundForBoxBackground() { + // When the text field's Spinner's background is null, use the Spinner's background for the + // box background. + return spinner != null + && boxBackground != null + && boxBackgroundMode != BOX_BACKGROUND_NONE; + } + + /** + * Returns the box's stroke color. + * + * @return the color used for the box's stroke + * @see #setBoxStrokeColor(int) + */ + public int getBoxStrokeColor() { + return focusedStrokeColor; + } + + /** + * Set the outline box's stroke color. + * + * <p>Calling this method when not in outline box mode will do nothing. + * + * @param boxStrokeColor the color to use for the box's stroke + * @see #getBoxStrokeColor() + */ + public void setBoxStrokeColor(@ColorInt int boxStrokeColor) { + if (focusedStrokeColor != boxStrokeColor) { + focusedStrokeColor = boxStrokeColor; + updatespinnerBoxState(); + } + } + + /** + * Set the resource used for the filled box's background color. + * + * @param boxBackgroundColorId the resource to use for the box's background color + */ + public void setBoxBackgroundColorResource(@ColorRes int boxBackgroundColorId) { + setBoxBackgroundColor(ContextCompat.getColor(getContext(), boxBackgroundColorId)); + } + + /** + * Returns the box's background color. + * + * @return the color used for the box's background + * @see #setBoxBackgroundColor(int) + */ + public int getBoxBackgroundColor() { + return boxBackgroundColor; + } + + /** + * Set the filled box's background color. + * + * @param boxBackgroundColor the color to use for the filled box's background + * @see #getBoxBackgroundColor() + */ + public void setBoxBackgroundColor(@ColorInt int boxBackgroundColor) { + if (this.boxBackgroundColor != boxBackgroundColor) { + this.boxBackgroundColor = boxBackgroundColor; + defaultFilledBackgroundColor = boxBackgroundColor; + applyBoxAttributes(); + } + } + + /** + * Set the resources used for the box's corner radii. + * + * @param boxCornerRadiusTopStartId the resource to use for the box's top start corner radius + * @param boxCornerRadiusTopEndId the resource to use for the box's top end corner radius + * @param boxCornerRadiusBottomEndId the resource to use for the box's bottom end corner radius + * @param boxCornerRadiusBottomStartId the resource to use for the box's bottom start corner + * radius + */ + public void setBoxCornerRadiiResources( + @DimenRes int boxCornerRadiusTopStartId, + @DimenRes int boxCornerRadiusTopEndId, + @DimenRes int boxCornerRadiusBottomEndId, + @DimenRes int boxCornerRadiusBottomStartId) { + setBoxCornerRadii( + getContext().getResources().getDimension(boxCornerRadiusTopStartId), + getContext().getResources().getDimension(boxCornerRadiusTopEndId), + getContext().getResources().getDimension(boxCornerRadiusBottomStartId), + getContext().getResources().getDimension(boxCornerRadiusBottomEndId)); + } + + /** + * Set the box's corner radii. + * + * @param boxCornerRadiusTopStart the value to use for the box's top start corner radius + * @param boxCornerRadiusTopEnd the value to use for the box's top end corner radius + * @param boxCornerRadiusBottomEnd the value to use for the box's bottom end corner radius + * @param boxCornerRadiusBottomStart the value to use for the box's bottom start corner radius + * @see #getBoxCornerRadiusTopStart() + * @see #getBoxCornerRadiusTopEnd() + * @see #getBoxCornerRadiusBottomEnd() + * @see #getBoxCornerRadiusBottomStart() + */ + public void setBoxCornerRadii( + float boxCornerRadiusTopStart, + float boxCornerRadiusTopEnd, + float boxCornerRadiusBottomStart, + float boxCornerRadiusBottomEnd) { + if (shapeAppearanceModel.getTopLeftCorner().getCornerSize() != boxCornerRadiusTopStart + || shapeAppearanceModel.getTopRightCorner().getCornerSize() != boxCornerRadiusTopEnd + || shapeAppearanceModel.getBottomRightCorner().getCornerSize() != boxCornerRadiusBottomEnd + || shapeAppearanceModel.getBottomLeftCorner().getCornerSize() + != boxCornerRadiusBottomStart) { + shapeAppearanceModel.getTopLeftCorner().setCornerSize(boxCornerRadiusTopStart); + shapeAppearanceModel.getTopRightCorner().setCornerSize(boxCornerRadiusTopEnd); + shapeAppearanceModel.getBottomRightCorner().setCornerSize(boxCornerRadiusBottomEnd); + shapeAppearanceModel.getBottomLeftCorner().setCornerSize(boxCornerRadiusBottomStart); + applyBoxAttributes(); + } + } + + /** + * Returns the box's top start corner radius. + * + * @return the value used for the box's top start corner radius + * @see #setBoxCornerRadii(float, float, float, float) + */ + public float getBoxCornerRadiusTopStart() { + return shapeAppearanceModel.getTopLeftCorner().getCornerSize(); + } + + /** + * Returns the box's top end corner radius. + * + * @return the value used for the box's top end corner radius + * @see #setBoxCornerRadii(float, float, float, float) + */ + public float getBoxCornerRadiusTopEnd() { + return shapeAppearanceModel.getTopRightCorner().getCornerSize(); + } + + /** + * Returns the box's bottom end corner radius. + * + * @return the value used for the box's bottom end corner radius + * @see #setBoxCornerRadii(float, float, float, float) + */ + public float getBoxCornerRadiusBottomEnd() { + return shapeAppearanceModel.getBottomLeftCorner().getCornerSize(); + } + + /** + * Returns the box's bottom start corner radius. + * + * @return the value used for the box's bottom start corner radius + * @see #setBoxCornerRadii(float, float, float, float) + */ + public float getBoxCornerRadiusBottomStart() { + return shapeAppearanceModel.getBottomRightCorner().getCornerSize(); + } + + /** + * Adjust the corner size based on the stroke width to maintain GradientDrawable's behavior. + * MaterialShapeDrawable internally adjusts the corner size so that the corner size does not + * depend on the stroke width. GradientDrawable does not account for stroke width, so this causes + * a visual diff when migrating from GradientDrawable to MaterialShapeDrawable. This method + * reverts the corner size adjustment in MaterialShapeDrawable to maintain the visual behavior + * from GradientDrawable for now. + */ + private void adjustCornerSizeForStrokeWidth() { + float strokeInset = boxBackgroundMode == BOX_BACKGROUND_OUTLINE ? boxStrokeWidthPx / 2f : 0; + if (strokeInset <= 0f) { + return; // Only adjust the corner size if there's a stroke inset. + } + + float cornerRadiusTopLeft = shapeAppearanceModel.getTopLeftCorner().getCornerSize(); + cornerAdjustedShapeAppearanceModel + .getTopLeftCorner() + .setCornerSize(cornerRadiusTopLeft + strokeInset); + + float cornerRadiusTopRight = shapeAppearanceModel.getTopRightCorner().getCornerSize(); + cornerAdjustedShapeAppearanceModel + .getTopRightCorner() + .setCornerSize(cornerRadiusTopRight + strokeInset); + + float cornerRadiusBottomRight = shapeAppearanceModel.getBottomRightCorner().getCornerSize(); + cornerAdjustedShapeAppearanceModel + .getBottomRightCorner() + .setCornerSize(cornerRadiusBottomRight + strokeInset); + + float cornerRadiusBottomLeft = shapeAppearanceModel.getBottomLeftCorner().getCornerSize(); + cornerAdjustedShapeAppearanceModel + .getBottomLeftCorner() + .setCornerSize(cornerRadiusBottomLeft + strokeInset); + + ensureCornerAdjustedShapeAppearanceModel(); + } + + private void ensureCornerAdjustedShapeAppearanceModel() { + if (boxBackgroundMode != BOX_BACKGROUND_NONE + && getBoxBackground() instanceof MaterialShapeDrawable) { + ((MaterialShapeDrawable) getBoxBackground()) + .setShapeAppearanceModel(cornerAdjustedShapeAppearanceModel); + } + } + + /** + * Returns the typeface used for the hint and any label views (such as counter and error views). + */ + @Nullable + public Typeface getTypeface() { + return typeface; + } + + /** + * Set the typeface to use for the hint and any label views (such as counter and error views). + * + * @param typeface typeface to use, or {@code null} to use the default. + */ + @SuppressWarnings("ReferenceEquality") // Matches the Typeface comparison in TextView + public void setTypeface(@Nullable Typeface typeface) { + if (typeface != this.typeface) { + this.typeface = typeface; + + collapsingTextHelper.setTypefaces(typeface); + indicatorViewController.setTypefaces(typeface); + } + } + + private void updateInputLayoutMargins() { + // Create/update the LayoutParams so that we can add enough top margin + // to the Spinner to make room for the label. + if (boxBackgroundMode != BOX_BACKGROUND_FILLED) { + final LayoutParams lp = (LayoutParams) inputFrame.getLayoutParams(); + final int newTopMargin = calculateLabelMarginTop(); + + if (newTopMargin != lp.topMargin) { + lp.topMargin = newTopMargin; + inputFrame.requestLayout(); + } + } + } + + @Override + public int getBaseline() { + if (spinner != null) { + return spinner.getBaseline() + getPaddingTop() + calculateLabelMarginTop(); + } else { + return super.getBaseline(); + } + } + + void updateLabelState(boolean animate) { + updateLabelState(animate, false); + } + + private void updateLabelState(boolean animate, boolean force) { + final boolean isEnabled = isEnabled(); + final boolean hasFocus = spinner != null && spinner.hasFocus(); + final boolean errorShouldBeShown = indicatorViewController.errorShouldBeShown(); + + // Set the expanded and collapsed labels to the default text color. + if (defaultHintTextColor != null) { + collapsingTextHelper.setCollapsedTextColor(defaultHintTextColor); + } + + // Set the collapsed and expanded label text colors based on the current state. + if (!isEnabled) { + collapsingTextHelper.setCollapsedTextColor(ColorStateList.valueOf(disabledColor)); + } else if (errorShouldBeShown) { + collapsingTextHelper.setCollapsedTextColor(indicatorViewController.getErrorViewTextColors()); + } else if (hasFocus && focusedTextColor != null) { + collapsingTextHelper.setCollapsedTextColor(focusedTextColor); + } // If none of these states apply, leave the expanded and collapsed colors as they are. + + // We should be showing the label so do so if it isn't already + if (isEnabled() && (hasFocus || errorShouldBeShown) && force) { + if (cutoutEnabled()) { + openCutout(); + } + } + } + + /** + * Returns the {@link Spinner} used for text input. + */ + @Nullable + public Spinner getSpinner() { + return spinner; + } + + private void setSpinner(Spinner spinner) { + // If we already have an Spinner, throw an exception + if (this.spinner != null) { + throw new IllegalArgumentException("We already have an Spinner, can only have one"); + } + + this.spinner = spinner; + onApplyBoxBackgroundMode(); + + final int spinnerGravity = this.spinner.getGravity(); + collapsingTextHelper.setCollapsedTextGravity( + Gravity.TOP | (spinnerGravity & ~Gravity.VERTICAL_GRAVITY_MASK)); + + updateSpinnerBackground(); + + indicatorViewController.adjustIndicatorPadding(); + dispatchOnSpinnerAttached(); + + // Update the label visibility with no animation, but force a state change + updateLabelState(false, true); + } + + private void setHintInternal(CharSequence hint) { + if (!TextUtils.equals(hint, this.hint)) { + this.hint = hint; + collapsingTextHelper.setText(hint); + // Reset the cutout to make room for a larger hint. + openCutout(); + } + } + + /** + * Returns the hint which is displayed in the floating label, if enabled. + * + * @return the hint, or null if there isn't one set, or the hint is not enabled. + */ + @Nullable + public CharSequence getHint() { + return hintEnabled ? hint : null; + } + + /** + * Set the hint to be displayed in the floating label, if enabled. + * + * @see #setHintEnabled(boolean) + */ + public void setHint(@Nullable CharSequence hint) { + if (hintEnabled) { + setHintInternal(hint); + sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + } + } + + /** + * Returns whether the floating label functionality is enabled or not in this layout. + * + * @see #setHintEnabled(boolean) + */ + public boolean isHintEnabled() { + return hintEnabled; + } + + /** + * Sets whether the floating label functionality is enabled or not in this layout. + * + * <p>If enabled, any non-empty hint in the child Spinner will be moved into the floating hint, + * and its existing hint will be cleared. If disabled, then any non-empty floating hint in this + * layout will be moved into the Spinner, and this layout's hint will be cleared. + * + * @see #setHint(CharSequence) + * @see #isHintEnabled() + */ + public void setHintEnabled(boolean enabled) { + if (enabled != hintEnabled) { + hintEnabled = enabled; + if (!hintEnabled) { + // Ensures a child Spinner provides its internal hint, not this MaterialSpinnerLayout's. + isProvidingHint = false; + // Now clear out any set hint + setHintInternal(null); + } else { + isProvidingHint = true; + } + + // Now update the Spinner top margin + if (spinner != null) { + updateInputLayoutMargins(); + } + } + } + + /** + * Returns whether or not this layout is actively managing a child {@link Spinner}'s hint. + */ + boolean isProvidingHint() { + return isProvidingHint; + } + + /** + * Sets the collapsed hint text color, size, style from the specified TextAppearance resource. + */ + public void setHintTextAppearance(@StyleRes int resId) { + collapsingTextHelper.setCollapsedTextAppearance(resId); + focusedTextColor = collapsingTextHelper.getCollapsedTextColor(); + + if (spinner != null) { + updateLabelState(false); + // Text size might have changed so update the top margin + updateInputLayoutMargins(); + } + } + + /** + * Gets the collapsed hint text color. + */ + @Nullable + public ColorStateList getHintTextColor() { + return collapsingTextHelper.getCollapsedTextColor(); + } + + /** + * Sets the collapsed hint text color from the specified ColorStateList resource. + */ + public void setHintTextColor(@Nullable ColorStateList hintTextColor) { + if (collapsingTextHelper.getCollapsedTextColor() != hintTextColor) { + collapsingTextHelper.setCollapsedTextColor(hintTextColor); + focusedTextColor = hintTextColor; + + if (spinner != null) { + updateLabelState(false); + } + } + } + + /** + * Returns the text color used by the hint in both the collapsed and expanded states, or null if + * no color has been set. + */ + @Nullable + public ColorStateList getDefaultHintTextColor() { + return defaultHintTextColor; + } + + /** + * Sets the text color used by the hint in both the collapsed and expanded states. + */ + public void setDefaultHintTextColor(@Nullable ColorStateList textColor) { + defaultHintTextColor = textColor; + focusedTextColor = textColor; + + if (spinner != null) { + updateLabelState(false); + } + } + + /** + * Sets the text color and size for the error message from the specified TextAppearance resource. + */ + public void setErrorTextAppearance(@StyleRes int errorTextAppearance) { + indicatorViewController.setErrorTextAppearance(errorTextAppearance); + } + + /** + * Sets the text color used by the error message in all states. + */ + public void setErrorTextColor(@Nullable ColorStateList errorTextColor) { + indicatorViewController.setErrorViewTextColor(errorTextColor); + } + + /** + * Returns the text color used by the error message in current state. + */ + @ColorInt + public int getErrorCurrentTextColors() { + return indicatorViewController.getErrorViewCurrentTextColor(); + } + + /** + * Sets the text color and size for the helper text from the specified TextAppearance resource. + */ + public void setHelperTextTextAppearance(@StyleRes int helperTextTextAppearance) { + indicatorViewController.setHelperTextAppearance(helperTextTextAppearance); + } + + /** + * Sets the text color used by the helper text in all states. + */ + public void setHelperTextColor(@Nullable ColorStateList helperTextColor) { + indicatorViewController.setHelperTextViewTextColor(helperTextColor); + } + + /** + * Returns whether the error functionality is enabled or not in this layout. + * + * @see #setErrorEnabled(boolean) + */ + public boolean isErrorEnabled() { + return indicatorViewController.isErrorEnabled(); + } + + /** + * Whether the error functionality is enabled or not in this layout. Enabling this functionality + * before setting an error message via {@link #setError(CharSequence)}, will mean that this layout + * will not change size when an error is displayed. + */ + public void setErrorEnabled(boolean enabled) { + indicatorViewController.setErrorEnabled(enabled); + } + + /** + * Returns whether the helper text functionality is enabled or not in this layout. + * + * @see #setHelperTextEnabled(boolean) + */ + public boolean isHelperTextEnabled() { + return indicatorViewController.isHelperTextEnabled(); + } + + /** + * Whether the helper text functionality is enabled or not in this layout. Enabling this + * functionality before setting a helper message via {@link #setHelperText(CharSequence)} will + * mean that this layout will not change size when a helper message is displayed. + */ + public void setHelperTextEnabled(boolean enabled) { + indicatorViewController.setHelperTextEnabled(enabled); + } + + /** + * Returns the text color used by the helper text in the current states. + */ + @ColorInt + public int getHelperTextCurrentTextColor() { + return indicatorViewController.getHelperTextViewCurrentTextColor(); + } + + @Override + public void setEnabled(boolean enabled) { + // Since we're set to addStatesFromChildren, we need to make sure that we set all + // children to enabled/disabled otherwise any enabled children will wipe out our disabled + // drawable state + recursiveSetEnabled(this, enabled); + super.setEnabled(enabled); + } + + void setTextAppearanceCompatWithErrorFallback(TextView textView, @StyleRes int textAppearance) { + boolean useDefaultColor = false; + try { + TextViewCompat.setTextAppearance(textView, textAppearance); + + if (VERSION.SDK_INT >= VERSION_CODES.M + && textView.getTextColors().getDefaultColor() == Color.MAGENTA) { + // Caused by our theme not extending from Theme.Design*. On API 23 and + // above, unresolved theme attrs result in MAGENTA rather than an exception. + // Flag so that we use a decent default + useDefaultColor = true; + } + } catch (Exception e) { + // Caused by our theme not extending from Theme.Design*. Flag so that we use + // a decent default + useDefaultColor = true; + } + if (useDefaultColor) { + // Probably caused by our theme not extending from Theme.Design*. Instead + // we manually set something appropriate + TextViewCompat.setTextAppearance(textView, R.style.TextAppearance_AppCompat_Caption); + textView.setTextColor(ContextCompat.getColor(getContext(), R.color.md_design_error)); + } + } + + private int calculateLabelMarginTop() { + if (!hintEnabled) { + return 0; + } + + switch (boxBackgroundMode) { + case BOX_BACKGROUND_OUTLINE: + return (int) (collapsingTextHelper.getCollapsedTextHeight() / 2); + case BOX_BACKGROUND_FILLED: + case BOX_BACKGROUND_NONE: + return (int) collapsingTextHelper.getCollapsedTextHeight(); + default: + return 0; + } + } + + private Rect calculateCollapsedTextBounds(Rect rect) { + if (spinner == null) { + throw new IllegalStateException(); + } + Rect bounds = tmpBoundsRect; + + bounds.bottom = rect.bottom; + switch (boxBackgroundMode) { + case BOX_BACKGROUND_OUTLINE: + bounds.left = rect.left + spinner.getPaddingLeft(); + bounds.top = rect.top - calculateLabelMarginTop(); + bounds.right = rect.right - spinner.getPaddingRight(); + return bounds; + case BOX_BACKGROUND_FILLED: + bounds.left = rect.left + spinner.getPaddingLeft(); + bounds.top = rect.top + boxCollapsedPaddingTopPx; + bounds.right = rect.right - spinner.getPaddingRight(); + return bounds; + default: + bounds.left = rect.left + spinner.getPaddingLeft(); + bounds.top = getPaddingTop(); + bounds.right = rect.right - spinner.getPaddingRight(); + return bounds; + } + } + + /* + * Calculates the box background color that should be set. + * + * The filled text field has a surface layer with value {@code ?attr/colorSurface} underneath its + * background that is taken into account when calculating the background color. + */ + private int calculateBoxBackgroundColor() { + int backgroundColor = boxBackgroundColor; + if (boxBackgroundMode == BOX_BACKGROUND_FILLED) { + int surfaceLayerColor = MaterialColors.INSTANCE.getColor(this, R.attr.colorSurface, Color.TRANSPARENT); + backgroundColor = MaterialColors.INSTANCE.layer(surfaceLayerColor, boxBackgroundColor); + } + return backgroundColor; + } + + private void applyBoxAttributes() { + if (boxBackground == null) { + return; + } + + if (canDrawOutlineStroke()) { + boxBackground.setStroke(boxStrokeWidthPx, boxStrokeColor); + } + + boxBackground.setFillColor(ColorStateList.valueOf(calculateBoxBackgroundColor())); + applyBoxUnderlineAttributes(); + invalidate(); + } + + private void applyBoxUnderlineAttributes() { + // Exit if the underline is not being drawn by MaterialSpinnerLayout. + if (boxUnderline == null) { + return; + } + + if (canDrawStroke()) { + boxUnderline.setFillColor(ColorStateList.valueOf(boxStrokeColor)); + } + invalidate(); + } + + private boolean canDrawOutlineStroke() { + return boxBackgroundMode == BOX_BACKGROUND_OUTLINE && canDrawStroke(); + } + + private boolean canDrawStroke() { + return boxStrokeWidthPx > -1 && boxStrokeColor != Color.TRANSPARENT; + } + + void updateSpinnerBackground() { + // Only update the color filter for the legacy text field, since we can directly change the + // Paint colors of the MaterialShapeDrawable box background without having to use color filters. + if (spinner == null || boxBackgroundMode != BOX_BACKGROUND_NONE) { + return; + } + + Drawable spinnerBackground = spinner.getBackground(); + if (spinnerBackground == null) { + return; + } + + spinnerBackground = spinnerBackground.mutate(); + + if (indicatorViewController.errorShouldBeShown()) { + // Set a color filter for the error color + spinnerBackground.setColorFilter( + new PorterDuffColorFilter(indicatorViewController.getErrorViewCurrentTextColor(), PorterDuff.Mode.SRC_IN) + ); + } else { + // Else reset the color filter and refresh the drawable state so that the + // normal tint is used + DrawableCompat.clearColorFilter(spinnerBackground); + spinner.refreshDrawableState(); + } + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + if (indicatorViewController.errorShouldBeShown()) { + ss.error = getError(); + } + return ss; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (!(state instanceof SavedState)) { + super.onRestoreInstanceState(state); + return; + } + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + setError(ss.error); + requestLayout(); + } + + /** + * Returns the error message that was set to be displayed with {@link #setError(CharSequence)}, or + * <code>null</code> if no error was set or if error displaying is not enabled. + * + * @see #setError(CharSequence) + */ + @Nullable + public CharSequence getError() { + return indicatorViewController.isErrorEnabled() ? indicatorViewController.getErrorText() : null; + } + + /** + * Sets an error message that will be displayed below our {@link Spinner}. If the {@code error} is + * {@code null}, the error message will be cleared. + * + * <p>If the error functionality has not been enabled via {@link #setErrorEnabled(boolean)}, then + * it will be automatically enabled if {@code error} is not empty. + * + * @param errorText Error message to display, or null to clear + * @see #getError() + */ + public void setError(@Nullable final CharSequence errorText) { + if (!indicatorViewController.isErrorEnabled()) { + if (TextUtils.isEmpty(errorText)) { + // If error isn't enabled, and the error is empty, just return + return; + } + // Else, we'll assume that they want to enable the error functionality + setErrorEnabled(true); + } + + if (!TextUtils.isEmpty(errorText)) { + indicatorViewController.showError(errorText); + } else { + indicatorViewController.hideError(); + } + } + + /** + * Returns the helper message that was set to be displayed with {@link + * #setHelperText(CharSequence)}, or <code>null</code> if no helper text was set or if helper text + * functionality is not enabled. + * + * @see #setHelperText(CharSequence) + */ + @Nullable + public CharSequence getHelperText() { + return indicatorViewController.isHelperTextEnabled() + ? indicatorViewController.getHelperText() + : null; + } + + /** + * Sets a helper message that will be displayed below the {@link Spinner}. If the {@code helper} + * is {@code null}, the helper text functionality will be disabled and the helper message will be + * hidden. + * + * <p>If the helper text functionality has not been enabled via {@link + * #setHelperTextEnabled(boolean)}, then it will be automatically enabled if {@code helper} is not + * empty. + * + * @param helperText Helper text to display + * @see #getHelperText() + */ + public void setHelperText(@Nullable final CharSequence helperText) { + // If helper text is null, disable helper if it's enabled. + if (TextUtils.isEmpty(helperText)) { + if (isHelperTextEnabled()) { + setHelperTextEnabled(false); + } + } else { + if (!isHelperTextEnabled()) { + setHelperTextEnabled(true); + } + indicatorViewController.showHelper(helperText); + } + } + + /** + * Returns whether any hint state changes, due to being focused or non-empty text, are animated. + * + * @see #setHintAnimationEnabled(boolean) + */ + public boolean isHintAnimationEnabled() { + return hintAnimationEnabled; + } + + /** + * Set whether any hint state changes, due to being focused or non-empty text, are animated. + * + * @see #isHintAnimationEnabled() + */ + public void setHintAnimationEnabled(boolean enabled) { + hintAnimationEnabled = enabled; + } + + /** + * Add a {@link OnSpinnerAttachedListener} that will be invoked when the edit text is attached, + * or from this method if the Spinner is already present. + * + * <p>Components that add a listener should take care to remove it when finished via {@link + * #removeOnSpinnerAttachedListener(OnSpinnerAttachedListener)}. + * + * @param listener listener to add + */ + public void addOnSpinnerAttachedListener(OnSpinnerAttachedListener listener) { + spinnerAttachedListeners.add(listener); + if (spinner != null) { + listener.onSpinnerAttached(); + } + } + + /** + * Remove the given {@link OnSpinnerAttachedListener} that was previously added via {@link + * #addOnSpinnerAttachedListener(OnSpinnerAttachedListener)}. + * + * @param listener listener to remove + */ + public void removeOnSpinnerAttachedListener(OnSpinnerAttachedListener listener) { + spinnerAttachedListeners.remove(listener); + } + + /** + * Remove all previously added {@link OnSpinnerAttachedListener}s. + */ + public void clearOnSpinnerAttachedListeners() { + spinnerAttachedListeners.clear(); + } + + private void dispatchOnSpinnerAttached() { + for (OnSpinnerAttachedListener listener : spinnerAttachedListeners) { + listener.onSpinnerAttached(); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + if (spinner != null) { + Rect rect = tmpRect; + DescendantOffsetUtils.getDescendantRect(this, spinner, rect); + updateBoxUnderlineBounds(rect); + + if (hintEnabled) { + collapsingTextHelper.setCollapsedBounds(calculateCollapsedTextBounds(rect)); + collapsingTextHelper.recalculate(); + + // If the label should be collapsed, set the cutout bounds on the CutoutDrawable to make + // sure it draws with a cutout in draw(). + if (cutoutEnabled()) { + openCutout(); + } + } + } + } + + private void updateBoxUnderlineBounds(Rect bounds) { + if (boxUnderline != null) { + int top = bounds.bottom - boxStrokeWidthFocusedPx; + boxUnderline.setBounds(bounds.left, top, bounds.right, bounds.bottom); + } + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + drawHint(canvas); + drawBoxUnderline(canvas); + } + + private void drawHint(Canvas canvas) { + if (hintEnabled) { + collapsingTextHelper.draw(canvas); + } + } + + private void drawBoxUnderline(Canvas canvas) { + if (boxUnderline != null) { + // Draw using the current boxStrokeWidth. + Rect underlineBounds = boxUnderline.getBounds(); + underlineBounds.top = underlineBounds.bottom - boxStrokeWidthPx; + boxUnderline.draw(canvas); + } + } + + private boolean cutoutEnabled() { + return hintEnabled && !TextUtils.isEmpty(hint) && boxBackground instanceof CutoutDrawable; + } + + private void openCutout() { + if (!cutoutEnabled()) { + return; + } + final RectF cutoutBounds = tmpRectF; + collapsingTextHelper.getCollapsedTextActualBounds(cutoutBounds); + applyCutoutPadding(cutoutBounds); + // Offset the cutout bounds by the MaterialSpinnerLayout's left padding to ensure that the cutout is + // inset relative to the MaterialSpinnerLayout's bounds. + cutoutBounds.offset(-getPaddingLeft(), 0); + ((CutoutDrawable) boxBackground).setCutout(cutoutBounds); + } + + private void closeCutout() { + if (cutoutEnabled()) { + ((CutoutDrawable) boxBackground).removeCutout(); + } + } + + private void applyCutoutPadding(RectF cutoutBounds) { + cutoutBounds.left -= boxLabelCutoutPaddingPx; + cutoutBounds.top -= boxLabelCutoutPaddingPx; + cutoutBounds.right += boxLabelCutoutPaddingPx; + cutoutBounds.bottom += boxLabelCutoutPaddingPx; + } + + @VisibleForTesting + boolean cutoutIsOpen() { + return cutoutEnabled() && ((CutoutDrawable) boxBackground).hasCutout(); + } + + @Override + protected void drawableStateChanged() { + if (inDrawableStateChanged) { + // Some of the calls below will update the drawable state of child views. Since we're + // using addStatesFromChildren we can get into infinite recursion, hence we'll just + // exit in this instance + return; + } + + inDrawableStateChanged = true; + + super.drawableStateChanged(); + + final int[] state = getDrawableState(); + boolean changed = collapsingTextHelper.setState(state); + + // Drawable state has changed so see if we need to update the label + updateLabelState(ViewCompat.isLaidOut(this) && isEnabled()); + updateSpinnerBackground(); + updatespinnerBoxState(); + + if (changed) { + invalidate(); + } + + inDrawableStateChanged = false; + } + + void updatespinnerBoxState() { + if (boxBackground == null || boxBackgroundMode == BOX_BACKGROUND_NONE) { + return; + } + + final boolean hasFocus = isFocused() || (spinner != null && spinner.hasFocus()); + final boolean isHovered = isHovered() || (spinner != null && spinner.isHovered()); + + // Update the text box's stroke color based on the current state. + if (!isEnabled()) { + boxStrokeColor = disabledColor; + } else if (indicatorViewController.errorShouldBeShown()) { + boxStrokeColor = indicatorViewController.getErrorViewCurrentTextColor(); + } else if (hasFocus) { + boxStrokeColor = focusedStrokeColor; + } else if (isHovered) { + boxStrokeColor = hoveredStrokeColor; + } else { + boxStrokeColor = defaultStrokeColor; + } + + // Update the text box's stroke width based on the current state. + if ((isHovered || hasFocus) && isEnabled()) { + boxStrokeWidthPx = boxStrokeWidthFocusedPx; + adjustCornerSizeForStrokeWidth(); + } else { + boxStrokeWidthPx = boxStrokeWidthDefaultPx; + adjustCornerSizeForStrokeWidth(); + } + + // Update the text box's background color based on the current state. + if (boxBackgroundMode == BOX_BACKGROUND_FILLED) { + if (!isEnabled()) { + boxBackgroundColor = disabledFilledBackgroundColor; + } else if (isHovered) { + boxBackgroundColor = hoveredFilledBackgroundColor; + } else { + boxBackgroundColor = defaultFilledBackgroundColor; + } + } + + applyBoxAttributes(); + } + + @VisibleForTesting + final boolean isHelperTextDisplayed() { + return indicatorViewController.helperTextIsDisplayed(); + } + + @VisibleForTesting + final int getHintCurrentCollapsedTextColor() { + return collapsingTextHelper.getCurrentCollapsedTextColor(); + } + + @VisibleForTesting + final float getHintCollapsedTextHeight() { + return collapsingTextHelper.getCollapsedTextHeight(); + } + + @VisibleForTesting + final int getErrorTextCurrentColor() { + return indicatorViewController.getErrorViewCurrentTextColor(); + } + + /** + * Values for box background mode. There is either a filled background, an outline background, or + * no background. + */ + @IntDef({BOX_BACKGROUND_NONE, BOX_BACKGROUND_FILLED, BOX_BACKGROUND_OUTLINE}) + @Retention(RetentionPolicy.SOURCE) + public @interface BoxBackgroundMode { + } + + /** + * Callback interface invoked when the view's {@link Spinner} is attached, or from {@link + * #addOnSpinnerAttachedListener(OnSpinnerAttachedListener)} if the edit text is already present. + * + * @see #addOnSpinnerAttachedListener(OnSpinnerAttachedListener) + */ + public interface OnSpinnerAttachedListener { + + /** + * Called when the {@link Spinner} is attached, or from {@link + * #addOnSpinnerAttachedListener(OnSpinnerAttachedListener)} if the edit text is already + * present. + */ + void onSpinnerAttached(); + } + + static class SavedState extends AbsSavedState { + public static final Creator<SavedState> CREATOR = + new ClassLoaderCreator<SavedState>() { + @Override + public SavedState createFromParcel(Parcel in, ClassLoader loader) { + return new SavedState(in, loader); + } + + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in, null); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + CharSequence error; + + SavedState(Parcelable superState) { + super(superState); + } + + SavedState(Parcel source, ClassLoader loader) { + super(source, loader); + error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + TextUtils.writeToParcel(error, dest, flags); + } + + @NotNull + @Override + public String toString() { + return "MaterialSpinnerLayout.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " error=" + + error + + "}"; + } + } +} diff --git a/ui_spinner/src/main/res/color/md_design_box_stroke_color.xml b/ui_spinner/src/main/res/color/md_design_box_stroke_color.xml new file mode 100644 index 000000000..b1a9a1b95 --- /dev/null +++ b/ui_spinner/src/main/res/color/md_design_box_stroke_color.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2018 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. +--> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="@color/md_mtrl_spinner_focused_box_stroke_color" android:state_focused="true"/> + <item android:color="@color/md_mtrl_spinner_hovered_box_stroke_color" android:state_hovered="true"/> + <item android:color="@color/md_mtrl_spinner_disabled_color" android:state_enabled="false"/> + <item android:color="@color/md_mtrl_spinner_default_box_stroke_color"/> +</selector> diff --git a/ui_spinner/src/main/res/color/md_design_error.xml b/ui_spinner/src/main/res/color/md_design_error.xml new file mode 100644 index 000000000..0a505fc72 --- /dev/null +++ b/ui_spinner/src/main/res/color/md_design_error.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2016 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. +--> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="?android:attr/textColorTertiary" android:state_enabled="false"/> + <item android:color="?attr/colorError"/> +</selector> diff --git a/ui_spinner/src/main/res/color/md_mtrl_error.xml b/ui_spinner/src/main/res/color/md_mtrl_error.xml new file mode 100644 index 000000000..d06940918 --- /dev/null +++ b/ui_spinner/src/main/res/color/md_mtrl_error.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2018 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. +--> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="?attr/colorOnError" android:state_enabled="false"/> + <item android:color="?attr/colorError"/> +</selector> diff --git a/ui_spinner/src/main/res/color/md_mtrl_filled_background_color.xml b/ui_spinner/src/main/res/color/md_mtrl_filled_background_color.xml new file mode 100644 index 000000000..903a1aada --- /dev/null +++ b/ui_spinner/src/main/res/color/md_mtrl_filled_background_color.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2018 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. +--> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:alpha="0.16" android:color="?attr/colorOnSurface" android:state_hovered="true"/> + <item android:alpha="0.04" android:color="?attr/colorOnSurface" android:state_enabled="false"/> + <item android:alpha="0.12" android:color="?attr/colorOnSurface"/> +</selector> diff --git a/ui_spinner/src/main/res/color/md_mtrl_filled_stroke_color.xml b/ui_spinner/src/main/res/color/md_mtrl_filled_stroke_color.xml new file mode 100644 index 000000000..802d8328f --- /dev/null +++ b/ui_spinner/src/main/res/color/md_mtrl_filled_stroke_color.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2018 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 + ~ + ~ https://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. + --> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="?attr/colorPrimary" android:state_focused="true"/> + <!-- 4% overlay over 42% colorOnSurface --> + <item android:alpha="0.46" android:color="?attr/colorOnSurface" android:state_hovered="true"/> + <item android:alpha="0.38" android:color="?attr/colorOnSurface" android:state_enabled="false"/> + <item android:alpha="0.42" android:color="?attr/colorOnSurface"/> +</selector> diff --git a/ui_spinner/src/main/res/color/md_mtrl_indicator_text_color.xml b/ui_spinner/src/main/res/color/md_mtrl_indicator_text_color.xml new file mode 100644 index 000000000..566e09455 --- /dev/null +++ b/ui_spinner/src/main/res/color/md_mtrl_indicator_text_color.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2018 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. +--> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:alpha="0.38" android:color="?attr/colorOnSurface" android:state_enabled="false"/> + <item android:alpha="0.6" android:color="?attr/colorOnSurface"/> +</selector> diff --git a/ui_spinner/src/main/res/color/md_mtrl_outlined_stroke_color.xml b/ui_spinner/src/main/res/color/md_mtrl_outlined_stroke_color.xml new file mode 100644 index 000000000..b687332a0 --- /dev/null +++ b/ui_spinner/src/main/res/color/md_mtrl_outlined_stroke_color.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2018 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. +--> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="?attr/colorPrimary" android:state_focused="true"/> + <item android:alpha="0.87" android:color="?attr/colorOnSurface" android:state_hovered="true"/> + <item android:alpha="0.12" android:color="?attr/colorOnSurface" android:state_enabled="false"/> + <item android:alpha="0.38" android:color="?attr/colorOnSurface"/> +</selector> diff --git a/ui_spinner/src/main/res/values/_attrs.xml b/ui_spinner/src/main/res/values/_attrs.xml new file mode 100644 index 000000000..52a536002 --- /dev/null +++ b/ui_spinner/src/main/res/values/_attrs.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2017 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. +--> +<resources> + <declare-styleable name="md_TextAppearance"> + <attr name="android:textSize" /> + <attr name="android:textColor" /> + </declare-styleable> +</resources> diff --git a/ui_spinner/src/main/res/values/color_attrs.xml b/ui_spinner/src/main/res/values/color_attrs.xml new file mode 100644 index 000000000..abb45ca87 --- /dev/null +++ b/ui_spinner/src/main/res/values/color_attrs.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2017 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. +--> +<resources> + <!-- A tonal variation of the primary color. --> + <attr name="colorPrimaryVariant" format="color"/> + <!-- The secondary branding color for the app, usually a bright complement to the primary + branding color. --> + <attr name="colorSecondary" format="color"/> + <!-- A tonal variation of the secondary color. --> + <attr name="colorSecondaryVariant" format="color"/> + <!-- The color of surfaces such as cards, sheets, menus. --> + <attr name="colorSurface" format="color"/> + <!-- A color that passes accessibility guidelines for text/iconography when drawn on top of + primary. --> + <attr name="colorOnPrimary" format="color"/> + <!-- A color that passes accessibility guidelines for text/iconography when drawn on top of + secondary. --> + <attr name="colorOnSecondary" format="color"/> + <!-- A color that passes accessibility guidelines for text/iconography when drawn on top of + background. --> + <attr name="colorOnBackground"/> + <!-- A color that passes accessibility guidelines for text/iconography when drawn on top of + error. --> + <attr name="colorOnError" format="color"/> + <!-- A color that passes accessibility guidelines for text/iconography when drawn on top of + surface. --> + <attr name="colorOnSurface" format="color"/> + + <!-- Private color attributes that help facilitate switching these pre-23 compatibility + values for the light and dark themes. --> + <attr name="colorOnPrimaryDisabled" format="color"/> + <attr name="colorOnPrimaryEmphasisHighType" format="color"/> + <attr name="colorOnPrimaryEmphasisMedium" format="color"/> + <attr name="colorOnSurfaceDisabled" format="color"/> + <attr name="colorOnSurfaceEmphasisHighType" format="color"/> + <attr name="colorOnSurfaceEmphasisMedium" format="color"/> + + <!-- The scrim background that appears below modals and expanded navigation menus. + The background can either be a color or a bitmap drawable with tileMode set to repeat. --> + <attr name="scrimBackground" format="color|reference"/> + + <!-- Internal flag used to denote that a theme is a Theme.MaterialComponents theme or a + Theme.MaterialComponents.Bridge theme. --> + <attr name="isMaterialTheme" format="boolean"/> + + <!-- When set to true, the material selection controls will tint themselves according to + Material Theme colors. When set to false, Material Theme colors will + be ignored. This value should be set to false when using custom drawables + that should not be tinted. This value is ignored if a buttonTint is set. + Set this attribute on your styles for each selection control.--> + <attr name="useMaterialThemeColors" format="boolean"/> + +</resources> diff --git a/ui_spinner/src/main/res/values/internal_attrs.xml b/ui_spinner/src/main/res/values/internal_attrs.xml new file mode 100644 index 000000000..a93bbc03b --- /dev/null +++ b/ui_spinner/src/main/res/values/internal_attrs.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2017 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. +--> +<resources> + + <attr name="materialThemeOverlay" format="reference"/> + + <declare-styleable name="ThemeEnforcement"> + <!-- Internal flag used to denote that a style uses new attributes defined by + Theme.MaterialComponents, and that the component should check via ThemeEnforcement that the + client's app theme inherits from Theme.MaterialComponents. + + Not all usages of new attributes are problematic in the context of a legacy app theme. You + should only use this flag if a particular usage is known to cause a visual glitch or crash. + For example, tinting a vector drawable with a non-existent theme attribute is known to + crash on pre-21 devices. --> + <attr name="md_enforceMaterialTheme" format="boolean"/> + <!-- Internal flag used to denote that a style requires that the textAppearance attribute is + specified and evaluates to a valid text appearance. --> + <attr name="md_enforceTextAppearance" format="boolean"/> + <!-- Attribute used to check that a component has a TextAppearance specified on it. --> + <attr name="android:textAppearance"/> + </declare-styleable> + + <declare-styleable name="ForegroundLinearLayout"> + <attr name="android:foreground"/> + <attr name="android:foregroundGravity"/> + <attr name="md_foregroundInsidePadding" format="boolean"/> + </declare-styleable> + + <declare-styleable name="ScrimInsetsFrameLayout"> + <attr name="md_insetForeground" format="color|reference"/> + </declare-styleable> + + <declare-styleable name="FlowLayout"> + <!-- Horizontal spacing between two items being laid out. --> + <attr name="md_itemSpacing" format="dimension"/> + <!-- Vertical Spacing between two lines of items being laid out. --> + <attr name="md_lineSpacing" format="dimension"/> + </declare-styleable> + +</resources> diff --git a/ui_spinner/src/main/res/values/internal_dimens.xml b/ui_spinner/src/main/res/values/internal_dimens.xml new file mode 100644 index 000000000..1934c28e0 --- /dev/null +++ b/ui_spinner/src/main/res/values/internal_dimens.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2017 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. +--> +<resources> + + <dimen name="md_design_navigation_icon_size">24dp</dimen> + <dimen name="md_design_navigation_icon_padding">32dp</dimen> + <dimen name="md_design_navigation_separator_vertical_padding">8dp</dimen> + <dimen name="md_design_navigation_padding_bottom">8dp</dimen> + +</resources> diff --git a/ui_spinner/src/main/res/values/shape_attrs.xml b/ui_spinner/src/main/res/values/shape_attrs.xml new file mode 100644 index 000000000..25e0903df --- /dev/null +++ b/ui_spinner/src/main/res/values/shape_attrs.xml @@ -0,0 +1,73 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2018 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 + ~ + ~ https://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. + --> + +<resources> + + <declare-styleable name="MaterialShape"> + <!-- Shape appearance style reference to be used to construct a ShapeAppearanceModel. --> + <attr name="md_shapeAppearance" format="reference"/> + <!-- Shape appearance overlay style reference to be used to modify the shapeAppearance. --> + <attr name="md_shapeAppearanceOverlay" format="reference"/> + </declare-styleable> + + <declare-styleable name="ShapeAppearance"> + <!-- Corner size to be used in the ShapeAppearance. All corners default to this value --> + <attr name="md_cornerSize" format="dimension"/> + <!-- Top left corner size to be used in the ShapeAppearance. --> + <attr name="md_cornerSizeTopLeft" format="dimension"/> + <!-- Top right corner size to be used in the ShapeAppearance. --> + <attr name="md_cornerSizeTopRight" format="dimension"/> + <!-- Bottom right corner size to be used in the ShapeAppearance. --> + <attr name="md_cornerSizeBottomRight" format="dimension"/> + <!-- Bottom left corner size to be used in the ShapeAppearance. --> + <attr name="md_cornerSizeBottomLeft" format="dimension"/> + + <!-- Corner family to be used in the ShapeAppearance. All corners default to this value --> + <attr name="md_cornerFamily" format="enum"> + <enum name="md_rounded" value="0"/> + <enum name="md_cut" value="1"/> + </attr> + <!-- Top left corner family to be used in the ShapeAppearance. --> + <attr name="md_cornerFamilyTopLeft" format="enum"> + <enum name="md_rounded" value="0"/> + <enum name="md_cut" value="1"/> + </attr> + <!-- Top right corner family to be used in the ShapeAppearance. --> + <attr name="md_cornerFamilyTopRight" format="enum"> + <enum name="md_rounded" value="0"/> + <enum name="md_cut" value="1"/> + </attr> + <!-- Bottom right corner family to be used in the ShapeAppearance. --> + <attr name="md_cornerFamilyBottomRight" format="enum"> + <enum name="md_rounded" value="0"/> + <enum name="md_cut" value="1"/> + </attr> + <!-- Bottom left corner family to be used in the ShapeAppearance. --> + <attr name="md_cornerFamilyBottomLeft" format="enum"> + <enum name="md_rounded" value="0"/> + <enum name="md_cut" value="1"/> + </attr> + </declare-styleable> + + <!-- Shape appearance style reference for small components. --> + <attr name="md_shapeAppearanceSmallComponent" format="reference"/> + <!-- Shape appearance style reference for medium components. --> + <attr name="md_shapeAppearanceMediumComponent" format="reference"/> + <!-- Shape appearance style reference for large components. --> + <attr name="md_shapeAppearanceLargeComponent" format="reference"/> + +</resources> diff --git a/ui_spinner/src/main/res/values/shape_dimens.xml b/ui_spinner/src/main/res/values/shape_dimens.xml new file mode 100644 index 000000000..87e0d2ba1 --- /dev/null +++ b/ui_spinner/src/main/res/values/shape_dimens.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2018 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 + ~ + ~ https://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. + --> + +<resources> + +</resources> diff --git a/ui_spinner/src/main/res/values/spinner_attrs.xml b/ui_spinner/src/main/res/values/spinner_attrs.xml new file mode 100644 index 000000000..789eee705 --- /dev/null +++ b/ui_spinner/src/main/res/values/spinner_attrs.xml @@ -0,0 +1,91 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2018 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 + ~ + ~ https://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. + --> +<resources> + + <!-- Style to use for spinnerLayout in the theme. --> + <attr name="md_materialSpinnerStyle" format="reference"/> + + <declare-styleable name="MaterialSpinnerLayout"> + <!-- The text color for input text. --> + <attr name="android:textColorHint"/> + + <!-- The hint to display in the floating label. --> + <attr name="android:hint"/> + <!-- Whether the layout's floating label functionality is enabled. --> + <attr name="md_hintEnabled" format="boolean"/> + <!-- Whether to animate hint state changes. --> + <attr name="md_hintAnimationEnabled" format="boolean"/> + <!-- TextAppearance of the hint in the collapsed floating label. --> + <attr name="md_hintTextAppearance" format="reference"/> + <!-- Text color of the hint in the collapsed floating label. + If set, this takes precedence over hintTextAppearance. --> + <attr name="md_hintTextColor" format="color"/> + + <!-- The text to display as helper text underneath the text input area. --> + <attr name="md_helperText" format="string"/> + <!-- Whether the layout's helper text functionality is enabled. --> + <attr name="md_helperTextEnabled" format="boolean"/> + <!-- TextAppearance of the helper text displayed underneath the text input area. --> + <attr name="md_helperTextTextAppearance" format="reference"/> + <!-- Text color of the helper text displayed underneath the text input area. + If set, this takes precedence over helperTextTextAppearance. --> + <attr name="md_helperTextTextColor" format="color"/> + + <!-- Whether the layout is laid out as if an error will be displayed. --> + <attr name="md_errorEnabled" format="boolean"/> + <!-- TextAppearance of any error message displayed. --> + <attr name="md_errorTextAppearance" format="reference"/> + <!-- Text color for any error message displayed. + If set, this takes precedence over errorTextAppearance. --> + <attr name="md_errorTextColor" format="color"/> + + <!-- Whether the text input area should be drawn as a filled box, an outline box, or not as a box.--> + <attr name="md_boxBackgroundMode"> + <!-- Specifies that there should be no box set on the text input area. --> + <enum name="md_none" value="0"/> + <!-- Filled box mode for the text input box. --> + <enum name="md_filled" value="1"/> + <!-- Outline box mode for the text input box. --> + <enum name="md_outline" value="2"/> + </attr> + <!-- Value to use for the EditText's collapsed top padding in box mode. --> + <attr name="md_boxCollapsedPaddingTop" format="dimension"/> + <!-- The value to use for the box's top start corner radius when in box mode. --> + <attr name="md_boxCornerRadiusTopStart" format="dimension"/> + <!-- The value to use for the box's top end corner radius when in box mode. --> + <attr name="md_boxCornerRadiusTopEnd" format="dimension"/> + <!-- The value to use for the box's bottom start corner radius when in box mode. --> + <attr name="md_boxCornerRadiusBottomStart" format="dimension"/> + <!-- The value to use for the box's bottom end corner radius when in box mode. --> + <attr name="md_boxCornerRadiusBottomEnd" format="dimension"/> + <!-- The color to use for the box's stroke when in outline box mode. --> + <attr name="md_boxStrokeColor" format="color"/> + <!-- The color to use for the box's background color when in filled box mode. --> + <attr name="md_boxBackgroundColor" format="color"/> + <!-- The value to use for the box's stroke when in outline box mode. --> + <attr name="md_boxStrokeWidth" format="dimension"/> + + <!-- Shape appearance style reference for MaterialSpinnerLayout. Attribute declaration is in the Shape + package. --> + <attr name="md_shapeAppearance"/> + <!-- Shape appearance overlay style reference for MaterialSpinnerLayout. To be used to augment + attributes declared in the shapeAppearance. Attribute declaration is in the Shape + package. --> + <attr name="md_shapeAppearanceOverlay"/> + </declare-styleable> + +</resources> diff --git a/ui_spinner/src/main/res/values/spinner_colors.xml b/ui_spinner/src/main/res/values/spinner_colors.xml new file mode 100644 index 000000000..d8b7ffab1 --- /dev/null +++ b/ui_spinner/src/main/res/values/spinner_colors.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2018 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 + ~ + ~ https://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. + --> +<resources> + + <!-- Color for the text field's disabled state: 12% black --> + <color name="md_mtrl_spinner_disabled_color">#1F000000</color> + <!-- Color for the text field's default box stroke: 42% black --> + <color name="md_mtrl_spinner_default_box_stroke_color">#6B000000</color> + <!-- Color for the text field's hovered box stroke: 87% black --> + <color name="md_mtrl_spinner_hovered_box_stroke_color">#DE000000</color> + <!-- Color for the text field's focused box stroke: transparent --> + <color name="md_mtrl_spinner_focused_box_stroke_color">#00000000</color> + <!-- Color for the text field's default box background: 4% black --> + <color name="md_mtrl_spinner_filled_box_default_background_color">#0A000000</color> + +</resources> diff --git a/ui_spinner/src/main/res/values/spinner_dimens.xml b/ui_spinner/src/main/res/values/spinner_dimens.xml new file mode 100644 index 000000000..a99ece13d --- /dev/null +++ b/ui_spinner/src/main/res/values/spinner_dimens.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2018 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 + ~ + ~ https://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. + --> +<resources> + <dimen name="md_design_spinner_caption_translate_y">5dp</dimen> + + <dimen name="md_mtrl_spinner_outline_box_expanded_padding">16dp</dimen> + <dimen name="md_mtrl_spinner_box_padding_end">12dp</dimen> + + <dimen name="md_mtrl_spinner_box_corner_radius_small">0dp</dimen> + <dimen name="md_mtrl_spinner_box_corner_radius_medium">4dp</dimen> + <dimen name="md_mtrl_spinner_box_stroke_width_default">1dp</dimen> + <dimen name="md_mtrl_spinner_box_stroke_width_focused">2dp</dimen> + <dimen name="md_mtrl_spinner_box_label_cutout_padding">4dp</dimen> + + <dimen name="md_mtrl_spinner_end_icon_padding_start">16dp</dimen> + <dimen name="md_mtrl_spinner_end_icon_padding_end">12dp</dimen> + <dimen name="md_mtrl_spinner_start_icon_padding_start">12dp</dimen> + <dimen name="md_mtrl_spinner_start_icon_padding_end">16dp</dimen> +</resources> diff --git a/ui_spinner/src/main/res/values/spinner_ids.xml b/ui_spinner/src/main/res/values/spinner_ids.xml new file mode 100644 index 000000000..7d99f9c65 --- /dev/null +++ b/ui_spinner/src/main/res/values/spinner_ids.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2018 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 + ~ + ~ https://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. + --> +<resources> + + <item name="md_spinner_error" type="id"/> + <item name="md_spinner_helper_text" type="id"/> + + <item name="md_spinner_background" type="id"/> + +</resources> diff --git a/ui_spinner/src/main/res/values/spinner_styles.xml b/ui_spinner/src/main/res/values/spinner_styles.xml new file mode 100644 index 000000000..1a5a85094 --- /dev/null +++ b/ui_spinner/src/main/res/values/spinner_styles.xml @@ -0,0 +1,180 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright 2018 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 + ~ + ~ https://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. + --> +<resources xmlns:tools="http://schemas.android.com/tools"> + + <style name="Widget.Design.MaterialSpinnerLayout" parent="android:Widget"> + <item name="materialThemeOverlay">@style/ThemeOverlay.Design.MaterialSpinner</item> + <item name="md_enforceMaterialTheme">false</item> + <item name="md_enforceTextAppearance">false</item> + + <item name="md_boxBackgroundMode">md_none</item> + <item name="md_boxStrokeColor">@color/md_design_box_stroke_color</item> + + <item name="md_errorTextAppearance">@style/TextAppearance.Design.Error</item> + <item name="md_helperTextTextAppearance">@style/TextAppearance.Design.HelperText</item> + <item name="md_hintTextAppearance">@style/TextAppearance.Design.Hint</item> + + <item name="md_errorTextColor">@null</item> + <item name="md_helperTextTextColor">@null</item> + <item name="md_hintTextColor">@null</item> + + <item name="md_shapeAppearance">@null</item> + <item name="md_shapeAppearanceOverlay">@null</item> + </style> + + <!-- Base style for spinnerLayout. You should use one of the sub-styles instead. --> + <style name="Base.Widget.MaterialSpinnerLayout" parent="Widget.Design.MaterialSpinnerLayout"> + <item name="md_enforceMaterialTheme">true</item> + <item name="md_enforceTextAppearance">true</item> + + <item name="md_boxBackgroundMode">md_outline</item> + <item name="md_boxBackgroundColor">@null</item> + <item name="md_boxCollapsedPaddingTop">0dp</item> + <item name="md_boxStrokeColor">@color/md_mtrl_outlined_stroke_color</item> + + <item name="md_errorTextAppearance">?attr/textAppearanceCaption</item> + <item name="md_helperTextTextAppearance">?attr/textAppearanceCaption</item> + <item name="md_hintTextAppearance">?attr/textAppearanceCaption</item> + + <item name="md_errorTextColor">@color/md_mtrl_error</item> + <item name="md_helperTextTextColor">@color/md_mtrl_indicator_text_color</item> + <!-- The color of the label when it is collapsed and the text field is active --> + <item name="md_hintTextColor">?attr/colorPrimary</item> + <!-- The color of the label in all other text field states (such as resting and disabled) --> + <item name="android:textColorHint">@color/md_mtrl_indicator_text_color</item> + + <item name="md_shapeAppearance">?attr/md_shapeAppearanceSmallComponent</item> + <item name="md_shapeAppearanceOverlay">@null</item> + + <item name="md_boxCornerRadiusBottomEnd">@dimen/md_mtrl_spinner_box_corner_radius_medium + </item> + <item name="md_boxCornerRadiusBottomStart">@dimen/md_mtrl_spinner_box_corner_radius_medium + </item> + <item name="md_boxCornerRadiusTopEnd">@dimen/md_mtrl_spinner_box_corner_radius_medium</item> + <item name="md_boxCornerRadiusTopStart">@dimen/md_mtrl_spinner_box_corner_radius_medium</item> + </style> + + <style name="Widget.MaterialSpinnerLayout.FilledBox" parent="Base.Widget.MaterialSpinnerLayout"> + <item name="materialThemeOverlay"> + @style/ThemeOverlay.MaterialSpinner.FilledBox + </item> + <item name="md_boxBackgroundMode">md_filled</item> + <item name="md_boxBackgroundColor">@color/md_mtrl_filled_background_color</item> + <item name="md_boxCollapsedPaddingTop">12dp</item> + <item name="md_boxStrokeColor">@color/md_mtrl_filled_stroke_color</item> + <item name="md_shapeAppearanceOverlay"> + @style/ShapeAppearanceOverlay.MaterialSpinnerLayout.FilledBox + </item> + </style> + + <style name="Widget.MaterialSpinnerLayout.FilledBox.Dense"> + <item name="materialThemeOverlay"> + @style/ThemeOverlay.MaterialSpinner.FilledBox.Dense + </item> + <item name="md_boxCollapsedPaddingTop">8dp</item> + </style> + + <style name="Widget.MaterialSpinnerLayout.OutlinedBox" parent="Base.Widget.MaterialSpinnerLayout"> + <item name="materialThemeOverlay"> + @style/ThemeOverlay.MaterialSpinner.OutlinedBox + </item> + <item name="md_boxCollapsedPaddingTop">0dp</item> + </style> + + <style name="Widget.MaterialSpinnerLayout.OutlinedBox.Dense"> + <item name="materialThemeOverlay"> + @style/ThemeOverlay.MaterialSpinner.OutlinedBox.Dense + </item> + </style> + + <!-- Base style for spinnerEditText. You should use one of the sub-styles instead. --> + <style name="Base.Widget.MaterialSpinner" parent=""> + <item name="android:background">@null</item> + <item name="android:paddingStart" tools:ignore="NewApi">12dp</item> + <item name="android:paddingEnd" tools:ignore="NewApi">12dp</item> + <item name="android:paddingLeft">12dp</item> + <item name="android:paddingRight">12dp</item> + <item name="android:paddingTop">16dp</item> + <item name="android:paddingBottom">16dp</item> + <item name="android:textAppearance">?attr/textAppearanceSubtitle1</item> + </style> + + <style name="Widget.MaterialSpinner.FilledBox" parent="Base.Widget.MaterialSpinner"> + <item name="android:paddingTop">28dp</item> + <item name="android:paddingBottom">12dp</item> + </style> + + <style name="Widget.MaterialSpinner.FilledBox.Dense"> + <item name="android:paddingTop">24dp</item> + <item name="android:paddingBottom">8dp</item> + </style> + + <style name="Widget.MaterialSpinner.OutlinedBox" parent="Base.Widget.MaterialSpinner" /> + + <style name="Widget.MaterialSpinner.OutlinedBox.Dense"> + <item name="android:paddingTop">13dp</item> + <item name="android:paddingBottom">13dp</item> + </style> + + <!-- Set of ThemeOverlays to be used internally in the spinnerLayout styles to automatically apply the correct spinnerEditText style to the spinnerEditText. --> + <style name="ThemeOverlay.Design.MaterialSpinner" parent="" /> + + <style name="ThemeOverlay.MaterialSpinner" parent="ThemeOverlay.Design.MaterialSpinner"> + <item name="colorControlActivated">?attr/colorPrimary</item> + </style> + + <style name="ThemeOverlay.MaterialSpinner.FilledBox"> + <item name="android:spinnerStyle">@style/Widget.MaterialSpinner.FilledBox</item> + <item name="spinnerStyle">@style/Widget.MaterialSpinner.FilledBox</item> + </style> + + <style name="ThemeOverlay.MaterialSpinner.FilledBox.Dense"> + <item name="android:spinnerStyle">@style/Widget.MaterialSpinner.FilledBox.Dense</item> + <item name="spinnerStyle">@style/Widget.MaterialSpinner.FilledBox.Dense</item> + </style> + + <style name="ThemeOverlay.MaterialSpinner.OutlinedBox"> + <item name="android:spinnerStyle">@style/Widget.MaterialSpinner.OutlinedBox</item> + <item name="spinnerStyle">@style/Widget.MaterialSpinner.OutlinedBox</item> + </style> + + <style name="ThemeOverlay.MaterialSpinner.OutlinedBox.Dense"> + <item name="android:spinnerStyle">@style/Widget.MaterialSpinner.OutlinedBox.Dense</item> + <item name="spinnerStyle">@style/Widget.MaterialSpinner.OutlinedBox.Dense</item> + </style> + + <style name="TextAppearance.Design.HelperText" parent="TextAppearance.AppCompat.Caption" /> + + <style name="TextAppearance.Design.Hint" parent="TextAppearance.AppCompat.Caption"> + <item name="android:textColor">?attr/colorControlActivated</item> + </style> + + <style name="TextAppearance.Design.Error" parent="TextAppearance.AppCompat.Caption"> + <item name="android:textColor">@color/md_design_error</item> + </style> + + <style name="TextAppearance.Design.Counter" parent="TextAppearance.AppCompat.Caption" /> + + <style name="TextAppearance.Design.Counter.Overflow" parent="TextAppearance.AppCompat.Caption"> + <item name="android:textColor">@color/md_design_error</item> + </style> + + <style name="ShapeAppearanceOverlay.MaterialSpinnerLayout.FilledBox" parent=""> + <item name="md_cornerSizeBottomLeft">@dimen/md_mtrl_spinner_box_corner_radius_small</item> + <item name="md_cornerSizeBottomRight">@dimen/md_mtrl_spinner_box_corner_radius_small</item> + </style> + +</resources> diff --git a/ui_spinner/src/main/res/values/typography_attrs.xml b/ui_spinner/src/main/res/values/typography_attrs.xml new file mode 100644 index 000000000..764b78851 --- /dev/null +++ b/ui_spinner/src/main/res/values/typography_attrs.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2018 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. +--> +<resources> + <!-- Text appearance for the Headline 1 style. --> + <attr name="textAppearanceHeadline1" format="reference"/> + <!-- Text appearance for the Headline 2 style. --> + <attr name="textAppearanceHeadline2" format="reference"/> + <!-- Text appearance for the Headline 3 style. --> + <attr name="textAppearanceHeadline3" format="reference"/> + <!-- Text appearance for the Headline 4 style. --> + <attr name="textAppearanceHeadline4" format="reference"/> + <!-- Text appearance for the Headline 5 style. --> + <attr name="textAppearanceHeadline5" format="reference"/> + <!-- Text appearance for the Headline 6 style. --> + <attr name="textAppearanceHeadline6" format="reference"/> + <!-- Text appearance for the Subtitle 1 style. --> + <attr name="textAppearanceSubtitle1" format="reference"/> + <!-- Text appearance for the Subtitle 2 style. --> + <attr name="textAppearanceSubtitle2" format="reference"/> + <!-- Text appearance for the Body 1 style. --> + <attr name="textAppearanceBody1" format="reference"/> + <!-- Text appearance for the Body 2 style. --> + <attr name="textAppearanceBody2" format="reference"/> + <!-- Text appearance for the Caption style. --> + <attr name="textAppearanceCaption" format="reference"/> + <!-- Text appearance for the Button style. --> + <attr name="textAppearanceButton" format="reference"/> + <!-- Text appearance for the Overline style. --> + <attr name="textAppearanceOverline" format="reference"/> +</resources> -- GitLab