Browse Source

Add DialogBehavior API and implement BottomSheet support. Other cleanup to the library and sample

Aidan Follestad 5 years ago
parent
commit
b04bb2ac58
68 changed files with 1629 additions and 526 deletions
  1. 23 6
      README.md
  2. 1 0
      bottomsheets/.gitignore
  3. 33 0
      bottomsheets/build.gradle
  4. 1 0
      bottomsheets/src/main/AndroidManifest.xml
  5. 249 0
      bottomsheets/src/main/java/com/afollestad/materialdialogs/bottomsheets/BottomSheet.kt
  6. 108 0
      bottomsheets/src/main/java/com/afollestad/materialdialogs/bottomsheets/BottomSheets.kt
  7. 155 0
      bottomsheets/src/main/java/com/afollestad/materialdialogs/bottomsheets/GridIconDialogAdapter.kt
  8. 17 5
      bottomsheets/src/main/java/com/afollestad/materialdialogs/bottomsheets/GridItem.kt
  9. 14 7
      bottomsheets/src/main/java/com/afollestad/materialdialogs/bottomsheets/HeightMatchesWidthImageView.kt
  10. 118 0
      bottomsheets/src/main/java/com/afollestad/materialdialogs/bottomsheets/Util.kt
  11. 3 0
      bottomsheets/src/main/res-public/values/integers.xml
  12. 33 0
      bottomsheets/src/main/res/layout/md_dialog_base_bottomsheet.xml
  13. 19 0
      bottomsheets/src/main/res/layout/md_dialog_base_no_buttons.xml
  14. 25 0
      bottomsheets/src/main/res/layout/md_griditem.xml
  15. 4 0
      bottomsheets/src/main/res/values-land/integers.xml
  16. 4 0
      bottomsheets/src/main/res/values-large-land/integers.xml
  17. 4 0
      bottomsheets/src/main/res/values-large/integers.xml
  18. 4 0
      bottomsheets/src/main/res/values/integers.xml
  19. 22 0
      bottomsheets/src/main/res/values/styles.xml
  20. 4 5
      color/src/main/java/com/afollestad/materialdialogs/color/DialogColorChooserExt.kt
  21. 0 17
      color/src/main/java/com/afollestad/materialdialogs/color/utils/ViewExt.kt
  22. 1 3
      color/src/main/java/com/afollestad/materialdialogs/color/view/SeekBarGroupLayout.kt
  23. 0 1
      color/src/main/res/values/dimens.xml
  24. 160 0
      core/src/main/java/com/afollestad/materialdialogs/DialogBehavior.kt
  25. 37 20
      core/src/main/java/com/afollestad/materialdialogs/MaterialDialog.kt
  26. 9 3
      core/src/main/java/com/afollestad/materialdialogs/actions/DialogActionExt.kt
  27. 5 3
      core/src/main/java/com/afollestad/materialdialogs/checkbox/DialogCheckboxExt.kt
  28. 3 3
      core/src/main/java/com/afollestad/materialdialogs/customview/DialogCustomViewExt.kt
  29. 15 6
      core/src/main/java/com/afollestad/materialdialogs/internal/button/DialogActionButtonLayout.kt
  30. 7 6
      core/src/main/java/com/afollestad/materialdialogs/internal/list/DialogRecyclerView.kt
  31. 1 1
      core/src/main/java/com/afollestad/materialdialogs/internal/list/MultiChoiceDialogAdapter.kt
  32. 1 1
      core/src/main/java/com/afollestad/materialdialogs/internal/list/PlainListDialogAdapter.kt
  33. 1 1
      core/src/main/java/com/afollestad/materialdialogs/internal/list/SingleChoiceDialogAdapter.kt
  34. 7 6
      core/src/main/java/com/afollestad/materialdialogs/internal/main/BaseSubLayout.kt
  35. 12 7
      core/src/main/java/com/afollestad/materialdialogs/internal/main/DialogContentLayout.kt
  36. 56 36
      core/src/main/java/com/afollestad/materialdialogs/internal/main/DialogLayout.kt
  37. 8 5
      core/src/main/java/com/afollestad/materialdialogs/internal/main/DialogScrollView.kt
  38. 4 1
      core/src/main/java/com/afollestad/materialdialogs/internal/main/DialogTitleLayout.kt
  39. 16 32
      core/src/main/java/com/afollestad/materialdialogs/list/DialogListExt.kt
  40. 3 3
      core/src/main/java/com/afollestad/materialdialogs/list/DialogMultiChoiceExt.kt
  41. 3 3
      core/src/main/java/com/afollestad/materialdialogs/list/DialogSingleChoiceExt.kt
  42. 13 60
      core/src/main/java/com/afollestad/materialdialogs/utils/Dialogs.kt
  43. 1 1
      core/src/main/java/com/afollestad/materialdialogs/utils/Dimens.kt
  44. 1 1
      core/src/main/java/com/afollestad/materialdialogs/utils/Fonts.kt
  45. 90 4
      core/src/main/java/com/afollestad/materialdialogs/utils/MDUtil.kt
  46. 0 36
      core/src/main/java/com/afollestad/materialdialogs/utils/Views.kt
  47. 1 2
      core/src/main/res/drawable/md_btn_selected.xml
  48. 1 2
      core/src/main/res/drawable/md_btn_selected_dark.xml
  49. 2 60
      core/src/main/res/layout/md_dialog_base.xml
  50. 35 0
      core/src/main/res/layout/md_dialog_stub_buttons.xml
  51. 25 0
      core/src/main/res/layout/md_dialog_stub_title.xml
  52. 0 2
      core/src/main/res/values/dimens.xml
  53. 13 0
      core/src/main/res/values/styles.xml
  54. 3 3
      dependencies.gradle
  55. 75 0
      documentation/BOTTOMSHEETS.md
  56. 1 1
      documentation/COLOR.md
  57. 1 1
      documentation/CORE.md
  58. 1 1
      documentation/DATETIME.md
  59. 1 1
      documentation/FILES.md
  60. 1 1
      documentation/INPUT.md
  61. 1 1
      documentation/LIFECYCLE.md
  62. 1 0
      sample/build.gradle
  63. 112 114
      sample/src/main/java/com/afollestad/materialdialogssample/MainActivity.kt
  64. 4 3
      sample/src/main/java/com/afollestad/materialdialogssample/Utils.kt
  65. 11 0
      sample/src/main/res/drawable/ic_icon_android.xml
  66. 43 49
      sample/src/main/res/layout/activity_main.xml
  67. 1 1
      sample/src/main/res/layout/custom_view.xml
  68. 1 1
      settings.gradle

+ 23 - 6
README.md

@@ -29,7 +29,7 @@ core and normal-use functionality.
 ```gradle
 dependencies {
   ...
-  implementation 'com.afollestad.material-dialogs:core:2.8.1'
+  implementation 'com.afollestad.material-dialogs:core:3.0.0-alpha1'
 }
 ```
 
@@ -46,7 +46,7 @@ The `input` module contains extensions to the core module, such as a text input
 ```gradle
 dependencies {
   ...
-  implementation 'com.afollestad.material-dialogs:input:2.8.1'
+  implementation 'com.afollestad.material-dialogs:input:3.0.0-alpha1'
 }
 ```
  
@@ -63,7 +63,7 @@ The `files` module contains extensions to the core module, such as a file and fo
 ```gradle
 dependencies {
   ...
-  implementation 'com.afollestad.material-dialogs:files:2.8.1'
+  implementation 'com.afollestad.material-dialogs:files:3.0.0-alpha1'
 }
 ```
 
@@ -80,7 +80,7 @@ The `color` module contains extensions to the core module, such as a color choos
 ```gradle
 dependencies {
   ...
-  implementation 'com.afollestad.material-dialogs:color:2.8.1'
+  implementation 'com.afollestad.material-dialogs:color:3.0.0-alpha1'
 }
 ```
 
@@ -97,7 +97,7 @@ The `datetime` module contains extensions to make date, time, and date-time pick
 ```gradle
 dependencies {
   ...
-  implementation 'com.afollestad.material-dialogs:datetime:2.8.1'
+  implementation 'com.afollestad.material-dialogs:datetime:3.0.0-alpha1'
 }
 ```
 
@@ -112,6 +112,23 @@ The `lifecycle` module contains extensions to make dialogs work with AndroidX li
 ```gradle
 dependencies {
   ...
-  implementation 'com.afollestad.material-dialogs:lifecycle:2.8.1'
+  implementation 'com.afollestad.material-dialogs:lifecycle:3.0.0-alpha1'
 }
 ```
