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>
+ * &lt;de.kuschku.ui.spinner.MaterialSpinnerLayout
+ *         android:layout_width=&quot;match_parent&quot;
+ *         android:layout_height=&quot;wrap_content&quot;
+ *         android:hint=&quot;@string/form_username&quot;&gt;
+ *
+ *     &lt;android.widget.Spinner
+ *             android:layout_width=&quot;match_parent&quot;
+ *             android:layout_height=&quot;wrap_content&quot;/&gt;
+ *
+ * &lt;/de.kuschku.ui.spinner.MaterialSpinnerLayout&gt;
+ * </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