+
+## Bottom Sheets
+
+[ ![Lifecycle](https://img.shields.io/bintray/v/drummer-aidan/maven/material-dialogs:bottomsheets.svg?label=bottomsheets) ](https://bintray.com/drummer-aidan/maven/material-dialogs%3Abottomsheets/_latestVersion)
+
+#### [Bottom Sheets Tutorial and Samples](documentation/BOTTOMSHEETS.md)
+
+The `bottomsheets` module contains extensions to turn modal dialogs into bottom sheets, among 
+other functionality like showing a grid of items. Be sure to checkout the sample project for this,
+too!
+
+```gradle
+dependencies {
+  ...
+  implementation 'com.afollestad.material-dialogs:bottomsheets:3.0.0-alpha1'
+}
+```

+ 1 - 0
bottomsheets/.gitignore

@@ -0,0 +1 @@
+/build

+ 33 - 0
bottomsheets/build.gradle

@@ -0,0 +1,33 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply from: '../dependencies.gradle'
+
+ext.shard = 'bottomsheets'
+apply from: '../bintrayconfig.gradle'
+
+android {
+  compileSdkVersion versions.compileSdk
+  buildToolsVersion versions.buildTools
+
+  defaultConfig {
+    minSdkVersion versions.minSdk
+    targetSdkVersion versions.compileSdk
+    versionCode versions.publishVersionCode
+    versionName versions.publishVersion
+  }
+
+  sourceSets {
+    main.res.srcDirs = [
+        'src/main/res',
+        'src/main/res-public'
+    ]
+  }
+}
+
+dependencies {
+  implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:' + versions.kotlin
+  implementation project(':core')
+  implementation 'com.google.android.material:material:' + versions.androidxMaterial
+}
+
+apply from: '../spotless.gradle'

+ 1 - 0
bottomsheets/src/main/AndroidManifest.xml

@@ -0,0 +1 @@
+<manifest package="com.afollestad.materialdialogs.bottomsheets"/>

+ 249 - 0
bottomsheets/src/main/java/com/afollestad/materialdialogs/bottomsheets/BottomSheet.kt

@@ -0,0 +1,249 @@
+/**
+ * Designed and developed by Aidan Follestad (@afollestad)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.afollestad.materialdialogs.bottomsheets
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.drawable.GradientDrawable
+import android.view.LayoutInflater
+import android.view.View.GONE
+import android.view.ViewGroup
+import android.view.Window
+import android.view.WindowManager.LayoutParams
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import com.afollestad.materialdialogs.DialogBehavior
+import com.afollestad.materialdialogs.MaterialDialog
+import com.afollestad.materialdialogs.internal.button.DialogActionButtonLayout
+import com.afollestad.materialdialogs.internal.button.shouldBeVisible
+import com.afollestad.materialdialogs.internal.main.DialogLayout
+import com.afollestad.materialdialogs.utils.MDUtil.getWidthAndHeight
+import com.afollestad.materialdialogs.utils.MDUtil.waitForHeight
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN
+import kotlin.math.min
+import kotlin.properties.Delegates.notNull
+
+/** @author Aidan Follestad (@afollestad) */
+class BottomSheet : DialogBehavior {
+  internal var bottomSheetBehavior: BottomSheetBehavior<*>? = null
+  private var bottomSheetView: ViewGroup? = null
+
+  private var rootView: CoordinatorLayout? = null
+  private var buttonsLayout: DialogActionButtonLayout? = null
+  private var dialog: MaterialDialog? = null
+
+  private var defaultPeekHeight: Int by notNull()
+  private var actualPeekHeight: Int by notNull()
+
+  @SuppressLint("InflateParams")
+  override fun createView(
+    context: Context,
+    window: Window,
+    layoutInflater: LayoutInflater,
+    dialog: MaterialDialog
+  ): ViewGroup {
+    rootView = layoutInflater.inflate(
+        R.layout.md_dialog_base_bottomsheet,
+        null,
+        false
+    ) as CoordinatorLayout
+
+    this.dialog = dialog
+    this.bottomSheetView = rootView!!.findViewById(R.id.md_root_bottom_sheet)
+    this.buttonsLayout = rootView!!.findViewById(R.id.md_button_layout)
+
+    val (_, windowHeight) = window.windowManager.getWidthAndHeight()
+    defaultPeekHeight = (windowHeight * DEFAULT_PEEK_HEIGHT_RATIO).toInt()
+    actualPeekHeight = defaultPeekHeight
+
+    setupBottomSheetBehavior()
+
+    return rootView!!
+  }
+
+  private fun setupBottomSheetBehavior() {
+    bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetView)
+        .apply {
+          isHideable = true
+          // start at 0 so we can animate it up when the dialog lays out the view
+          peekHeight = 0
+        }
+
+    bottomSheetBehavior!!.setCallbacks(
+        onSlide = { currentHeight ->
+          // Slide the buttons layout down as the bottom sheet is hiding itself
+          val buttonsLayoutHeight = buttonsLayout?.measuredHeight ?: currentHeight + 1
+          if (currentHeight in 1..buttonsLayoutHeight) {
+            val diff = buttonsLayoutHeight - currentHeight
+            buttonsLayout?.translationY = diff.toFloat()
+          } else if (currentHeight > 0) {
+            buttonsLayout?.translationY = 0f
+          }
+          // Show divider over buttons layout if sheet is sliding down
+          invalidateDividers(currentHeight)
+        },
+        onHide = {
+          buttonsLayout?.visibility = GONE
+          dialog?.dismiss()
+          dialog = null
+        }
+    )
+
+    bottomSheetView!!.waitForHeight {
+      actualPeekHeight = min(defaultPeekHeight, min(this.measuredHeight, defaultPeekHeight))
+      bottomSheetBehavior?.animatePeekHeight(
+          view = bottomSheetView!!,
+          start = 0,
+          dest = actualPeekHeight,
+          duration = LAYOUT_PEEK_CHANGE_DURATION_MS,
+          onEnd = {
+            invalidateDividers(actualPeekHeight)
+          }
+      )
+      showButtons()
+    }
+  }
+
+  private fun invalidateDividers(currentHeight: Int) {
+    val contentLayout = dialog?.view?.contentLayout ?: return
+    val mainViewHeight = dialog?.view?.measuredHeight ?: return
+    val scrollView = contentLayout.scrollView
+    val recyclerView = contentLayout.recyclerView
+    when {
+      currentHeight < mainViewHeight -> buttonsLayout?.drawDivider = true
+      scrollView != null -> scrollView.invalidateDividers()
+      recyclerView != null -> recyclerView.invalidateDividers()
+      else -> buttonsLayout?.drawDivider = false
+    }
+  }
+
+  override fun getDialogLayout(root: ViewGroup): DialogLayout {
+    return (root.findViewById(R.id.md_root) as DialogLayout).also { dialogLayout ->
+      dialogLayout.attachButtonsLayout(buttonsLayout!!)
+    }
+  }
+
+  override fun setWindowConstraints(
+    context: Context,
+    window: Window,
+    view: DialogLayout,
+    maxWidth: Int?
+  ) {
+    if (maxWidth == 0) {
+      // Postpone
+      return
+    }
+    window.setSoftInputMode(LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
+    val lp = LayoutParams()
+        .apply {
+          copyFrom(window.attributes)
+          width = LayoutParams.MATCH_PARENT
+          height = LayoutParams.MATCH_PARENT
+        }
+    window.attributes = lp
+  }
+
+  override fun setBackgroundColor(
+    context: Context,
+    window: Window,
+    view: DialogLayout,
+    color: Int,
+    cornerRounding: Float
+  ) {
+    window.setBackgroundDrawable(null)
+    bottomSheetView?.background = GradientDrawable().apply {
+      cornerRadii = floatArrayOf(
+          cornerRounding, cornerRounding, // top left
+          cornerRounding, cornerRounding, // top right
+          0f, 0f, // bottom left
+          0f, 0f // bottom right
+      )
+      setColor(color)
+    }
+    buttonsLayout?.setBackgroundColor(color)
+  }
+
+  override fun onPreShow(dialog: MaterialDialog) {
+    if (dialog.cancelOnTouchOutside) {
+      // Clicking outside the bottom sheet dismisses the dialog
+      rootView?.setOnClickListener { this.dialog?.dismiss() }
+    }
+  }
+
+  override fun onPostShow(dialog: MaterialDialog) = Unit
+
+  override fun onDismiss(): Boolean {
+    if (dialog != null &&
+        bottomSheetBehavior != null &&
+        bottomSheetBehavior!!.state != STATE_HIDDEN
+    ) {
+      bottomSheetBehavior!!.state = STATE_HIDDEN
+      hideButtons { cleanup() }
+      return true
+    }
+    return false
+  }
+
+  private fun cleanup() {
+    bottomSheetBehavior = null
+    bottomSheetView = null
+    buttonsLayout = null
+    rootView = null
+  }
+
+  private fun showButtons() {
+    if (!buttonsLayout.shouldBeVisible()) {
+      return
+    }
+    val start = buttonsLayout!!.measuredHeight
+    buttonsLayout?.translationY = start.toFloat()
+    val animator = animateValues(
+        from = start,
+        to = 0,
+        duration = BUTTONS_SHOW_DURATION_MS,
+        onUpdate = { buttonsLayout?.translationY = it.toFloat() }
+    )
+    buttonsLayout?.onDetach { animator.cancel() }
+    animator.apply {
+      startDelay = BUTTONS_SHOW_START_DELAY_MS
+      start()
+    }
+  }
+
+  private fun hideButtons(onEnd: () -> Unit) {
+    if (buttonsLayout.shouldBeVisible()) {
+      val animator = animateValues(
+          from = 0,
+          to = buttonsLayout!!.measuredHeight,
+          duration = LAYOUT_PEEK_CHANGE_DURATION_MS,
+          onUpdate = { buttonsLayout?.translationY = it.toFloat() },
+          onEnd = onEnd
+      )
+      buttonsLayout?.onDetach { animator.cancel() }
+      animator.start()
+      return
+    }
+    onEnd()
+  }
+
+  private companion object {
+    private const val DEFAULT_PEEK_HEIGHT_RATIO = 0.6f
+    private const val LAYOUT_PEEK_CHANGE_DURATION_MS = 250L
+
+    private const val BUTTONS_SHOW_START_DELAY_MS = 100L
+    private const val BUTTONS_SHOW_DURATION_MS = 180L
+  }
+}

+ 108 - 0
bottomsheets/src/main/java/com/afollestad/materialdialogs/bottomsheets/BottomSheets.kt

@@ -0,0 +1,108 @@
+/**
+ * Designed and developed by Aidan Follestad (@afollestad)
+ *
+ * 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.
+ */
+@file:Suppress("unused")
+
+package com.afollestad.materialdialogs.bottomsheets
+
+import androidx.annotation.CheckResult
+import androidx.annotation.IntegerRes
+import androidx.recyclerview.widget.GridLayoutManager
+import com.afollestad.materialdialogs.MaterialDialog
+import com.afollestad.materialdialogs.internal.list.DialogAdapter
+import com.afollestad.materialdialogs.list.customListAdapter
+import com.afollestad.materialdialogs.list.getListAdapter
+import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED
+import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
+
+/** Expands the bottom sheet, so that it's at its maximum height. */
+fun MaterialDialog.expandBottomSheet(): MaterialDialog {
+  check(dialogBehavior is BottomSheet) {
+    "This dialog is not a bottom sheet dialog."
+  }
+  (dialogBehavior as BottomSheet).bottomSheetBehavior?.let {
+    it.state = STATE_EXPANDED
+  }
+  return this
+}
+
+/** Collapses the bottom sheet, so that it's at its peek height. */
+fun MaterialDialog.collapseBottomSheet(): MaterialDialog {
+  check(dialogBehavior is BottomSheet) {
+    "This dialog is not a bottom sheet dialog."
+  }
+  (dialogBehavior as BottomSheet).bottomSheetBehavior?.let {
+    it.state = STATE_COLLAPSED
+  }
+  return this
+}
+
+typealias GridItemListener<IT> =
+    ((dialog: MaterialDialog, index: Int, item: IT) -> Unit)?
+
+/**
+ * Populates the bottom sheet with a grid of items that have icon and text.
+ */
+@CheckResult fun <IT : GridItem> MaterialDialog.gridItems(
+  items: List<IT>,
+  @IntegerRes customGridWidth: Int? = null,
+  disabledIndices: IntArray? = null,
+  waitForPositiveButton: Boolean = true,
+  selection: GridItemListener<IT> = null
+): MaterialDialog {
+  if (getListAdapter() != null) {
+    return updateGridItems(
+        items = items,
+        disabledIndices = disabledIndices
+    )
+  }
+
+  val gridWidth = windowContext.resources.getInteger(customGridWidth ?: R.integer.md_grid_width)
+  val layoutManager = GridLayoutManager(windowContext, gridWidth)
+  return customListAdapter(
+      adapter = GridIconDialogAdapter(
+          dialog = this,
+          items = items,
+          disabledItems = disabledIndices,
+          waitForPositiveButton = waitForPositiveButton,
+          selection = selection
+      ),
+      layoutManager = layoutManager
+  )
+}
+
+/**
+ * Updates the grid items, and optionally the disabled indices, for a bottom sheet.
+ *
+ * @author Aidan Follestad (@afollestad)
+ */
+fun MaterialDialog.updateGridItems(
+  items: List<GridItem>,
+  disabledIndices: IntArray? = null
+): MaterialDialog {
+  val adapter = getListAdapter()
+  check(adapter != null) {
+    "updateGridItems(...) can't be used before you've created a bottom sheet grid dialog."
+  }
+  if (adapter is DialogAdapter<*, *>) {
+    @Suppress("UNCHECKED_CAST")
+    (adapter as DialogAdapter<GridItem, *>).replaceItems(items)
+
+    if (disabledIndices != null) {
+      adapter.disableItems(disabledIndices)
+    }
+  }
+  return this
+}

+ 155 - 0
bottomsheets/src/main/java/com/afollestad/materialdialogs/bottomsheets/GridIconDialogAdapter.kt

@@ -0,0 +1,155 @@
+/**
+ * Designed and developed by Aidan Follestad (@afollestad)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.afollestad.materialdialogs.bottomsheets
+
+import android.view.View
+import android.view.View.OnClickListener
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.afollestad.materialdialogs.MaterialDialog
+import com.afollestad.materialdialogs.WhichButton.POSITIVE
+import com.afollestad.materialdialogs.actions.hasActionButton
+import com.afollestad.materialdialogs.actions.hasActionButtons
+import com.afollestad.materialdialogs.internal.list.DialogAdapter
+import com.afollestad.materialdialogs.list.getItemSelector
+import com.afollestad.materialdialogs.utils.MDUtil.inflate
+import com.afollestad.materialdialogs.utils.MDUtil.maybeSetTextColor
+
+private const val KEY_ACTIVATED_INDEX = "activated_index"
+
+/** @author Aidan Follestad (afollestad) */
+internal class GridItemViewHolder(
+  itemView: View,
+  private val adapter: GridIconDialogAdapter<*>
+) : RecyclerView.ViewHolder(itemView), OnClickListener {
+  init {
+    itemView.setOnClickListener(this)
+  }
+
+  val iconView: ImageView = itemView.findViewById(R.id.md_grid_icon)
+  val titleView: TextView = itemView.findViewById(R.id.md_grid_title)
+
+  override fun onClick(view: View) = adapter.itemClicked(adapterPosition)
+}
+
+/**
+ * The default list adapter for list dialogs, containing plain textual list items.
+ *
+ * @author Aidan Follestad (afollestad)
+ */
+internal class GridIconDialogAdapter<IT : GridItem>(
+  private var dialog: MaterialDialog,
+  private var items: List<IT>,
+  disabledItems: IntArray?,
+  private var waitForPositiveButton: Boolean,
+  private var selection: GridItemListener<IT>
+) : RecyclerView.Adapter<GridItemViewHolder>(), DialogAdapter<IT, GridItemListener<IT>> {
+
+  private var disabledIndices: IntArray = disabledItems ?: IntArray(0)
+
+  fun itemClicked(index: Int) {
+    if (waitForPositiveButton && dialog.hasActionButton(POSITIVE)) {
+      // Wait for positive action button, mark clicked item as activated so that we can call the
+      // selection listener when the button is pressed.
+      val lastActivated = dialog.config[KEY_ACTIVATED_INDEX] as? Int
+      dialog.config[KEY_ACTIVATED_INDEX] = index
+      if (lastActivated != null) {
+        notifyItemChanged(lastActivated)
+      }
+      notifyItemChanged(index)
+    } else {
+      // Don't wait for action buttons, call listener and dismiss if auto dismiss is applicable
+      this.selection?.invoke(dialog, index, this.items[index])
+      if (dialog.autoDismissEnabled && !dialog.hasActionButtons()) {
+        dialog.dismiss()
+      }
+    }
+  }
+
+  override fun onCreateViewHolder(
+    parent: ViewGroup,
+    viewType: Int
+  ): GridItemViewHolder {
+    val listItemView: View = parent.inflate(dialog.windowContext, R.layout.md_griditem)
+    val viewHolder = GridItemViewHolder(
+        itemView = listItemView,
+        adapter = this
+    )
+    viewHolder.titleView.maybeSetTextColor(dialog.windowContext, R.attr.md_color_content)
+    return viewHolder
+  }
+
+  override fun getItemCount() = items.size
+
+  override fun onBindViewHolder(
+    holder: GridItemViewHolder,
+    position: Int
+  ) {
+    holder.itemView.isEnabled = !disabledIndices.contains(position)
+    val currentItem = items[position]
+
+    holder.titleView.text = currentItem.title
+    holder.itemView.background = dialog.getItemSelector()
+    currentItem.populateIcon(holder.iconView)
+
+    val activatedIndex = dialog.config[KEY_ACTIVATED_INDEX] as? Int
+    holder.itemView.isActivated = activatedIndex != null && activatedIndex == position
+
+    if (dialog.bodyFont != null) {
+      holder.titleView.typeface = dialog.bodyFont
+    }
+  }
+
+  override fun positiveButtonClicked() {
+    val activatedIndex = dialog.config[KEY_ACTIVATED_INDEX] as? Int
+    if (activatedIndex != null) {
+      selection?.invoke(dialog, activatedIndex, items[activatedIndex])
+      dialog.config.remove(KEY_ACTIVATED_INDEX)
+    }
+  }
+
+  override fun replaceItems(
+    items: List<IT>,
+    listener: GridItemListener<IT>
+  ) {
+    this.items = items
+    if (listener != null) {
+      this.selection = listener
+    }
+    this.notifyDataSetChanged()
+  }
+
+  override fun disableItems(indices: IntArray) {
+    this.disabledIndices = indices
+    notifyDataSetChanged()
+  }
+
+  override fun checkItems(indices: IntArray) = Unit
+
+  override fun uncheckItems(indices: IntArray) = Unit
+
+  override fun toggleItems(indices: IntArray) = Unit
+
+  override fun checkAllItems() = Unit
+
+  override fun uncheckAllItems() = Unit
+
+  override fun toggleAllChecked() = Unit
+
+  override fun isItemChecked(index: Int) = false
+}

+ 17 - 5
core/src/main/java/com/afollestad/materialdialogs/utils/Strings.kt → bottomsheets/src/main/java/com/afollestad/materialdialogs/bottomsheets/GridItem.kt

@@ -13,11 +13,23 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.afollestad.materialdialogs.utils
+package com.afollestad.materialdialogs.bottomsheets
 
-import androidx.annotation.ArrayRes
-import com.afollestad.materialdialogs.MaterialDialog
+import android.widget.ImageView
+import androidx.annotation.DrawableRes
 
-internal fun MaterialDialog.getStringArray(@ArrayRes res: Int?): Array<String>? {
-  return if (res != null) return windowContext.resources.getStringArray(res) else emptyArray()
+/** @author Aidan Follestad (@afollestad) */
+interface GridItem {
+  val title: String
+  fun populateIcon(imageView: ImageView)
+}
+
+/** @author Aidan Follestad (@afollestad) */
+data class BasicGridItem(
+  @DrawableRes val iconRes: Int,
+  override val title: String
+) : GridItem {
+  override fun populateIcon(imageView: ImageView) {
+    imageView.setImageResource(iconRes)
+  }
 }

+ 14 - 7
core/src/main/java/com/afollestad/materialdialogs/utils/Windows.kt → bottomsheets/src/main/java/com/afollestad/materialdialogs/bottomsheets/HeightMatchesWidthImageView.kt

@@ -13,13 +13,20 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.afollestad.materialdialogs.utils
+package com.afollestad.materialdialogs.bottomsheets
 
-import android.graphics.Point
-import android.view.WindowManager
+import android.content.Context
+import android.util.AttributeSet
+import androidx.appcompat.widget.AppCompatImageView
 
-internal fun WindowManager.getWidthAndHeight(): Pair<Int, Int> {
-  val size = Point()
-  defaultDisplay.getSize(size)
-  return Pair(size.x, size.y)
+/** @author Aidan Follestad (@afollestad) */
+class HeightMatchesWidthImageView(
+  context: Context,
+  attrs: AttributeSet?
+) : AppCompatImageView(context, attrs) {
+
+  override fun onMeasure(
+    widthMeasureSpec: Int,
+    heightMeasureSpec: Int
+  ) = super.onMeasure(widthMeasureSpec, widthMeasureSpec)
 }

+ 118 - 0
bottomsheets/src/main/java/com/afollestad/materialdialogs/bottomsheets/Util.kt

@@ -0,0 +1,118 @@
+/**
+ * Designed and developed by Aidan Follestad (@afollestad)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.afollestad.materialdialogs.bottomsheets
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.view.View
+import android.view.View.OnAttachStateChangeListener
+import android.view.animation.DecelerateInterpolator
+import androidx.annotation.CheckResult
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
+import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED
+import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN
+import java.lang.Float.isNaN
+import kotlin.math.abs
+
+internal fun BottomSheetBehavior<*>.setCallbacks(
+  onSlide: (currentHeight: Int) -> Unit,
+  onHide: () -> Unit
+) {
+  setBottomSheetCallback(object : BottomSheetCallback() {
+    private var currentState: Int = STATE_COLLAPSED
+
+    override fun onSlide(
+      view: View,
+      dY: Float
+    ) {
+      if (state == STATE_HIDDEN) return
+      val percentage = if (isNaN(dY)) 0f else dY
+      if (percentage > 0f) {
+        val diff = peekHeight * abs(percentage)
+        onSlide((peekHeight + diff).toInt())
+      } else {
+        val diff = peekHeight * abs(percentage)
+        onSlide((peekHeight - diff).toInt())
+      }
+    }
+
+    override fun onStateChanged(
+      view: View,
+      state: Int
+    ) {
+      currentState = state
+      if (state == STATE_HIDDEN) onHide()
+    }
+  })
+}
+
+internal fun BottomSheetBehavior<*>.animatePeekHeight(
+  view: View,
+  start: Int = peekHeight,
+  dest: Int,
+  duration: Long,
+  onEnd: () -> Unit = {}
+) {
+  if (dest == start) {
+    return
+  } else if (duration <= 0) {
+    peekHeight = dest
+    return
+  }
+  val animator = animateValues(
+      from = start,
+      to = dest,
+      duration = duration,
+      onUpdate = this::setPeekHeight,
+      onEnd = onEnd
+  )
+  view.onDetach { animator.cancel() }
+  animator.start()
+}
+
+@CheckResult internal fun animateValues(
+  from: Int,
+  to: Int,
+  duration: Long,
+  onUpdate: (currentValue: Int) -> Unit,
+  onEnd: () -> Unit = {}
+): Animator {
+  return ValueAnimator.ofInt(from, to)
+      .apply {
+        this.interpolator = DecelerateInterpolator()
+        this.duration = duration
+        addUpdateListener {
+          onUpdate(it.animatedValue as Int)
+        }
+        addListener(object : AnimatorListenerAdapter() {
+          override fun onAnimationEnd(animation: Animator) = onEnd()
+        })
+      }
+}
+
+internal fun <T : View> T.onDetach(onAttached: T.() -> Unit) {
+  addOnAttachStateChangeListener(object : OnAttachStateChangeListener {
+    @Suppress("UNCHECKED_CAST")
+    override fun onViewDetachedFromWindow(v: View) {
+      removeOnAttachStateChangeListener(this)
+      (v as T).onAttached()
+    }
+
+    override fun onViewAttachedToWindow(v: View) = Unit
+  })
+}

+ 3 - 0
bottomsheets/src/main/res-public/values/integers.xml

@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+</resources>

+ 33 - 0
bottomsheets/src/main/res/layout/md_dialog_base_bottomsheet.xml

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.coordinatorlayout.widget.CoordinatorLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    >
+
+  <FrameLayout
+      android:id="@+id/md_root_bottom_sheet"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:clickable="true"
+      android:focusable="true"
+      app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
+      >
+
+    <include
+        layout="@layout/md_dialog_base_no_buttons"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        />
+
+  </FrameLayout>
+
+  <include
+      layout="@layout/md_dialog_stub_buttons"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:layout_gravity="bottom"
+      />
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout>

+ 19 - 0
bottomsheets/src/main/res/layout/md_dialog_base_no_buttons.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.afollestad.materialdialogs.internal.main.DialogLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/md_root"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    >
+
+  <include layout="@layout/md_dialog_stub_title"/>
+
+  <com.afollestad.materialdialogs.internal.main.DialogContentLayout
+      android:id="@+id/md_content_layout"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      />
+
+  <!-- No buttons -->
+
+</com.afollestad.materialdialogs.internal.main.DialogLayout>

+ 25 - 0
bottomsheets/src/main/res/layout/md_griditem.xml

@@ -0,0 +1,25 @@
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    style="@style/MD_GridItem"
+    >
+  <LinearLayout
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:layout_gravity="center"
+      android:orientation="vertical"
+      >
+    <com.afollestad.materialdialogs.bottomsheets.HeightMatchesWidthImageView
+        android:id="@+id/md_grid_icon"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:scaleType="center"
+        tools:ignore="ContentDescription"
+        />
+    <com.afollestad.materialdialogs.internal.rtl.RtlTextView
+        android:id="@+id/md_grid_title"
+        tools:text="Item"
+        style="@style/MD_GridItemText"
+        />
+  </LinearLayout>
+</FrameLayout>

+ 4 - 0
bottomsheets/src/main/res/values-land/integers.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <integer name="md_grid_width">6</integer>
+</resources>

+ 4 - 0
bottomsheets/src/main/res/values-large-land/integers.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <integer name="md_grid_width">8</integer>
+</resources>

+ 4 - 0
bottomsheets/src/main/res/values-large/integers.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <integer name="md_grid_width">6</integer>
+</resources>

+ 4 - 0
bottomsheets/src/main/res/values/integers.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <integer name="md_grid_width">4</integer>
+</resources>

+ 22 - 0
bottomsheets/src/main/res/values/styles.xml

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+  <style name="MD_GridItem" parent="@style/MD_ListItem">
+    <item name="android:paddingTop">8dp</item>
+    <item name="android:paddingBottom">8dp</item>
+    <item name="android:paddingLeft">4dp</item>
+    <item name="android:paddingRight">4dp</item>
+    <item name="android:gravity">center</item>
+  </style>
+
+  <style name="MD_GridItemText">
+    <item name="android:layout_width">wrap_content</item>
+    <item name="android:layout_height">wrap_content</item>
+    <item name="android:layout_gravity">center</item>
+    <item name="android:gravity">center</item>
+    <item name="android:lines">2</item>
+    <item name="android:textSize">@dimen/md_listitem_textsize</item>
+    <item name="android:textColor">@color/md_list_item_textcolor</item>
+  </style>
+
+</resources>

+ 4 - 5
color/src/main/java/com/afollestad/materialdialogs/color/DialogColorChooserExt.kt

@@ -15,7 +15,6 @@
  */
 package com.afollestad.materialdialogs.color
 
-import android.R.attr
 import android.annotation.SuppressLint
 import android.content.Context.INPUT_METHOD_SERVICE
 import android.graphics.Color
@@ -119,7 +118,7 @@ fun MaterialDialog.colorChooser(
           context.getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager
         imm?.hideSoftInputFromWindow(hexValueView.windowToken, 0)
       } else {
-        invalidateDividers(false, false)
+        invalidateDividers(showTop = false, showBottom = false)
       }
     }
 
@@ -304,12 +303,12 @@ internal fun MaterialDialog.updateActionButtonsColor(@ColorInt color: Int) {
   val adjustedColor = Color.rgb(Color.red(color), Color.green(color), Color.blue(color))
   val isAdjustedDark = adjustedColor.isColorDark(0.25)
   val isPrimaryDark =
-    resolveColor(context = context, attr = attr.textColorPrimary).isColorDark()
+    resolveColor(context = context, attr = android.R.attr.textColorPrimary).isColorDark()
 
   val finalColor = if (isPrimaryDark && !isAdjustedDark) {
-    resolveColor(context = context, attr = attr.textColorPrimary)
+    resolveColor(context = context, attr = android.R.attr.textColorPrimary)
   } else if (!isPrimaryDark && isAdjustedDark) {
-    resolveColor(context = context, attr = attr.textColorPrimaryInverse)
+    resolveColor(context = context, attr = android.R.attr.textColorPrimaryInverse)
   } else {
     adjustedColor
   }

+ 0 - 17
color/src/main/java/com/afollestad/materialdialogs/color/utils/ViewExt.kt

@@ -21,7 +21,6 @@ import android.view.View.INVISIBLE
 import android.view.View.VISIBLE
 import android.view.ViewGroup.MarginLayoutParams
 import android.widget.RelativeLayout
-import android.widget.SeekBar
 import androidx.annotation.IdRes
 import androidx.viewpager.widget.ViewPager
 
@@ -43,22 +42,6 @@ internal fun ViewPager.onPageSelected(selection: (Int) -> Unit) {
   })
 }
 
-internal fun Array<SeekBar>.progressChanged(selection: (Int) -> Unit) {
-  val listener = object : SeekBar.OnSeekBarChangeListener {
-    override fun onProgressChanged(
-      p0: SeekBar?,
-      p1: Int,
-      p2: Boolean
-    ) = selection(p1)
-
-    override fun onStartTrackingTouch(p0: SeekBar?) = Unit
-    override fun onStopTrackingTouch(p0: SeekBar?) = Unit
-  }
-  for (bar in this) {
-    bar.setOnSeekBarChangeListener(listener)
-  }
-}
-
 internal fun View.changeHeight(height: Int) {
   if (height == 0) {
     visibility = INVISIBLE

+ 1 - 3
color/src/main/java/com/afollestad/materialdialogs/color/view/SeekBarGroupLayout.kt

@@ -56,9 +56,7 @@ class SeekBarGroupLayout(
 
   @SuppressLint("ClickableViewAccessibility")
   override fun onTouchEvent(event: MotionEvent): Boolean {
-    val action = event.actionMasked
-
-    when (action) {
+    when (event.actionMasked) {
       ACTION_DOWN -> {
         val target = closestSeekBar(event)
         if (target != null) {

+ 0 - 1
color/src/main/res/values/dimens.xml

@@ -11,7 +11,6 @@
 
   <dimen name="color_argb_preview_height">72dp</dimen>
   <dimen name="color_argb_preview_width_landscape">120dp</dimen>
-  <dimen name="color_argb_preview_border_radius">4dp</dimen>
   <dimen name="color_argb_preview_marginBottom">8dp</dimen>
   <dimen name="color_argb_preview_marginRight_landscape">8dp</dimen>
 

+ 160 - 0
core/src/main/java/com/afollestad/materialdialogs/DialogBehavior.kt

@@ -0,0 +1,160 @@
+/**
+ * Designed and developed by Aidan Follestad (@afollestad)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.afollestad.materialdialogs
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.drawable.GradientDrawable
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.view.Window
+import android.view.WindowManager.LayoutParams
+import androidx.annotation.ColorInt
+import androidx.annotation.Px
+import com.afollestad.materialdialogs.WhichButton.NEGATIVE
+import com.afollestad.materialdialogs.WhichButton.POSITIVE
+import com.afollestad.materialdialogs.actions.getActionButton
+import com.afollestad.materialdialogs.internal.main.DialogLayout
+import com.afollestad.materialdialogs.utils.MDUtil.getWidthAndHeight
+import com.afollestad.materialdialogs.utils.isVisible
+import kotlin.math.min
+
+/** @author Aidan Follestad (@afollestad) */
+interface DialogBehavior {
+  /** Creates the root layout of the dialog. */
+  fun createView(
+    context: Context,
+    window: Window,
+    layoutInflater: LayoutInflater,
+    dialog: MaterialDialog
+  ): ViewGroup
+
+  /** Retrieves the [DialogLayout] from the view inflated in [createView]. */
+  fun getDialogLayout(root: ViewGroup): DialogLayout
+
+  /** Sets window constraints, width and height. */
+  fun setWindowConstraints(
+    context: Context,
+    window: Window,
+    view: DialogLayout,
+    @Px maxWidth: Int?
+  )
+
+  /** Sets the root dialog background. */
+  fun setBackgroundColor(
+    context: Context,
+    window: Window,
+    view: DialogLayout,
+    @ColorInt color: Int,
+    cornerRounding: Float
+  )
+
+  /** Called when the dialog is about to be shown. */
+  fun onPreShow(dialog: MaterialDialog)
+
+  /** Called when the dialog is has been shown. */
+  fun onPostShow(dialog: MaterialDialog)
+
+  /**
+   * Called when the dialog is being dismissed. Return true if you've handled
+   * it, and if super.dismiss() should NOT be called on the dialog. This is an
+   * opportunity to cleanup resources, as well.
+   */
+  fun onDismiss(): Boolean
+}
+
+/** @author Aidan Follestad (@afollestad) */
+object ModalDialog : DialogBehavior {
+  @SuppressLint("InflateParams")
+  override fun createView(
+    context: Context,
+    window: Window,
+    layoutInflater: LayoutInflater,
+    dialog: MaterialDialog
+  ): ViewGroup {
+    return layoutInflater.inflate(
+        R.layout.md_dialog_base,
+        null,
+        false
+    ) as ViewGroup
+  }
+
+  override fun getDialogLayout(root: ViewGroup): DialogLayout {
+    return root as DialogLayout
+  }
+
+  override fun setWindowConstraints(
+    context: Context,
+    window: Window,
+    view: DialogLayout,
+    maxWidth: Int?
+  ) {
+    if (maxWidth == 0) {
+      // Postpone
+      return
+    }
+
+    window.setSoftInputMode(LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
+    val wm = window.windowManager ?: return
+    val res = context.resources
+    val (windowWidth, windowHeight) = wm.getWidthAndHeight()
+
+    val windowVerticalPadding =
+      res.getDimensionPixelSize(R.dimen.md_dialog_vertical_margin)
+    view.maxHeight = windowHeight - windowVerticalPadding * 2
+
+    val lp = LayoutParams().apply {
+      copyFrom(window.attributes)
+
+      val windowHorizontalPadding =
+        res.getDimensionPixelSize(R.dimen.md_dialog_horizontal_margin)
+      val calculatedWidth = windowWidth - windowHorizontalPadding * 2
+      val actualMaxWidth =
+        maxWidth ?: res.getDimensionPixelSize(R.dimen.md_dialog_max_width)
+      width = min(actualMaxWidth, calculatedWidth)
+    }
+    window.attributes = lp
+  }
+
+  override fun setBackgroundColor(
+    context: Context,
+    window: Window,
+    view: DialogLayout,
+    @ColorInt color: Int,
+    cornerRounding: Float
+  ) {
+    window.setBackgroundDrawable(GradientDrawable().apply {
+      cornerRadius = cornerRounding
+      setColor(color)
+    })
+  }
+
+  override fun onPreShow(dialog: MaterialDialog) = Unit
+
+  override fun onPostShow(dialog: MaterialDialog) {
+    val negativeBtn = dialog.getActionButton(NEGATIVE)
+    if (negativeBtn.isVisible()) {
+      negativeBtn.post { negativeBtn.requestFocus() }
+      return
+    }
+    val positiveBtn = dialog.getActionButton(POSITIVE)
+    if (positiveBtn.isVisible()) {
+      positiveBtn.post { positiveBtn.requestFocus() }
+    }
+  }
+
+  override fun onDismiss(): Boolean = false
+}

+ 37 - 20
core/src/main/java/com/afollestad/materialdialogs/MaterialDialog.kt

@@ -21,6 +21,7 @@ import android.app.Dialog
 import android.content.Context
 import android.graphics.Typeface
 import android.graphics.drawable.Drawable
+import android.view.LayoutInflater
 import androidx.annotation.CheckResult
 import androidx.annotation.DimenRes
 import androidx.annotation.DrawableRes
@@ -35,31 +36,20 @@ import com.afollestad.materialdialogs.callbacks.invokeAll
 import com.afollestad.materialdialogs.internal.list.DialogAdapter
 import com.afollestad.materialdialogs.internal.main.DialogLayout
 import com.afollestad.materialdialogs.list.getListAdapter
+import com.afollestad.materialdialogs.utils.MDUtil.assertOneSet
 import com.afollestad.materialdialogs.utils.hideKeyboard
-import com.afollestad.materialdialogs.utils.inflate
 import com.afollestad.materialdialogs.utils.isVisible
 import com.afollestad.materialdialogs.utils.populateIcon
 import com.afollestad.materialdialogs.utils.populateText
-import com.afollestad.materialdialogs.utils.postShow
 import com.afollestad.materialdialogs.utils.preShow
 import com.afollestad.materialdialogs.utils.setDefaults
-import com.afollestad.materialdialogs.utils.setWindowConstraints
-
-internal fun assertOneSet(
-  method: String,
-  b: Any?,
-  a: Int?
-) {
-  if (a == null && b == null) {
-    throw IllegalArgumentException("$method: You must specify a resource ID or literal value")
-  }
-}
 
 typealias DialogCallback = (MaterialDialog) -> Unit
 
 /** @author Aidan Follestad (afollestad) */
 class MaterialDialog(
-  val windowContext: Context
+  val windowContext: Context,
+  val dialogBehavior: DialogBehavior = ModalDialog
 ) : Dialog(windowContext, inferTheme(windowContext).styleRes) {
 
   /**
@@ -86,9 +76,12 @@ class MaterialDialog(
     internal set
   var buttonFont: Typeface? = null
     internal set
+  var cancelOnTouchOutside: Boolean = true
+    internal set
   @Px private var maxWidth: Int? = null
 
-  internal val view: DialogLayout = inflate(R.layout.md_dialog_base)
+  /** The root layout of the dialog. */
+  val view: DialogLayout
 
   internal val preShowListeners = mutableListOf<DialogCallback>()
   internal val showListeners = mutableListOf<DialogCallback>()
@@ -100,8 +93,16 @@ class MaterialDialog(
   private val neutralListeners = mutableListOf<DialogCallback>()
 
   init {
-    setContentView(view)
-    this.view.dialog = this
+    val layoutInflater = LayoutInflater.from(windowContext)
+    val rootView = dialogBehavior.createView(
+        context = windowContext,
+        window = window!!,
+        layoutInflater = layoutInflater,
+        dialog = this
+    )
+    setContentView(rootView)
+    this.view = dialogBehavior.getDialogLayout(rootView)
+        .also { it.attachDialog(this) }
     setDefaults()
   }
 
@@ -320,7 +321,7 @@ class MaterialDialog(
       literal!!
     }
     if (shouldSetConstraints) {
-      setWindowConstraints(this.maxWidth)
+      setWindowConstraints()
     }
     return this
   }
@@ -333,10 +334,11 @@ class MaterialDialog(
 
   /** Opens the dialog. */
   override fun show() {
-    setWindowConstraints(maxWidth)
+    setWindowConstraints()
     preShow()
+    dialogBehavior.onPreShow(this)
     super.show()
-    postShow()
+    dialogBehavior.onPostShow(this)
   }
 
   /** Applies multiple properties to the dialog and opens it. */
@@ -354,11 +356,15 @@ class MaterialDialog(
 
   /** A fluent version of [setCanceledOnTouchOutside]. */
   fun cancelOnTouchOutside(cancelable: Boolean): MaterialDialog {
+    cancelOnTouchOutside = true
     this.setCanceledOnTouchOutside(cancelable)
     return this
   }
 
   override fun dismiss() {
+    if (dialogBehavior.onDismiss()) {
+      return
+    }
     hideKeyboard()
     super.dismiss()
   }
@@ -377,4 +383,15 @@ class MaterialDialog(
       dismiss()
     }
   }
+
+  private fun setWindowConstraints() {
+    window?.let {
+      dialogBehavior.setWindowConstraints(
+          context = windowContext,
+          maxWidth = maxWidth,
+          window = it,
+          view = view
+      )
+    }
+  }
 }

+ 9 - 3
core/src/main/java/com/afollestad/materialdialogs/actions/DialogActionExt.kt

@@ -17,17 +17,23 @@ package com.afollestad.materialdialogs.actions
 
 import com.afollestad.materialdialogs.MaterialDialog
 import com.afollestad.materialdialogs.WhichButton
+import com.afollestad.materialdialogs.internal.button.DialogActionButton
 import com.afollestad.materialdialogs.utils.isVisible
 
 /** Returns true if the dialog has visible action buttons. */
-fun MaterialDialog.hasActionButtons() = view.buttonsLayout.visibleButtons.isNotEmpty()
+fun MaterialDialog.hasActionButtons(): Boolean {
+  return view.buttonsLayout?.visibleButtons?.isNotEmpty() ?: false
+}
 
 /** Returns true if the given button is visible in the dialog. */
 fun MaterialDialog.hasActionButton(which: WhichButton) = getActionButton(which).isVisible()
 
 /** Returns the underlying view for an action button in the dialog. */
-fun MaterialDialog.getActionButton(which: WhichButton) =
-  view.buttonsLayout.actionButtons[which.index]
+fun MaterialDialog.getActionButton(which: WhichButton): DialogActionButton {
+  return view.buttonsLayout?.actionButtons?.get(which.index) ?: throw IllegalStateException(
+      "The dialog does not have an attached buttons layout."
+  )
+}
 
 /** Enables or disables an action button. */
 fun MaterialDialog.setActionButtonEnabled(

+ 5 - 3
core/src/main/java/com/afollestad/materialdialogs/checkbox/DialogCheckboxExt.kt

@@ -24,7 +24,7 @@ import androidx.annotation.StringRes
 import androidx.core.widget.CompoundButtonCompat
 import com.afollestad.materialdialogs.MaterialDialog
 import com.afollestad.materialdialogs.R
-import com.afollestad.materialdialogs.assertOneSet
+import com.afollestad.materialdialogs.utils.MDUtil.assertOneSet
 import com.afollestad.materialdialogs.utils.MDUtil.createColorSelector
 import com.afollestad.materialdialogs.utils.MDUtil.maybeSetTextColor
 import com.afollestad.materialdialogs.utils.MDUtil.resolveString
@@ -33,7 +33,9 @@ import com.afollestad.materialdialogs.utils.resolveColors
 typealias BooleanCallback = ((Boolean) -> Unit)?
 
 @CheckResult fun MaterialDialog.getCheckBoxPrompt(): CheckBox {
-  return view.buttonsLayout.checkBoxPrompt
+  return view.buttonsLayout?.checkBoxPrompt ?: throw IllegalStateException(
+      "The dialog does not have an attached buttons layout."
+  )
 }
 
 @CheckResult fun MaterialDialog.isCheckPromptChecked() = getCheckBoxPrompt().isChecked
@@ -51,7 +53,7 @@ typealias BooleanCallback = ((Boolean) -> Unit)?
   onToggle: BooleanCallback
 ): MaterialDialog {
   assertOneSet("checkBoxPrompt", text, res)
-  view.buttonsLayout.checkBoxPrompt.run {
+  view.buttonsLayout?.checkBoxPrompt?.run {
     this.visibility = View.VISIBLE
     this.text = text ?: resolveString(this@checkBoxPrompt, res)
     this.isChecked = isCheckedDefault

+ 3 - 3
core/src/main/java/com/afollestad/materialdialogs/customview/DialogCustomViewExt.kt

@@ -19,8 +19,8 @@ import android.view.View
 import androidx.annotation.CheckResult
 import androidx.annotation.LayoutRes
 import com.afollestad.materialdialogs.MaterialDialog
-import com.afollestad.materialdialogs.assertOneSet
-import com.afollestad.materialdialogs.utils.MDUtil.waitForLayout
+import com.afollestad.materialdialogs.utils.MDUtil.assertOneSet
+import com.afollestad.materialdialogs.utils.MDUtil.waitForWidth
 
 internal const val CUSTOM_VIEW_NO_VERTICAL_PADDING = "md.custom_view_no_vertical_padding"
 
@@ -66,7 +66,7 @@ fun MaterialDialog.customView(
   )
       .also {
         if (dialogWrapContent) {
-          it.waitForLayout {
+          it.waitForWidth {
             maxWidth(literal = measuredWidth)
           }
         }

+ 15 - 6
core/src/main/java/com/afollestad/materialdialogs/internal/button/DialogActionButtonLayout.kt

@@ -23,6 +23,8 @@ import android.view.View.MeasureSpec.EXACTLY
 import android.view.View.MeasureSpec.UNSPECIFIED
 import android.view.View.MeasureSpec.getSize
 import android.view.View.MeasureSpec.makeMeasureSpec
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
 import androidx.appcompat.widget.AppCompatCheckBox
 import com.afollestad.materialdialogs.R
 import com.afollestad.materialdialogs.WhichButton
@@ -39,7 +41,8 @@ import com.afollestad.materialdialogs.utils.isVisible
  *
  * @author Aidan Follestad (afollestad)
  */
-internal class DialogActionButtonLayout(
+@RestrictTo(LIBRARY_GROUP)
+class DialogActionButtonLayout(
   context: Context,
   attrs: AttributeSet? = null
 ) : BaseSubLayout(context, attrs) {
@@ -67,8 +70,6 @@ internal class DialogActionButtonLayout(
     get() = actionButtons.filter { it.isVisible() }
         .toTypedArray()
 
-  fun shouldBeVisible() = visibleButtons.isNotEmpty() || checkBoxPrompt.isVisible()
-
   override fun onFinishInflate() {
     super.onFinishInflate()
     actionButtons = arrayOf(
@@ -80,7 +81,7 @@ internal class DialogActionButtonLayout(
 
     for ((i, btn) in actionButtons.withIndex()) {
       val which = WhichButton.fromIndex(i)
-      btn.setOnClickListener { dialogParent().dialog.onActionButtonClicked(which) }
+      btn.setOnClickListener { dialog.onActionButtonClicked(which) }
     }
   }
 
@@ -104,8 +105,8 @@ internal class DialogActionButtonLayout(
     }
 
     // Buttons plus any spacing around that makes up the "frame"
-    val baseContext = dialogParent().dialog.context
-    val appContext = dialogParent().dialog.windowContext
+    val baseContext = dialog.context
+    val appContext = dialog.windowContext
     for (button in visibleButtons) {
       button.update(
           baseContext = baseContext,
@@ -276,3 +277,11 @@ internal class DialogActionButtonLayout(
     else -> buttonFrameSpecHeight
   }
 }
+
+@RestrictTo(LIBRARY_GROUP)
+fun DialogActionButtonLayout?.shouldBeVisible(): Boolean {
+  if (this == null) {
+    return false
+  }
+  return visibleButtons.isNotEmpty() || checkBoxPrompt.isVisible()
+}

+ 7 - 6
core/src/main/java/com/afollestad/materialdialogs/internal/list/DialogRecyclerView.kt

@@ -17,13 +17,15 @@ package com.afollestad.materialdialogs.internal.list
 
 import android.content.Context
 import android.util.AttributeSet
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
 import androidx.recyclerview.widget.GridLayoutManager
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 import com.afollestad.materialdialogs.MaterialDialog
 import com.afollestad.materialdialogs.internal.main.DialogLayout
 import com.afollestad.materialdialogs.utils.invalidateDividers
-import com.afollestad.materialdialogs.utils.waitForLayout
+import com.afollestad.materialdialogs.utils.MDUtil.waitForWidth
 
 typealias InvalidateDividersDelegate = (scrolledDown: Boolean, atBottom: Boolean) -> Unit
 
@@ -33,6 +35,7 @@ typealias InvalidateDividersDelegate = (scrolledDown: Boolean, atBottom: Boolean
  *
  * @author Aidan Follestad (afollestad)
  */
+@RestrictTo(LIBRARY_GROUP)
 class DialogRecyclerView(
   context: Context,
   attrs: AttributeSet? = null
@@ -51,7 +54,7 @@ class DialogRecyclerView(
 
   override fun onAttachedToWindow() {
     super.onAttachedToWindow()
-    waitForLayout {
+    waitForWidth {
       invalidateDividers()
       invalidateOverScroll()
     }
@@ -74,8 +77,7 @@ class DialogRecyclerView(
   }
 
   private fun isAtTop(): Boolean {
-    val lm = layoutManager
-    return when (lm) {
+    return when (val lm = layoutManager) {
       is LinearLayoutManager -> lm.findFirstCompletelyVisibleItemPosition() == 0
       is GridLayoutManager -> lm.findFirstCompletelyVisibleItemPosition() == 0
       else -> false
@@ -84,8 +86,7 @@ class DialogRecyclerView(
 
   private fun isAtBottom(): Boolean {
     val lastIndex = adapter!!.itemCount - 1
-    val lm = layoutManager
-    return when (lm) {
+    return when (val lm = layoutManager) {
       is LinearLayoutManager -> lm.findLastCompletelyVisibleItemPosition() == lastIndex
       is GridLayoutManager -> lm.findLastCompletelyVisibleItemPosition() == lastIndex
       else -> false

+ 1 - 1
core/src/main/java/com/afollestad/materialdialogs/internal/list/MultiChoiceDialogAdapter.kt

@@ -31,8 +31,8 @@ import com.afollestad.materialdialogs.list.MultiChoiceListener
 import com.afollestad.materialdialogs.list.getItemSelector
 import com.afollestad.materialdialogs.utils.MDUtil.createColorSelector
 import com.afollestad.materialdialogs.utils.MDUtil.maybeSetTextColor
+import com.afollestad.materialdialogs.utils.MDUtil.inflate
 import com.afollestad.materialdialogs.utils.appendAll
-import com.afollestad.materialdialogs.utils.inflate
 import com.afollestad.materialdialogs.utils.pullIndices
 import com.afollestad.materialdialogs.utils.removeAll
 import com.afollestad.materialdialogs.utils.resolveColors

+ 1 - 1
core/src/main/java/com/afollestad/materialdialogs/internal/list/PlainListDialogAdapter.kt

@@ -28,7 +28,7 @@ import com.afollestad.materialdialogs.actions.hasActionButtons
 import com.afollestad.materialdialogs.list.ItemListener
 import com.afollestad.materialdialogs.list.getItemSelector
 import com.afollestad.materialdialogs.utils.MDUtil.maybeSetTextColor
-import com.afollestad.materialdialogs.utils.inflate
+import com.afollestad.materialdialogs.utils.MDUtil.inflate
 
 private const val KEY_ACTIVATED_INDEX = "activated_index"
 

+ 1 - 1
core/src/main/java/com/afollestad/materialdialogs/internal/list/SingleChoiceDialogAdapter.kt

@@ -31,7 +31,7 @@ import com.afollestad.materialdialogs.list.SingleChoiceListener
 import com.afollestad.materialdialogs.list.getItemSelector
 import com.afollestad.materialdialogs.utils.MDUtil.createColorSelector
 import com.afollestad.materialdialogs.utils.MDUtil.maybeSetTextColor
-import com.afollestad.materialdialogs.utils.inflate
+import com.afollestad.materialdialogs.utils.MDUtil.inflate
 import com.afollestad.materialdialogs.utils.resolveColors
 
 /** @author Aidan Follestad (afollestad) */

+ 7 - 6
core/src/main/java/com/afollestad/materialdialogs/internal/main/BaseSubLayout.kt

@@ -20,18 +20,23 @@ import android.graphics.Paint
 import android.graphics.Paint.Style.STROKE
 import android.util.AttributeSet
 import android.view.ViewGroup
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
+import com.afollestad.materialdialogs.MaterialDialog
 import com.afollestad.materialdialogs.R
 import com.afollestad.materialdialogs.R.attr
 import com.afollestad.materialdialogs.utils.MDUtil.dimenPx
 import com.afollestad.materialdialogs.utils.MDUtil.resolveColor
 
-internal abstract class BaseSubLayout(
+@RestrictTo(LIBRARY_GROUP)
+abstract class BaseSubLayout internal constructor(
   context: Context,
   attrs: AttributeSet? = null
 ) : ViewGroup(context, attrs) {
 
   private val dividerPaint = Paint()
   protected val dividerHeight = dimenPx(R.dimen.md_divider_height)
+  lateinit var dialog: MaterialDialog
 
   var drawDivider: Boolean = false
     set(value) {
@@ -47,16 +52,12 @@ internal abstract class BaseSubLayout(
     dividerPaint.isAntiAlias = true
   }
 
-  protected fun dialogParent(): DialogLayout {
-    return parent as DialogLayout
-  }
-
   protected fun dividerPaint(): Paint {
     dividerPaint.color = getDividerColor()
     return dividerPaint
   }
 
   private fun getDividerColor(): Int {
-    return resolveColor(dialogParent().dialog.context, attr = attr.md_divider_color)
+    return resolveColor(dialog.context, attr = attr.md_divider_color)
   }
 }

+ 12 - 7
core/src/main/java/com/afollestad/materialdialogs/internal/main/DialogContentLayout.kt

@@ -28,17 +28,20 @@ import android.view.ViewGroup
 import android.widget.FrameLayout
 import android.widget.TextView
 import androidx.annotation.LayoutRes
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
 import androidx.annotation.StringRes
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.LayoutManager
 import com.afollestad.materialdialogs.MaterialDialog
 import com.afollestad.materialdialogs.R
 import com.afollestad.materialdialogs.internal.button.DialogActionButtonLayout
 import com.afollestad.materialdialogs.internal.list.DialogRecyclerView
 import com.afollestad.materialdialogs.utils.MDUtil.maybeSetTextColor
 import com.afollestad.materialdialogs.utils.MDUtil.resolveString
+import com.afollestad.materialdialogs.utils.MDUtil.updatePadding
 import com.afollestad.materialdialogs.utils.inflate
-import com.afollestad.materialdialogs.utils.updatePadding
 
 /**
  * The middle section of the dialog, between [DialogTitleLayout] and
@@ -47,19 +50,20 @@ import com.afollestad.materialdialogs.utils.updatePadding
  *
  * @author Aidan Follestad (afollestad)
  */
-internal class DialogContentLayout(
+@RestrictTo(LIBRARY_GROUP)
+class DialogContentLayout(
   context: Context,
   attrs: AttributeSet? = null
 ) : FrameLayout(context, attrs) {
 
   private val rootLayout: DialogLayout?
     get() = parent as DialogLayout
-  private var scrollView: DialogScrollView? = null
   private var scrollFrame: ViewGroup? = null
   private var messageTextView: TextView? = null
 
-  internal var recyclerView: DialogRecyclerView? = null
-  internal var customView: View? = null
+  var scrollView: DialogScrollView? = null
+  var recyclerView: DialogRecyclerView? = null
+  var customView: View? = null
 
   fun setMessage(
     dialog: MaterialDialog,
@@ -89,12 +93,13 @@ internal class DialogContentLayout(
 
   fun addRecyclerView(
     dialog: MaterialDialog,
-    adapter: RecyclerView.Adapter<*>
+    adapter: RecyclerView.Adapter<*>,
+    layoutManager: LayoutManager?
   ) {
     if (recyclerView == null) {
       recyclerView = inflate<DialogRecyclerView>(R.layout.md_dialog_stub_recyclerview).apply {
         this.attach(dialog)
-        this.layoutManager = LinearLayoutManager(dialog.windowContext)
+        this.layoutManager = layoutManager ?: LinearLayoutManager(dialog.windowContext)
       }
       addView(recyclerView)
     }

+ 56 - 36
core/src/main/java/com/afollestad/materialdialogs/internal/main/DialogLayout.kt

@@ -35,6 +35,7 @@ import androidx.annotation.ColorInt
 import com.afollestad.materialdialogs.MaterialDialog
 import com.afollestad.materialdialogs.R
 import com.afollestad.materialdialogs.internal.button.DialogActionButtonLayout
+import com.afollestad.materialdialogs.internal.button.shouldBeVisible
 import com.afollestad.materialdialogs.utils.MDUtil.dimenPx
 import com.afollestad.materialdialogs.utils.dp
 import com.afollestad.materialdialogs.utils.isRtl
@@ -46,7 +47,7 @@ import com.afollestad.materialdialogs.utils.isVisible
  *
  * @author Aidan Follestad (afollestad)
  */
-internal class DialogLayout(
+class DialogLayout(
   context: Context,
   attrs: AttributeSet?
 ) : FrameLayout(context, attrs) {
@@ -62,10 +63,11 @@ internal class DialogLayout(
   internal val frameMarginVertical = dimenPx(R.dimen.md_dialog_frame_margin_vertical)
   internal val frameMarginVerticalLess = dimenPx(R.dimen.md_dialog_frame_margin_vertical_less)
 
-  internal lateinit var dialog: MaterialDialog
-  internal lateinit var titleLayout: DialogTitleLayout
-  internal lateinit var contentLayout: DialogContentLayout
-  internal lateinit var buttonsLayout: DialogActionButtonLayout
+  lateinit var dialog: MaterialDialog
+  lateinit var titleLayout: DialogTitleLayout
+  lateinit var contentLayout: DialogContentLayout
+  var buttonsLayout: DialogActionButtonLayout? = null
+  private var isButtonsLayoutAChild: Boolean = true
 
   override fun onFinishInflate() {
     super.onFinishInflate()
@@ -74,12 +76,25 @@ internal class DialogLayout(
     buttonsLayout = findViewById(R.id.md_button_layout)
   }
 
-  internal fun invalidateDividers(
-    scrolledDown: Boolean,
-    atBottom: Boolean
+  fun attachDialog(dialog: MaterialDialog) {
+    titleLayout.dialog = dialog
+    buttonsLayout?.dialog = dialog
+  }
+
+  fun attachButtonsLayout(buttonsLayout: DialogActionButtonLayout) {
+    this.buttonsLayout = buttonsLayout
+    this.isButtonsLayoutAChild = false
+  }
+
+  /**
+   * Shows or hides the top and bottom dividers, which separate the title, content, and buttons.
+   */
+  fun invalidateDividers(
+    showTop: Boolean,
+    showBottom: Boolean
   ) {
-    titleLayout.drawDivider = scrolledDown
-    buttonsLayout.drawDivider = atBottom
+    titleLayout.drawDivider = showTop
+    buttonsLayout?.drawDivider = showBottom
   }
 
   override fun onMeasure(
@@ -88,7 +103,7 @@ internal class DialogLayout(
   ) {
     val specWidth = getSize(widthMeasureSpec)
     var specHeight = getSize(heightMeasureSpec)
-    if (specHeight > maxHeight) {
+    if (maxHeight in 1 until specHeight) {
       specHeight = maxHeight
     }
 
@@ -97,14 +112,14 @@ internal class DialogLayout(
         makeMeasureSpec(0, UNSPECIFIED)
     )
     if (buttonsLayout.shouldBeVisible()) {
-      buttonsLayout.measure(
+      buttonsLayout!!.measure(
           makeMeasureSpec(specWidth, EXACTLY),
           makeMeasureSpec(0, UNSPECIFIED)
       )
     }
 
     val titleAndButtonsHeight =
-      titleLayout.measuredHeight + buttonsLayout.measuredHeight
+      titleLayout.measuredHeight + (buttonsLayout?.measuredHeight ?: 0)
     val remainingHeight = specHeight - titleAndButtonsHeight
     contentLayout.measure(
         makeMeasureSpec(specWidth, EXACTLY),
@@ -113,7 +128,7 @@ internal class DialogLayout(
 
     val totalHeight = titleLayout.measuredHeight +
         contentLayout.measuredHeight +
-        buttonsLayout.measuredHeight
+        (buttonsLayout?.measuredHeight ?: 0)
     setMeasuredDimension(specWidth, totalHeight)
   }
 
@@ -135,18 +150,23 @@ internal class DialogLayout(
         titleBottom
     )
 
-    val buttonsTop =
-      measuredHeight - buttonsLayout.measuredHeight
-    if (buttonsLayout.shouldBeVisible()) {
-      val buttonsLeft = 0
-      val buttonsRight = measuredWidth
-      val buttonsBottom = measuredHeight
-      buttonsLayout.layout(
-          buttonsLeft,
-          buttonsTop,
-          buttonsRight,
-          buttonsBottom
-      )
+    val buttonsTop: Int
+    if (isButtonsLayoutAChild) {
+      buttonsTop =
+        measuredHeight - (buttonsLayout?.measuredHeight ?: 0)
+      if (buttonsLayout.shouldBeVisible()) {
+        val buttonsLeft = 0
+        val buttonsRight = measuredWidth
+        val buttonsBottom = measuredHeight
+        buttonsLayout!!.layout(
+            buttonsLeft,
+            buttonsTop,
+            buttonsRight,
+            buttonsBottom
+        )
+      }
+    } else {
+      buttonsTop = measuredHeight
     }
 
     val contentLeft = 0
@@ -192,10 +212,10 @@ internal class DialogLayout(
     }
     canvas.verticalLine(CYAN, start = buttonsRight)
 
-    if (buttonsLayout.stackButtons) {
+    if (buttonsLayout?.stackButtons == true) {
       // Fill visible parts of buttons
-      var currentTop = buttonsLayout.top + dp(8)
-      for (button in buttonsLayout.visibleButtons) {
+      var currentTop = buttonsLayout!!.top + dp(8)
+      for (button in buttonsLayout!!.visibleButtons) {
         val currentBottom = currentTop + dp(36)
         canvas.box(
             CYAN,
@@ -209,18 +229,18 @@ internal class DialogLayout(
       }
 
       // Blue line over the top of the buttons layout
-      canvas.horizontalLine(BLUE, start = buttonsLayout.top.toFloat())
+      canvas.horizontalLine(BLUE, start = buttonsLayout!!.top.toFloat())
 
       // Red line over the top edge of the buttons
-      val buttonsTop = buttonsLayout.top.toFloat() + dp(8)
+      val buttonsTop = buttonsLayout!!.top.toFloat() + dp(8)
       val buttonsBottom = measuredHeight.toFloat() - dp(8)
       canvas.horizontalLine(RED, start = buttonsTop)
       canvas.horizontalLine(RED, start = buttonsBottom)
-    } else {
+    } else if (buttonsLayout != null) {
       // Fill visible parts of buttons
-      for (button in buttonsLayout.visibleButtons) {
-        val top = buttonsLayout.top + button.top.toFloat() + dp(8)
-        val bottom = buttonsLayout.bottom.toFloat() - dp(8)
+      for (button in buttonsLayout!!.visibleButtons) {
+        val top = buttonsLayout!!.top + button.top.toFloat() + dp(8)
+        val bottom = buttonsLayout!!.bottom.toFloat() - dp(8)
         val left = button.left.toFloat() + dp(4)
         val right = button.right.toFloat() - dp(4)
         canvas.box(
@@ -234,7 +254,7 @@ internal class DialogLayout(
       }
 
       // Magenta line over the top of the buttons layout
-      canvas.horizontalLine(MAGENTA, start = buttonsLayout.top.toFloat())
+      canvas.horizontalLine(MAGENTA, start = buttonsLayout!!.top.toFloat())
       // Red line over the top and bottom edge of the buttons
       val buttonsTop = measuredHeight.toFloat() - (dp(52) - dp(8))
       val buttonsBottom = measuredHeight.toFloat() - dp(8)

+ 8 - 5
core/src/main/java/com/afollestad/materialdialogs/internal/main/DialogScrollView.kt

@@ -18,7 +18,9 @@ package com.afollestad.materialdialogs.internal.main
 import android.content.Context
 import android.util.AttributeSet
 import android.widget.ScrollView
-import com.afollestad.materialdialogs.utils.waitForLayout
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
+import com.afollestad.materialdialogs.utils.MDUtil.waitForWidth
 
 /**
  * A [ScrollView] which reports whether or not it's scrollable based on whether the content
@@ -27,7 +29,8 @@ import com.afollestad.materialdialogs.utils.waitForLayout
  *
  * @author Aidan Follestad (afollestad)
  */
-internal class DialogScrollView(
+@RestrictTo(LIBRARY_GROUP)
+class DialogScrollView(
   context: Context?,
   attrs: AttributeSet? = null
 ) : ScrollView(context, attrs) {
@@ -39,7 +42,7 @@ internal class DialogScrollView(
 
   override fun onAttachedToWindow() {
     super.onAttachedToWindow()
-    waitForLayout {
+    waitForWidth {
       invalidateDividers()
       invalidateOverScroll()
     }
@@ -55,9 +58,9 @@ internal class DialogScrollView(
     invalidateDividers()
   }
 
-  private fun invalidateDividers() {
+  fun invalidateDividers() {
     if (childCount == 0 || measuredHeight == 0 || !isScrollable) {
-      rootView?.invalidateDividers(scrolledDown = false, atBottom = false)
+      rootView?.invalidateDividers(showTop = false, showBottom = false)
       return
     }
     val view = getChildAt(childCount - 1)

+ 4 - 1
core/src/main/java/com/afollestad/materialdialogs/internal/main/DialogTitleLayout.kt

@@ -25,6 +25,8 @@ import android.view.View.MeasureSpec.getSize
 import android.view.View.MeasureSpec.makeMeasureSpec
 import android.widget.ImageView
 import android.widget.TextView
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
 import com.afollestad.materialdialogs.R
 import com.afollestad.materialdialogs.utils.MDUtil.dimenPx
 import com.afollestad.materialdialogs.utils.isNotVisible
@@ -37,7 +39,8 @@ import java.lang.Math.max
  *
  * @author Aidan Follestad (afollestad)
  */
-internal class DialogTitleLayout(
+@RestrictTo(LIBRARY_GROUP)
+class DialogTitleLayout(
   context: Context,
   attrs: AttributeSet? = null
 ) : BaseSubLayout(context, attrs) {

+ 16 - 32
core/src/main/java/com/afollestad/materialdialogs/list/DialogListExt.kt

@@ -27,15 +27,15 @@ import androidx.annotation.CheckResult
 import androidx.annotation.RestrictTo
 import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
 import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.LayoutManager
 import com.afollestad.materialdialogs.MaterialDialog
 import com.afollestad.materialdialogs.R
-import com.afollestad.materialdialogs.assertOneSet
-import com.afollestad.materialdialogs.internal.list.MultiChoiceDialogAdapter
+import com.afollestad.materialdialogs.internal.list.DialogAdapter
 import com.afollestad.materialdialogs.internal.list.PlainListDialogAdapter
-import com.afollestad.materialdialogs.internal.list.SingleChoiceDialogAdapter
+import com.afollestad.materialdialogs.utils.MDUtil.assertOneSet
+import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
 import com.afollestad.materialdialogs.utils.MDUtil.ifNotZero
 import com.afollestad.materialdialogs.utils.MDUtil.resolveDrawable
-import com.afollestad.materialdialogs.utils.getStringArray
 import com.afollestad.materialdialogs.utils.resolveColor
 
 /**
@@ -60,11 +60,13 @@ import com.afollestad.materialdialogs.utils.resolveColor
  * Cannot be used in combination with message, input, and some other types of dialogs.
  */
 @CheckResult fun MaterialDialog.customListAdapter(
-  adapter: RecyclerView.Adapter<*>
+  adapter: RecyclerView.Adapter<*>,
+  layoutManager: LayoutManager? = null
 ): MaterialDialog {
   this.view.contentLayout.addRecyclerView(
       dialog = this,
-      adapter = adapter
+      adapter = adapter,
+      layoutManager = layoutManager
   )
   return this
 }
@@ -84,7 +86,7 @@ import com.afollestad.materialdialogs.utils.resolveColor
   selection: ItemListener = null
 ): MaterialDialog {
   assertOneSet("listItems", items, res)
-  val array = items ?: getStringArray(res)?.toList() ?: return this
+  val array = items ?: windowContext.getStringArray(res).toList()
 
   if (getListAdapter() != null) {
     return updateListItems(
@@ -117,35 +119,17 @@ fun MaterialDialog.updateListItems(
   disabledIndices: IntArray? = null
 ): MaterialDialog {
   assertOneSet("updateListItems", items, res)
-  val array = items ?: getStringArray(res)?.toList() ?: return this
+  val array = items ?: windowContext.getStringArray(res).toList()
   val adapter = getListAdapter()
   check(adapter != null) {
     "updateListItems(...) can't be used before you've created a list dialog."
   }
-  when (adapter) {
-    is PlainListDialogAdapter -> {
-      adapter.replaceItems(array)
-      if (disabledIndices != null) {
-        adapter.disableItems(disabledIndices)
-      }
-      return this
-    }
-    is SingleChoiceDialogAdapter -> {
-      adapter.replaceItems(array)
-      if (disabledIndices != null) {
-        adapter.disableItems(disabledIndices)
-      }
-    }
-    is MultiChoiceDialogAdapter -> {
-      adapter.replaceItems(array)
-      if (disabledIndices != null) {
-        adapter.disableItems(disabledIndices)
-      }
-    }
-    else -> {
-      throw UnsupportedOperationException(
-          "updateListItems() cannot work with adapter of type ${adapter::class.java}"
-      )
+  if (adapter is DialogAdapter<*, *>) {
+    @Suppress("UNCHECKED_CAST")
+    (adapter as DialogAdapter<String, *>).replaceItems(array)
+
+    if (disabledIndices != null) {
+      adapter.disableItems(disabledIndices)
     }
   }
   return this

+ 3 - 3
core/src/main/java/com/afollestad/materialdialogs/list/DialogMultiChoiceExt.kt

@@ -22,10 +22,10 @@ import androidx.annotation.CheckResult
 import com.afollestad.materialdialogs.MaterialDialog
 import com.afollestad.materialdialogs.WhichButton.POSITIVE
 import com.afollestad.materialdialogs.actions.setActionButtonEnabled
-import com.afollestad.materialdialogs.assertOneSet
+import com.afollestad.materialdialogs.utils.MDUtil.assertOneSet
 import com.afollestad.materialdialogs.internal.list.DialogAdapter
 import com.afollestad.materialdialogs.internal.list.MultiChoiceDialogAdapter
-import com.afollestad.materialdialogs.utils.getStringArray
+import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
 
 /**
  * @param res The string array resource to populate the list with.
@@ -47,7 +47,7 @@ import com.afollestad.materialdialogs.utils.getStringArray
   selection: MultiChoiceListener = null
 ): MaterialDialog {
   assertOneSet("listItemsMultiChoice", items, res)
-  val array = items ?: getStringArray(res)?.toList() ?: return this
+  val array = items ?: windowContext.getStringArray(res).toList()
 
   if (getListAdapter() != null) {
     return updateListItems(

+ 3 - 3
core/src/main/java/com/afollestad/materialdialogs/list/DialogSingleChoiceExt.kt

@@ -22,10 +22,10 @@ import androidx.annotation.CheckResult
 import com.afollestad.materialdialogs.MaterialDialog
 import com.afollestad.materialdialogs.WhichButton.POSITIVE
 import com.afollestad.materialdialogs.actions.setActionButtonEnabled
-import com.afollestad.materialdialogs.assertOneSet
+import com.afollestad.materialdialogs.utils.MDUtil.assertOneSet
 import com.afollestad.materialdialogs.internal.list.DialogAdapter
 import com.afollestad.materialdialogs.internal.list.SingleChoiceDialogAdapter
-import com.afollestad.materialdialogs.utils.getStringArray
+import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
 
 /**
  * @param res The string array resource to populate the list with.
@@ -44,7 +44,7 @@ import com.afollestad.materialdialogs.utils.getStringArray
   selection: SingleChoiceListener = null
 ): MaterialDialog {
   assertOneSet("listItemsSingleChoice", items, res)
-  val array = items ?: getStringArray(res)?.toList() ?: return this
+  val array = items ?: windowContext.getStringArray(res).toList()
   require(initialSelection >= -1 || initialSelection < array.size) {
     "Initial selection $initialSelection must be between -1 and " +
         "the size of your items array ${array.size}"

+ 13 - 60
core/src/main/java/com/afollestad/materialdialogs/utils/Dialogs.kt

@@ -18,64 +18,24 @@ package com.afollestad.materialdialogs.utils
 import android.content.Context.INPUT_METHOD_SERVICE
 import android.graphics.Typeface
 import android.graphics.drawable.Drawable
-import android.graphics.drawable.GradientDrawable
 import android.view.View
-import android.view.WindowManager
-import android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
 import android.view.inputmethod.InputMethodManager
 import android.widget.ImageView
 import android.widget.TextView
 import androidx.annotation.ColorInt
 import androidx.annotation.DrawableRes
-import androidx.annotation.Px
 import androidx.annotation.RestrictTo
 import androidx.annotation.RestrictTo.Scope
 import androidx.annotation.StringRes
 import com.afollestad.materialdialogs.MaterialDialog
 import com.afollestad.materialdialogs.R
-import com.afollestad.materialdialogs.WhichButton.NEGATIVE
-import com.afollestad.materialdialogs.WhichButton.POSITIVE
-import com.afollestad.materialdialogs.actions.getActionButton
 import com.afollestad.materialdialogs.callbacks.invokeAll
 import com.afollestad.materialdialogs.checkbox.getCheckBoxPrompt
 import com.afollestad.materialdialogs.customview.CUSTOM_VIEW_NO_VERTICAL_PADDING
 import com.afollestad.materialdialogs.utils.MDUtil.maybeSetTextColor
+import com.afollestad.materialdialogs.utils.MDUtil.resolveDimen
 import com.afollestad.materialdialogs.utils.MDUtil.resolveDrawable
 import com.afollestad.materialdialogs.utils.MDUtil.resolveString
-import kotlin.math.min
-
-internal fun MaterialDialog.setWindowConstraints(
-  @Px maxWidth: Int? = null
-) {
-  if (maxWidth == 0) {
-    // Postpone
-    return
-  }
-
-  val win = window ?: return
-  win.setSoftInputMode(SOFT_INPUT_ADJUST_RESIZE)
-  val wm = win.windowManager ?: return
-  val res = context.resources
-  val (windowWidth, windowHeight) = wm.getWidthAndHeight()
-
-  val windowVerticalPadding = res.getDimensionPixelSize(
-      R.dimen.md_dialog_vertical_margin
-  )
-  val windowHorizontalPadding = res.getDimensionPixelSize(
-      R.dimen.md_dialog_horizontal_margin
-  )
-  val calculatedWidth = windowWidth - windowHorizontalPadding * 2
-  val actualMaxWidth =
-    maxWidth ?: res.getDimensionPixelSize(R.dimen.md_dialog_max_width)
-
-  view.maxHeight = windowHeight - windowVerticalPadding * 2
-  val lp = WindowManager.LayoutParams()
-      .apply {
-        copyFrom(win.attributes)
-        width = min(actualMaxWidth, calculatedWidth)
-      }
-  win.attributes = lp
-}
 
 internal fun MaterialDialog.setDefaults() {
   // Background color and corner radius
@@ -91,9 +51,9 @@ internal fun MaterialDialog.setDefaults() {
 
 @RestrictTo(Scope.LIBRARY_GROUP)
 fun MaterialDialog.invalidateDividers(
-  scrolledDown: Boolean,
-  atBottom: Boolean
-) = view.invalidateDividers(scrolledDown, atBottom)
+  showTop: Boolean,
+  showBottom: Boolean
+) = view.invalidateDividers(showTop, showBottom)
 
 internal fun MaterialDialog.preShow() {
   val customViewNoVerticalPadding = config[CUSTOM_VIEW_NO_VERTICAL_PADDING] as? Boolean == true
@@ -116,18 +76,6 @@ internal fun MaterialDialog.preShow() {
   }
 }
 
-internal fun MaterialDialog.postShow() {
-  val negativeBtn = getActionButton(NEGATIVE)
-  if (negativeBtn.isVisible()) {
-    negativeBtn.post { negativeBtn.requestFocus() }
-    return
-  }
-  val positiveBtn = getActionButton(POSITIVE)
-  if (positiveBtn.isVisible()) {
-    positiveBtn.post { positiveBtn.requestFocus() }
-  }
-}
-
 internal fun MaterialDialog.populateIcon(
   imageView: ImageView,
   @DrawableRes iconRes: Int?,
@@ -179,9 +127,14 @@ internal fun MaterialDialog.hideKeyboard() {
 }
 
 internal fun MaterialDialog.colorBackground(@ColorInt color: Int): MaterialDialog {
-  window?.setBackgroundDrawable(GradientDrawable().apply {
-    cornerRadius = dimen(attr = R.attr.md_corner_radius)
-    setColor(color)
-  })
+  window?.let {
+    dialogBehavior.setBackgroundColor(
+        context = windowContext,
+        window = it,
+        view = view,
+        color = color,
+        cornerRounding = resolveDimen(windowContext, attr = R.attr.md_corner_radius)
+    )
+  }
   return this
 }

+ 1 - 1
core/src/main/java/com/afollestad/materialdialogs/utils/Dimens.kt

@@ -22,7 +22,7 @@ import androidx.annotation.AttrRes
 import androidx.annotation.DimenRes
 import com.afollestad.materialdialogs.MaterialDialog
 import com.afollestad.materialdialogs.R
-import com.afollestad.materialdialogs.assertOneSet
+import com.afollestad.materialdialogs.utils.MDUtil.assertOneSet
 
 internal fun MaterialDialog.dimen(
   @DimenRes res: Int? = null,

+ 1 - 1
core/src/main/java/com/afollestad/materialdialogs/utils/Fonts.kt

@@ -22,7 +22,7 @@ import androidx.annotation.CheckResult
 import androidx.annotation.FontRes
 import androidx.core.content.res.ResourcesCompat
 import com.afollestad.materialdialogs.MaterialDialog
-import com.afollestad.materialdialogs.assertOneSet
+import com.afollestad.materialdialogs.utils.MDUtil.assertOneSet
 
 @CheckResult internal fun MaterialDialog.font(
   @FontRes res: Int? = null,

+ 90 - 4
core/src/main/java/com/afollestad/materialdialogs/utils/MDUtil.kt

@@ -20,19 +20,25 @@ import android.content.Context
 import android.content.res.ColorStateList
 import android.content.res.Configuration.ORIENTATION_LANDSCAPE
 import android.graphics.Color
+import android.graphics.Point
 import android.graphics.drawable.Drawable
 import android.text.Editable
 import android.text.Html
 import android.text.TextWatcher
+import android.view.LayoutInflater
 import android.view.View
+import android.view.ViewGroup
 import android.view.ViewTreeObserver
+import android.view.WindowManager
 import android.widget.EditText
 import android.widget.TextView
+import androidx.annotation.ArrayRes
 import androidx.annotation.AttrRes
 import androidx.annotation.ColorInt
 import androidx.annotation.ColorRes
 import androidx.annotation.DimenRes
 import androidx.annotation.DrawableRes
+import androidx.annotation.LayoutRes
 import androidx.annotation.RestrictTo
 import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
 import androidx.annotation.StringRes
@@ -140,7 +146,7 @@ object MDUtil {
   @RestrictTo(LIBRARY_GROUP) fun resolveInt(
     context: Context,
     @AttrRes attr: Int,
-    defaultValue: Int
+    defaultValue: Int = 0
   ): Int {
     val a = context.theme.obtainStyledAttributes(intArrayOf(attr))
     try {
@@ -150,6 +156,19 @@ object MDUtil {
     }
   }
 
+  @RestrictTo(LIBRARY_GROUP) fun resolveDimen(
+    context: Context,
+    @AttrRes attr: Int,
+    defaultValue: Float = 0f
+  ): Float {
+    val a = context.theme.obtainStyledAttributes(intArrayOf(attr))
+    try {
+      return a.getDimension(0, defaultValue)
+    } finally {
+      a.recycle()
+    }
+  }
+
   @RestrictTo(LIBRARY_GROUP) fun Int.isColorDark(threshold: Double = 0.5): Boolean {
     if (this == Color.TRANSPARENT) {
       return false
@@ -232,7 +251,7 @@ object MDUtil {
     )
   }
 
-  @RestrictTo(LIBRARY_GROUP) fun <T : View> T.waitForLayout(block: T.() -> Unit) {
+  @RestrictTo(LIBRARY_GROUP) fun <T : View> T.waitForWidth(block: T.() -> Unit) {
     if (measuredWidth > 0 && measuredHeight > 0) {
       this.block()
       return
@@ -246,11 +265,78 @@ object MDUtil {
           viewTreeObserver.removeOnGlobalLayoutListener(this)
           return
         }
-        if (measuredWidth > 0 && measuredHeight > 0) {
+        if (measuredWidth > 0 && measuredHeight > 0 && lastWidth != measuredWidth) {
           lastWidth = measuredWidth
-          this@waitForLayout.block()
+          this@waitForWidth.block()
+        }
+      }
+    })
+  }
+
+  @RestrictTo(LIBRARY_GROUP) fun <T : View> T.waitForHeight(block: T.() -> Unit) {
+    if (measuredWidth > 0 && measuredHeight > 0) {
+      this.block()
+      return
+    }
+
+    viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
+      var lastHeight: Int? = null
+
+      override fun onGlobalLayout() {
+        if (lastHeight != null && lastHeight == measuredHeight) {
+          viewTreeObserver.removeOnGlobalLayoutListener(this)
+          return
+        }
+        if (measuredWidth > 0 && measuredHeight > 0 && lastHeight != measuredHeight) {
+          lastHeight = measuredHeight
+          this@waitForHeight.block()
         }
       }
     })
   }
+
+  @RestrictTo(LIBRARY_GROUP) fun WindowManager.getWidthAndHeight(): Pair<Int, Int> {
+    val size = Point()
+    defaultDisplay.getSize(size)
+    return Pair(size.x, size.y)
+  }
+
+  @RestrictTo(LIBRARY_GROUP) fun <T : View> T?.updatePadding(
+    left: Int = this?.paddingLeft ?: 0,
+    top: Int = this?.paddingTop ?: 0,
+    right: Int = this?.paddingRight ?: 0,
+    bottom: Int = this?.paddingBottom ?: 0
+  ) {
+    if (this != null &&
+        left == this.paddingLeft &&
+        top == this.paddingTop &&
+        right == this.paddingRight &&
+        bottom == this.paddingBottom
+    ) {
+      // no change needed, don't want to invalidate layout
+      return
+    }
+    this?.setPadding(left, top, right, bottom)
+  }
+
+  @RestrictTo(LIBRARY_GROUP) fun assertOneSet(
+    method: String,
+    b: Any?,
+    a: Int?
+  ) {
+    if (a == null && b == null) {
+      throw IllegalArgumentException("$method: You must specify a resource ID or literal value")
+    }
+  }
+
+  @RestrictTo(LIBRARY_GROUP) fun Context.getStringArray(@ArrayRes res: Int?): Array<String> {
+    return if (res != null) return resources.getStringArray(res) else emptyArray()
+  }
+
+  @Suppress("UNCHECKED_CAST")
+  @RestrictTo(LIBRARY_GROUP)
+  fun <R : View> ViewGroup.inflate(
+    ctxt: Context = context,
+    @LayoutRes res: Int
+  ) = LayoutInflater.from(ctxt).inflate(res, this, false) as R
 }

+ 0 - 36
core/src/main/java/com/afollestad/materialdialogs/utils/Views.kt

@@ -15,25 +15,17 @@
  */
 package com.afollestad.materialdialogs.utils
 
-import android.content.Context
 import android.os.Build.VERSION.SDK_INT
 import android.os.Build.VERSION_CODES.JELLY_BEAN_MR1
 import android.view.Gravity
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
-import android.view.ViewTreeObserver
 import android.widget.Button
 import android.widget.TextView
 import androidx.annotation.LayoutRes
 import com.afollestad.materialdialogs.MaterialDialog
 
-@Suppress("UNCHECKED_CAST")
-internal fun <R : View> ViewGroup.inflate(
-  ctxt: Context = context,
-  @LayoutRes res: Int
-) = LayoutInflater.from(ctxt).inflate(res, this, false) as R
-
 @Suppress("UNCHECKED_CAST")
 internal fun <T> MaterialDialog.inflate(
   @LayoutRes res: Int,
@@ -46,34 +38,6 @@ internal fun <T> ViewGroup.inflate(
   root: ViewGroup? = this
 ) = LayoutInflater.from(context).inflate(res, root, false) as T
 
-internal fun <T : View> T?.updatePadding(
-  left: Int = this?.paddingLeft ?: 0,
-  top: Int = this?.paddingTop ?: 0,
-  right: Int = this?.paddingRight ?: 0,
-  bottom: Int = this?.paddingBottom ?: 0
-) {
-  if (this != null &&
-      left == this.paddingLeft &&
-      top == this.paddingTop &&
-      right == this.paddingRight &&
-      bottom == this.paddingBottom
-  ) {
-    // no change needed, don't want to invalidate layout
-    return
-  }
-  this?.setPadding(left, top, right, bottom)
-}
-
-internal inline fun <T : View> T.waitForLayout(crossinline f: T.() -> Unit) =
-  viewTreeObserver.apply {
-    addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
-      override fun onGlobalLayout() {
-        removeOnGlobalLayoutListener(this)
-        this@waitForLayout.f()
-      }
-    })
-  }!!
-
 internal fun <T : View> T.isVisible(): Boolean {
   return if (this is Button) {
     this.visibility == View.VISIBLE && this.text.trim().isNotBlank()

+ 1 - 2
core/src/main/res/drawable/md_btn_selected.xml

@@ -5,8 +5,7 @@
     android:insetLeft="@dimen/md_action_button_inset_horizontal"
     android:insetRight="@dimen/md_action_button_inset_horizontal"
     android:insetTop="@dimen/md_action_button_inset_vertical">
-  <shape xmlns:android="http://schemas.android.com/apk/res/android"
-      android:shape="rectangle">
+  <shape android:shape="rectangle">
     <corners android:radius="@dimen/md_action_button_corner_radius"/>
     <solid android:color="@color/md_btn_selected"/>
     <padding

+ 1 - 2
core/src/main/res/drawable/md_btn_selected_dark.xml

@@ -5,8 +5,7 @@
     android:insetLeft="@dimen/md_action_button_inset_horizontal"
     android:insetRight="@dimen/md_action_button_inset_horizontal"
     android:insetTop="@dimen/md_action_button_inset_vertical">
-  <shape xmlns:android="http://schemas.android.com/apk/res/android"
-      android:shape="rectangle">
+  <shape android:shape="rectangle">
     <corners android:radius="@dimen/md_action_button_corner_radius"/>
     <solid android:color="@color/md_btn_selected_dark"/>
     <padding

+ 2 - 60
core/src/main/res/layout/md_dialog_base.xml

@@ -1,37 +1,12 @@
 <?xml version="1.0" encoding="utf-8"?>
 <com.afollestad.materialdialogs.internal.main.DialogLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/md_root"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     >
 
-  <com.afollestad.materialdialogs.internal.main.DialogTitleLayout
-      android:id="@+id/md_title_layout"
-      android:layout_width="match_parent"
-      android:layout_height="wrap_content"
-      >
-
-    <ImageView
-        android:id="@+id/md_icon_title"
-        android:layout_width="56dp"
-        android:layout_height="56dp"
-        android:scaleType="fitCenter"
-        android:visibility="gone"
-        tools:ignore="ContentDescription"
-        />
-
-    <com.afollestad.materialdialogs.internal.rtl.RtlTextView
-        android:id="@+id/md_text_title"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:visibility="gone"
-        tools:text="Use Google's location service?"
-        style="@style/MD_Dialog_Title_Text"
-        />
-
-  </com.afollestad.materialdialogs.internal.main.DialogTitleLayout>
+  <include layout="@layout/md_dialog_stub_title"/>
 
   <com.afollestad.materialdialogs.internal.main.DialogContentLayout
       android:id="@+id/md_content_layout"
@@ -39,39 +14,6 @@
       android:layout_height="wrap_content"
       />
 
-  <com.afollestad.materialdialogs.internal.button.DialogActionButtonLayout
-      android:id="@+id/md_button_layout"
-      android:layout_width="match_parent"
-      android:layout_height="wrap_content"
-      >
-
-    <androidx.appcompat.widget.AppCompatCheckBox
-        android:id="@+id/md_checkbox_prompt"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:gravity="start|center_vertical"
-        android:minHeight="@dimen/md_checkbox_prompt_height"
-        android:visibility="gone"
-        />
-
-    <com.afollestad.materialdialogs.internal.button.DialogActionButton
-        android:id="@+id/md_button_positive"
-        tools:text="Agree"
-        style="@style/MD_ActionButton"
-        />
-
-    <com.afollestad.materialdialogs.internal.button.DialogActionButton
-        android:id="@+id/md_button_negative"
-        tools:text="Disagree"
-        style="@style/MD_ActionButton"
-        />
-
-    <com.afollestad.materialdialogs.internal.button.DialogActionButton
-        android:id="@+id/md_button_neutral"
-        tools:text="Idk"
-        style="@style/MD_ActionButton"
-        />
-
-  </com.afollestad.materialdialogs.internal.button.DialogActionButtonLayout>
+  <include layout="@layout/md_dialog_stub_buttons"/>
 
 </com.afollestad.materialdialogs.internal.main.DialogLayout>

+ 35 - 0
core/src/main/res/layout/md_dialog_stub_buttons.xml

@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.afollestad.materialdialogs.internal.button.DialogActionButtonLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/md_button_layout"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    >
+
+  <androidx.appcompat.widget.AppCompatCheckBox
+      android:id="@+id/md_checkbox_prompt"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      style="@style/MD_Dialog_CheckPrompt"
+      />
+
+  <com.afollestad.materialdialogs.internal.button.DialogActionButton
+      android:id="@+id/md_button_positive"
+      tools:text="Agree"
+      style="@style/MD_ActionButton"
+      />
+
+  <com.afollestad.materialdialogs.internal.button.DialogActionButton
+      android:id="@+id/md_button_negative"
+      tools:text="Disagree"
+      style="@style/MD_ActionButton"
+      />
+
+  <com.afollestad.materialdialogs.internal.button.DialogActionButton
+      android:id="@+id/md_button_neutral"
+      tools:text="Idk"
+      style="@style/MD_ActionButton"
+      />
+
+</com.afollestad.materialdialogs.internal.button.DialogActionButtonLayout>

+ 25 - 0
core/src/main/res/layout/md_dialog_stub_title.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.afollestad.materialdialogs.internal.main.DialogTitleLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/md_title_layout"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    >
+
+  <ImageView
+      android:id="@+id/md_icon_title"
+      tools:ignore="ContentDescription"
+      style="@style/MD_Dialog_Icon"
+      />
+
+  <com.afollestad.materialdialogs.internal.rtl.RtlTextView
+      android:id="@+id/md_text_title"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:visibility="gone"
+      tools:text="Use Google's location service?"
+      style="@style/MD_Dialog_Title_Text"
+      />
+
+</com.afollestad.materialdialogs.internal.main.DialogTitleLayout>

+ 0 - 2
core/src/main/res/values/dimens.xml

@@ -20,12 +20,10 @@
   <dimen name="md_action_button_padding_vertical">4dp</dimen>
 
   <dimen name="md_action_button_min_width">64dp</dimen>
-  <dimen name="md_action_button_spacing">8dp</dimen>
   <dimen name="md_action_button_corner_radius">2dp</dimen>
   <dimen name="md_action_button_textsize">14sp</dimen>
 
   <dimen name="md_checkbox_prompt_height">48dp</dimen>
-  <dimen name="md_stacked_action_button_padding_horizontal">16dp</dimen>
 
   <dimen name="md_title_textsize">20sp</dimen>
   <dimen name="md_message_textsize">16sp</dimen>

+ 13 - 0
core/src/main/res/values/styles.xml

@@ -38,6 +38,13 @@
     <item name="android:windowExitAnimation">@anim/popup_exit</item>
   </style>
 
+  <style name="MD_Dialog_Icon">
+    <item name="android:layout_width">56dp</item>
+    <item name="android:layout_height">56dp</item>
+    <item name="android:scaleType">fitCenter</item>
+    <item name="android:visibility">gone</item>
+  </style>
+
   <style name="MD_Dialog_Title_Text">
     <item name="android:textColor">?android:textColorPrimary</item>
     <item name="android:textSize">@dimen/md_title_textsize</item>
@@ -74,6 +81,12 @@
     <item name="android:textAllCaps">false</item>
   </style>
 
+  <style name="MD_Dialog_CheckPrompt">
+    <item name="android:gravity">start|center_vertical</item>
+    <item name="android:minHeight">@dimen/md_checkbox_prompt_height</item>
+    <item name="android:visibility">gone</item>
+  </style>
+
   <style name="MD_ListItem">
     <item name="android:background">@null</item>
     <item name="android:layout_width">match_parent</item>

+ 3 - 3
dependencies.gradle

@@ -3,8 +3,8 @@ ext.versions = [
     minSdk              : 16,
     compileSdk          : 28,
     buildTools          : '28.0.3',
-    publishVersion      : '2.8.1',
-    publishVersionCode  : 242,
+    publishVersion      : '3.0.0-alpha1',
+    publishVersionCode  : 243,
 
     // Plugins
     gradlePlugin        : '3.4.0',
@@ -21,7 +21,7 @@ ext.versions = [
 
     // Kotlin
     kotlin              : '1.3.31',
-    coroutines          : '1.2.0',
+    coroutines          : '1.2.1',
 
     // afollestad
     assent              : '2.2.3',

+ 75 - 0
documentation/BOTTOMSHEETS.md

@@ -0,0 +1,75 @@
+# Bottom Sheets
+
+## Table of Contents
+
+1. [Gradle Dependency](#gradle-dependency)
+2. [Usage](#usage)
+3. [Item Grids](#item-grids)
+
+
+## Gradle Dependency
+
+[ ![Bottom Sheets](https://api.bintray.com/packages/drummer-aidan/maven/material-dialogs%3Abottomsheets/images/download.svg) ](https://bintray.com/drummer-aidan/maven/material-dialogs%3Abottomsheets/_latestVersion)
+
+The `bottomsheets` module contains extensions to turn modal dialogs into bottom sheets, among 
+other functionality like showing a grid of items.
+
+```gradle
+dependencies {
+  ...
+  implementation 'com.afollestad.material-dialogs:bottomsheets:3.0.0-alpha1'
+}
+```
+
+## Usage
+
+Making a dialog a bottom sheet is as simple as passing a constructed instance of `BottomSheet` 
+as the second parameter to `MaterialDialog`'s constructor.
+
+```kotlin
+MaterialDialog(this, BottomSheet()).show {
+  ...
+}
+```
+
+## Item Grids
+
+Since it's common to show a grid of items in a bottom sheet, this module contains a method to do 
+that.
+
+```kotlin
+val items = listOf(
+    BasicGridItem(R.drawable.some_icon, "One"),
+    BasicGridItem(R.drawable.another_icon, "Two"),
+    BasicGridItem(R.drawable.hello_world, "Three"),
+    BasicGridItem(R.drawable.material_dialogs, "Four")
+)
+
+MaterialDialog(this, BottomSheet()).show {
+  ...
+  gridItems(items) { _, index, item ->
+    toast("Selected item ${item.title} at index $index")
+  }
+}
+```
+
+Note that `gridItems` can take a list of anything that inherits from the `GridItem` interface, 
+if you wish to pass a custom item type. You just need to override the `title` value along with the 
+`populateIcon(ImageView)` function.
+
+---
+
+There a few extra parameters that you can provide, most of which are equivelent to what you can 
+provide to `listItems`. `customGridWidth` is an optional integer resource that allows you to set a 
+width for the grid - you can have different widths for different resource configurations (tablet, 
+landscape, etc.)
+
+```kotlin
+gridItems(
+  items: List<IT : GridItem>,
+  @IntegerRes customGridWidth: Int? = null,
+  disabledIndices: IntArray? = null,
+  waitForPositiveButton: Boolean = true,
+  selection: GridItemListener<IT> = null
+): MaterialDialog
+```

+ 1 - 1
documentation/COLOR.md

@@ -16,7 +16,7 @@ The `color` module contains extensions to the core module, such as a color choos
 ```gradle
 dependencies {
   ...
-  implementation 'com.afollestad.material-dialogs:color:2.8.1'
+  implementation 'com.afollestad.material-dialogs:color:3.0.0-alpha1'
 }
 ```
 

+ 1 - 1
documentation/CORE.md

@@ -35,7 +35,7 @@ core and normal-use functionality.
 ```gradle
 dependencies {
   ...
-  implementation 'com.afollestad.material-dialogs:core:2.8.1'
+  implementation 'com.afollestad.material-dialogs:core:3.0.0-alpha1'
 }
 ```
 

+ 1 - 1
documentation/DATETIME.md

@@ -16,7 +16,7 @@ The `datetime` module contains extensions to make date, time, and date-time pick
 ```gradle
 dependencies {
   ...
-  implementation 'com.afollestad.material-dialogs:datetime:2.8.1'
+  implementation 'com.afollestad.material-dialogs:datetime:3.0.0-alpha1'
 }
 ```
 

+ 1 - 1
documentation/FILES.md

@@ -23,7 +23,7 @@ The `files` module contains extensions to the core module, such as a file and fo
 ```gradle
 dependencies {
   ...
-  implementation 'com.afollestad.material-dialogs:files:2.8.1'
+  implementation 'com.afollestad.material-dialogs:files:3.0.0-alpha1'
 }
 ```
 

+ 1 - 1
documentation/INPUT.md

@@ -19,7 +19,7 @@ The `input` module contains extensions to the core module, such as a text input
 ```gradle
 dependencies {
   ...
-  implementation 'com.afollestad.material-dialogs:input:2.8.1'
+  implementation 'com.afollestad.material-dialogs:input:3.0.0-alpha1'
 }
 ```
 

+ 1 - 1
documentation/LIFECYCLE.md

@@ -15,7 +15,7 @@ The `lifecycle` module contains extensions to make dialogs work with AndroidX li
 ```gradle
 dependencies {
   ...
-  implementation 'com.afollestad.material-dialogs:lifecycle:2.8.1'
+  implementation 'com.afollestad.material-dialogs:lifecycle:3.0.0-alpha1'
 }
 ```
 

+ 1 - 0
sample/build.gradle

@@ -26,6 +26,7 @@ dependencies {
   implementation project(':files')
   implementation project(':color')
   implementation project(':datetime')
+  implementation project(':bottomsheets')
 
   implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:' + versions.kotlin
 

+ 112 - 114
sample/src/main/java/com/afollestad/materialdialogssample/MainActivity.kt

@@ -32,7 +32,12 @@ import androidx.appcompat.app.AppCompatActivity
 import com.afollestad.assent.Permission.READ_EXTERNAL_STORAGE
 import com.afollestad.assent.Permission.WRITE_EXTERNAL_STORAGE
 import com.afollestad.assent.runWithPermissions
+import com.afollestad.materialdialogs.DialogBehavior
 import com.afollestad.materialdialogs.MaterialDialog
+import com.afollestad.materialdialogs.ModalDialog
+import com.afollestad.materialdialogs.bottomsheets.BasicGridItem
+import com.afollestad.materialdialogs.bottomsheets.BottomSheet
+import com.afollestad.materialdialogs.bottomsheets.gridItems
 import com.afollestad.materialdialogs.callbacks.onCancel
 import com.afollestad.materialdialogs.callbacks.onDismiss
 import com.afollestad.materialdialogs.callbacks.onShow
@@ -55,11 +60,16 @@ import kotlinx.android.synthetic.main.activity_main.basic_buttons
 import kotlinx.android.synthetic.main.activity_main.basic_checkbox_titled_buttons
 import kotlinx.android.synthetic.main.activity_main.basic_html_content
 import kotlinx.android.synthetic.main.activity_main.basic_icon
-import kotlinx.android.synthetic.main.activity_main.basic_long
 import kotlinx.android.synthetic.main.activity_main.basic_long_titled_buttons
 import kotlinx.android.synthetic.main.activity_main.basic_stacked_buttons
 import kotlinx.android.synthetic.main.activity_main.basic_titled
 import kotlinx.android.synthetic.main.activity_main.basic_titled_buttons
+import kotlinx.android.synthetic.main.activity_main.bottomsheet_colorPicker
+import kotlinx.android.synthetic.main.activity_main.bottomsheet_customView
+import kotlinx.android.synthetic.main.activity_main.bottomsheet_dateTimePicker
+import kotlinx.android.synthetic.main.activity_main.bottomsheet_grid
+import kotlinx.android.synthetic.main.activity_main.bottomsheet_info
+import kotlinx.android.synthetic.main.activity_main.bottomsheet_list
 import kotlinx.android.synthetic.main.activity_main.buttons_callbacks
 import kotlinx.android.synthetic.main.activity_main.buttons_neutral
 import kotlinx.android.synthetic.main.activity_main.buttons_stacked
@@ -85,17 +95,10 @@ import kotlinx.android.synthetic.main.activity_main.input_counter
 import kotlinx.android.synthetic.main.activity_main.input_message
 import kotlinx.android.synthetic.main.activity_main.list
 import kotlinx.android.synthetic.main.activity_main.list_buttons
-import kotlinx.android.synthetic.main.activity_main.list_checkPrompt
 import kotlinx.android.synthetic.main.activity_main.list_checkPrompt_buttons
 import kotlinx.android.synthetic.main.activity_main.list_dont_wait_positive
 import kotlinx.android.synthetic.main.activity_main.list_long
-import kotlinx.android.synthetic.main.activity_main.list_long_buttons
-import kotlinx.android.synthetic.main.activity_main.list_long_items
-import kotlinx.android.synthetic.main.activity_main.list_long_items_buttons
-import kotlinx.android.synthetic.main.activity_main.list_long_items_titled
-import kotlinx.android.synthetic.main.activity_main.list_long_items_titled_buttons
 import kotlinx.android.synthetic.main.activity_main.list_long_titled
-import kotlinx.android.synthetic.main.activity_main.list_long_titled_buttons
 import kotlinx.android.synthetic.main.activity_main.list_titled
 import kotlinx.android.synthetic.main.activity_main.list_titled_buttons
 import kotlinx.android.synthetic.main.activity_main.list_titled_message_buttons
@@ -112,17 +115,6 @@ import kotlinx.android.synthetic.main.activity_main.time_picker
 
 /** @author Aidan Follestad (afollestad) */
 class MainActivity : AppCompatActivity() {
-
-  companion object {
-    const val KEY_PREFS = "prefs"
-    const val KEY_THEME = "KEY_THEME"
-    const val KEY_DEBUG_MODE = "debug_mode"
-
-    const val LIGHT = "light"
-    const val DARK = "dark"
-    const val CUSTOM = "custom"
-  }
-
   private var debugMode = false
   private lateinit var prefs: SharedPreferences
 
@@ -197,13 +189,6 @@ class MainActivity : AppCompatActivity() {
       }
     }
 
-    basic_long.setOnClickListener {
-      MaterialDialog(this).show {
-        message(R.string.loremIpsum)
-        debugMode(debugMode)
-      }
-    }
-
     basic_long_titled_buttons.setOnClickListener {
       MaterialDialog(this).show {
         title(R.string.useGoogleLocationServices)
@@ -312,17 +297,6 @@ class MainActivity : AppCompatActivity() {
       }
     }
 
-    list_long_buttons.setOnClickListener {
-      MaterialDialog(this).show {
-        listItems(R.array.states) { _, index, text ->
-          toast("Selected item $text at index $index")
-        }
-        positiveButton(R.string.agree)
-        negativeButton(R.string.disagree)
-        debugMode(debugMode)
-      }
-    }
-
     list_long_titled.setOnClickListener {
       MaterialDialog(this).show {
         title(R.string.states)
@@ -333,73 +307,6 @@ class MainActivity : AppCompatActivity() {
       }
     }
 
-    list_long_titled_buttons.setOnClickListener {
-      MaterialDialog(this).show {
-        title(R.string.states)
-        listItems(R.array.states) { _, index, text ->
-          toast("Selected item $text at index $index")
-        }
-        positiveButton(R.string.agree)
-        negativeButton(R.string.disagree)
-        debugMode(debugMode)
-      }
-    }
-
-    list_long_items.setOnClickListener {
-      MaterialDialog(this).show {
-        listItems(R.array.socialNetworks_longItems) { _, index, text ->
-          toast("Selected item $text at index $index")
-        }
-        debugMode(debugMode)
-      }
-    }
-
-    list_long_items_buttons.setOnClickListener {
-      MaterialDialog(this).show {
-        listItems(R.array.socialNetworks_longItems) { _, index, text ->
-          toast("Selected item $text at index $index")
-        }
-        positiveButton(R.string.agree)
-        negativeButton(R.string.disagree)
-        debugMode(debugMode)
-      }
-    }
-
-    list_long_items_titled.setOnClickListener {
-      MaterialDialog(this).show {
-        title(R.string.socialNetworks)
-        listItems(R.array.socialNetworks_longItems) { _, index, text ->
-          toast("Selected item $text at index $index")
-        }
-        debugMode(debugMode)
-      }
-    }
-
-    list_long_items_titled_buttons.setOnClickListener {
-      MaterialDialog(this).show {
-        title(R.string.socialNetworks)
-        listItems(R.array.socialNetworks_longItems) { _, index, text ->
-          toast("Selected item $text at index $index")
-        }
-        positiveButton(R.string.agree)
-        negativeButton(R.string.disagree)
-        debugMode(debugMode)
-      }
-    }
-
-    list_checkPrompt.setOnClickListener {
-      MaterialDialog(this).show {
-        title(R.string.socialNetworks)
-        listItems(R.array.socialNetworks_longItems) { _, index, text ->
-          toast("Selected item $text at index $index")
-        }
-        checkBoxPrompt(R.string.checkboxConfirm) { checked ->
-          toast("Checked? $checked")
-        }
-        debugMode(debugMode)
-      }
-    }
-
     list_checkPrompt_buttons.setOnClickListener {
       MaterialDialog(this).show {
         title(R.string.socialNetworks)
@@ -553,13 +460,13 @@ class MainActivity : AppCompatActivity() {
       MaterialDialog(this).show {
         title(R.string.useGoogleLocationServices)
         message(R.string.useGoogleLocationServicesPrompt)
-        positiveButton(R.string.agree) { _ ->
+        positiveButton(R.string.agree) {
           toast("On positive")
         }
-        negativeButton(R.string.disagree) { _ ->
+        negativeButton(R.string.disagree) {
           toast("On negative")
         }
-        neutralButton(R.string.more_info) { _ ->
+        neutralButton(R.string.more_info) {
           toast("On neutral")
         }
         debugMode(debugMode)
@@ -572,9 +479,9 @@ class MainActivity : AppCompatActivity() {
         message(R.string.useGoogleLocationServicesPrompt)
         positiveButton(R.string.agree)
         negativeButton(R.string.disagree)
-        onShow { _ -> toast("onShow") }
-        onCancel { _ -> toast("onCancel") }
-        onDismiss { _ -> toast("onDismiss") }
+        onShow { toast("onPreShow") }
+        onCancel { toast("onCancel") }
+        onDismiss { toast("onDismiss") }
         debugMode(debugMode)
       }
     }
@@ -660,6 +567,7 @@ class MainActivity : AppCompatActivity() {
         }
         positiveButton(R.string.select)
         negativeButton(android.R.string.cancel)
+
         debugMode(debugMode)
       }
     }
@@ -675,14 +583,15 @@ class MainActivity : AppCompatActivity() {
         }
         positiveButton(R.string.select)
         negativeButton(android.R.string.cancel)
+
         debugMode(debugMode)
       }
     }
 
     colorChooser_customColors.setOnClickListener {
-      val topLevel = intArrayOf(Color.TRANSPARENT, Color.RED, Color.YELLOW, Color.BLUE)
+      val topLevel = intArrayOf(TRANSPARENT, Color.RED, Color.YELLOW, Color.BLUE)
       val subLevel = arrayOf(
-          intArrayOf(Color.WHITE, Color.TRANSPARENT, Color.BLACK),
+          intArrayOf(Color.WHITE, TRANSPARENT, Color.BLACK),
           intArrayOf(Color.LTGRAY, Color.GRAY, Color.DKGRAY),
           intArrayOf(Color.GREEN),
           intArrayOf(Color.MAGENTA, Color.CYAN)
@@ -698,6 +607,7 @@ class MainActivity : AppCompatActivity() {
         }
         positiveButton(R.string.select)
         negativeButton(android.R.string.cancel)
+
         debugMode(debugMode)
       }
     }
@@ -712,6 +622,7 @@ class MainActivity : AppCompatActivity() {
         }
         positiveButton(R.string.select)
         negativeButton(android.R.string.cancel)
+
         debugMode(debugMode)
       }
     }
@@ -728,6 +639,7 @@ class MainActivity : AppCompatActivity() {
         }
         positiveButton(R.string.select)
         negativeButton(android.R.string.cancel)
+
         debugMode(debugMode)
       }
     }
@@ -745,6 +657,7 @@ class MainActivity : AppCompatActivity() {
         }
         positiveButton(R.string.select)
         negativeButton(android.R.string.cancel)
+
         debugMode(debugMode)
       }
     }
@@ -787,10 +700,85 @@ class MainActivity : AppCompatActivity() {
         debugMode(debugMode)
       }
     }
+
+    bottomsheet_info.setOnClickListener {
+      MaterialDialog(this, BottomSheet()).show {
+        title(R.string.useGoogleLocationServices)
+        message(R.string.useGoogleLocationServicesPrompt)
+        positiveButton(R.string.agree)
+        negativeButton(R.string.disagree)
+        debugMode(debugMode)
+      }
+    }
+
+    bottomsheet_list.setOnClickListener {
+      MaterialDialog(this, BottomSheet()).show {
+        listItems(R.array.states) { _, index, text ->
+          toast("Selected item $text at index $index")
+        }
+        positiveButton(R.string.agree)
+        negativeButton(R.string.disagree)
+        debugMode(debugMode)
+      }
+    }
+
+    bottomsheet_grid.setOnClickListener {
+      val items = listOf(
+          BasicGridItem(R.drawable.ic_icon_android, "One"),
+          BasicGridItem(R.drawable.ic_icon_android, "Two"),
+          BasicGridItem(R.drawable.ic_icon_android, "Three"),
+          BasicGridItem(R.drawable.ic_icon_android, "Four"),
+          BasicGridItem(R.drawable.ic_icon_android, "Five"),
+          BasicGridItem(R.drawable.ic_icon_android, "Six"),
+          BasicGridItem(R.drawable.ic_icon_android, "Seven"),
+          BasicGridItem(R.drawable.ic_icon_android, "Eight")
+      )
+
+      MaterialDialog(this, BottomSheet()).show {
+        gridItems(items) { _, index, item ->
+          toast("Selected item ${item.title} at index $index")
+        }
+        positiveButton(R.string.agree)
+        negativeButton(R.string.disagree)
+        debugMode(debugMode)
+      }
+    }
+
+    bottomsheet_customView.setOnClickListener {
+      showCustomViewDialog(BottomSheet())
+    }
+
+    bottomsheet_colorPicker.setOnClickListener {
+      MaterialDialog(this, BottomSheet()).show {
+        title(R.string.custom_colors_argb)
+        colorChooser(
+            colors = ColorPalette.Primary,
+            subColors = ColorPalette.PrimarySub,
+            allowCustomArgb = true,
+            showAlphaSelector = true
+        ) { _, color ->
+          toast("Selected color: ${color.toHex()}")
+        }
+        positiveButton(R.string.select)
+        negativeButton(android.R.string.cancel)
+
+        debugMode(debugMode)
+      }
+    }
+
+    bottomsheet_dateTimePicker.setOnClickListener {
+      MaterialDialog(this, BottomSheet()).show {
+        title(text = "Select Date and Time")
+        dateTimePicker(requireFutureDateTime = true) { _, dateTime ->
+          toast("Selected date/time: ${dateTime.formatDateTime()}")
+        }
+        debugMode(debugMode)
+      }
+    }
   }
 
-  private fun showCustomViewDialog() {
-    val dialog = MaterialDialog(this).show {
+  private fun showCustomViewDialog(dialogBehavior: DialogBehavior = ModalDialog) {
+    val dialog = MaterialDialog(this, dialogBehavior).show {
       title(R.string.googleWifi)
       customView(R.layout.custom_view, scrollable = true)
       positiveButton(R.string.connect) { dialog ->
@@ -949,4 +937,14 @@ class MainActivity : AppCompatActivity() {
     }
     return super.onOptionsItemSelected(item)
   }
+
+  companion object {
+    private const val KEY_PREFS = "prefs"
+    private const val KEY_THEME = "KEY_THEME"
+    private const val KEY_DEBUG_MODE = "debug_mode"
+
+    private const val LIGHT = "light"
+    private const val DARK = "dark"
+    private const val CUSTOM = "custom"
+  }
 }

+ 4 - 3
sample/src/main/java/com/afollestad/materialdialogssample/Utils.kt

@@ -20,6 +20,7 @@ import android.content.SharedPreferences
 import android.widget.Toast
 import java.text.SimpleDateFormat
 import java.util.Calendar
+import java.util.Locale
 
 private var toast: Toast? = null
 
@@ -47,13 +48,13 @@ internal inline fun SharedPreferences.commit(crossinline exec: PrefEditor.() ->
 internal fun Int.toHex() = "#${Integer.toHexString(this)}"
 
 internal fun Calendar.formatTime(): String {
-  return SimpleDateFormat("kk:mm a").format(this.time)
+  return SimpleDateFormat("kk:mm a", Locale.US).format(this.time)
 }
 
 internal fun Calendar.formatDate(): String {
-  return SimpleDateFormat("MMMM dd, yyyy").format(this.time)
+  return SimpleDateFormat("MMMM dd, yyyy", Locale.US).format(this.time)
 }
 
 internal fun Calendar.formatDateTime(): String {
-  return SimpleDateFormat("kk:mm a, MMMM dd, yyyy").format(this.time)
+  return SimpleDateFormat("kk:mm a, MMMM dd, yyyy", Locale.US).format(this.time)
 }

+ 11 - 0
sample/src/main/res/drawable/ic_icon_android.xml

@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:height="56dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0"
+    android:width="56dp">
+  <path
+      android:fillColor="?android:textColorPrimary"
+      android:pathData="M6,18c0,0.55 0.45,1 1,1h1v3.5c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5L11,19h2v3.5c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5L16,19h1c0.55,0 1,-0.45 1,-1L18,8L6,8v10zM3.5,8C2.67,8 2,8.67 2,9.5v7c0,0.83 0.67,1.5 1.5,1.5S5,17.33 5,16.5v-7C5,8.67 4.33,8 3.5,8zM20.5,8c-0.83,0 -1.5,0.67 -1.5,1.5v7c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5v-7c0,-0.83 -0.67,-1.5 -1.5,-1.5zM15.53,2.16l1.3,-1.3c0.2,-0.2 0.2,-0.51 0,-0.71 -0.2,-0.2 -0.51,-0.2 -0.71,0l-1.48,1.48C13.85,1.23 12.95,1 12,1c-0.96,0 -1.86,0.23 -2.66,0.63L7.85,0.15c-0.2,-0.2 -0.51,-0.2 -0.71,0 -0.2,0.2 -0.2,0.51 0,0.71l1.31,1.31C6.97,3.26 6,5.01 6,7h12c0,-1.99 -0.97,-3.75 -2.47,-4.84zM10,5L9,5L9,4h1v1zM15,5h-1L14,4h1v1z"
+      tools:ignore="NewApi"/>
+</vector>

+ 43 - 49
sample/src/main/res/layout/activity_main.xml

@@ -58,12 +58,6 @@
         style="@style/SampleButton"
         />
 
-    <Button
-        android:id="@+id/basic_long"
-        android:text="Basic Long"
-        style="@style/SampleButton"
-        />
-
     <Button
         android:id="@+id/basic_long_titled_buttons"
         android:text="Basic Long + Title + Buttons"
@@ -131,55 +125,12 @@
         style="@style/SampleButton"
         />
 
-    <Button
-        android:id="@+id/list_long_buttons"
-        android:text="List Long + Buttons"
-        style="@style/SampleButton"
-        />
-
     <Button
         android:id="@+id/list_long_titled"
         android:text="List Long + Title"
         style="@style/SampleButton"
         />
 
-    <Button
-        android:id="@+id/list_long_titled_buttons"
-        android:text="List Long + Title + Buttons"
-        style="@style/SampleButton"
-        />
-
-    <Button
-        android:id="@+id/list_long_items"
-        android:text="List, Long Items"
-        style="@style/SampleButton"
-        />
-
-    <Button
-        android:id="@+id/list_long_items_buttons"
-        android:text="List, Long Items + Buttons"
-        style="@style/SampleButton"
-        />
-
-    <Button
-        android:id="@+id/list_long_items_titled"
-        android:text="List, Long Items + Title"
-        style="@style/SampleButton"
-        />
-
-    <Button
-        android:id="@+id/list_long_items_titled_buttons"
-
-        android:text="List, Long Items + Title + Buttons"
-        style="@style/SampleButton"
-        />
-
-    <Button
-        android:id="@+id/list_checkPrompt"
-        android:text="List + Title + Checkbox Prompt"
-        style="@style/SampleButton"
-        />
-
     <Button
         android:id="@+id/list_checkPrompt_buttons"
         android:text="List + Title + Checkbox Prompt + Buttons"
@@ -452,6 +403,49 @@
         style="@style/SampleButton"
         />
 
+    <!-- Bottom Sheets -->
+
+    <TextView
+        android:text="Bottom Sheets"
+        style="@style/SampleHeader"
+        />
+
+    <Button
+        android:id="@+id/bottomsheet_info"
+        android:text="Informational"
+        style="@style/SampleButton"
+        />
+
+    <Button
+        android:id="@+id/bottomsheet_list"
+        android:text="Item List"
+        style="@style/SampleButton"
+        />
+
+    <Button
+        android:id="@+id/bottomsheet_grid"
+        android:text="Item Grid"
+        style="@style/SampleButton"
+        />
+
+    <Button
+        android:id="@+id/bottomsheet_customView"
+        android:text="Custom View"
+        style="@style/SampleButton"
+        />
+
+    <Button
+        android:id="@+id/bottomsheet_colorPicker"
+        android:text="Color Picker"
+        style="@style/SampleButton"
+        />
+
+    <Button
+        android:id="@+id/bottomsheet_dateTimePicker"
+        android:text="DateTime Picker"
+        style="@style/SampleButton"
+        />
+
   </LinearLayout>
 
 </ScrollView>

+ 1 - 1
sample/src/main/res/layout/custom_view.xml

@@ -80,7 +80,7 @@
       android:text="Hello World"
       android:textColor="?android:textColorPrimary"
       android:textSize="@dimen/customEntry"
-      tools:ignore="HardcodedText,LabelFor"
+      tools:ignore="Autofill,HardcodedText,LabelFor"
       />
 
   <CheckBox

+ 1 - 1
settings.gradle

@@ -1 +1 @@
-include ':core', ':sample', ':files', ':color', ':input', ':lifecycle', ':datetime'
+include ':core', ':sample', ':files', ':color', ':input', ':lifecycle', ':datetime', ':bottomsheets'