瀏覽代碼

Merge pull request #392 from teslacoil/master

Refactor padding to be more efficient
Aidan Follestad 10 年之前
父節點
當前提交
05d25dc226

+ 96 - 50
library/src/main/java/com/afollestad/materialdialogs/DialogInit.java

@@ -1,7 +1,10 @@
 package com.afollestad.materialdialogs;
 
+import android.content.Context;
+import android.content.res.ColorStateList;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
+import android.graphics.Color;
 import android.graphics.PorterDuff;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
@@ -9,7 +12,6 @@ import android.text.method.LinkMovementMethod;
 import android.view.ContextThemeWrapper;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
 import android.widget.EditText;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
@@ -18,6 +20,7 @@ import android.widget.ProgressBar;
 import android.widget.ScrollView;
 import android.widget.TextView;
 
+import com.afollestad.materialdialogs.internal.MDButton;
 import com.afollestad.materialdialogs.util.DialogUtils;
 import com.afollestad.materialdialogs.util.TypefaceHelper;
 
@@ -114,9 +117,9 @@ class DialogInit {
         dialog.listView = (ListView) dialog.view.findViewById(R.id.contentListView);
 
         // Button views initially used by checkIfStackingNeeded()
-        dialog.positiveButton = dialog.view.findViewById(R.id.buttonDefaultPositive);
-        dialog.neutralButton = dialog.view.findViewById(R.id.buttonDefaultNeutral);
-        dialog.negativeButton = dialog.view.findViewById(R.id.buttonDefaultNegative);
+        dialog.positiveButton = (MDButton) dialog.view.findViewById(R.id.buttonDefaultPositive);
+        dialog.neutralButton = (MDButton) dialog.view.findViewById(R.id.buttonDefaultNeutral);
+        dialog.negativeButton = (MDButton) dialog.view.findViewById(R.id.buttonDefaultNegative);
 
         // Set up the initial visibility of action buttons based on whether or not text was set
         dialog.positiveButton.setVisibility(builder.positiveText != null ? View.VISIBLE : View.GONE);
@@ -136,6 +139,7 @@ class DialogInit {
                 dialog.icon.setVisibility(View.GONE);
             }
         }
+
         // Setup icon size limiting
         int maxIconSize = builder.maxIconSize;
         if (maxIconSize == -1)
@@ -149,6 +153,13 @@ class DialogInit {
             dialog.icon.requestLayout();
         }
 
+        // Setup divider color in case content scrolls
+        if (builder.dividerColor == 0)
+            builder.dividerColor = DialogUtils.resolveColor(builder.context, R.attr.md_divider_color);
+        if (builder.dividerColor == 0)
+            builder.dividerColor = DialogUtils.resolveColor(dialog.getContext(), R.attr.md_divider);
+        dialog.view.setDividerColor(builder.dividerColor);
+
         // Setup title and title frame
         if (builder.title == null) {
             dialog.titleFrame.setVisibility(View.GONE);
@@ -156,10 +167,10 @@ class DialogInit {
             dialog.title.setText(builder.title);
             dialog.setTypeface(dialog.title, builder.mediumFont);
             dialog.title.setTextColor(builder.titleColor);
-            dialog.title.setGravity(MaterialDialog.gravityEnumToGravity(builder.titleGravity));
+            dialog.title.setGravity(builder.titleGravity.getGravityInt());
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                 //noinspection ResourceType
-                dialog.title.setTextAlignment(MaterialDialog.gravityEnumToTextAlignment(builder.titleGravity));
+                dialog.title.setTextAlignment(builder.titleGravity.getTextAlignment());
             }
         }
 
@@ -175,32 +186,81 @@ class DialogInit {
                 dialog.content.setLinkTextColor(builder.positiveColor);
             }
             dialog.content.setTextColor(builder.contentColor);
-            dialog.content.setGravity(MaterialDialog.gravityEnumToGravity(builder.contentGravity));
+            dialog.content.setGravity(builder.contentGravity.getGravityInt());
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                 //noinspection ResourceType
-                dialog.content.setTextAlignment(MaterialDialog.gravityEnumToTextAlignment(builder.contentGravity));
+                dialog.content.setTextAlignment(builder.contentGravity.getTextAlignment());
             }
         } else if (dialog.content != null) {
             dialog.content.setVisibility(View.GONE);
         }
 
+        // Setup buttons
+        dialog.view.setButtonGravity(builder.buttonsGravity);
+        dialog.view.setButtonStackedGravity(builder.btnStackedGravity);
+        dialog.view.setForceStack(builder.forceStacking);
+        boolean textAllCaps;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+            textAllCaps = DialogUtils.resolveBoolean(builder.context, android.R.attr.textAllCaps, true);
+            if (textAllCaps)
+                textAllCaps = DialogUtils.resolveBoolean(builder.context, R.attr.textAllCaps, true);
+        } else {
+            textAllCaps = DialogUtils.resolveBoolean(builder.context, R.attr.textAllCaps, true);
+        }
+        if (dialog.positiveButton != null && builder.positiveText != null) {
+            MDButton positiveTextView = dialog.positiveButton;
+            dialog.setTypeface(positiveTextView, builder.mediumFont);
+            positiveTextView.setAllCapsCompat(textAllCaps);
+            positiveTextView.setText(builder.positiveText);
+            positiveTextView.setTextColor(getActionTextStateList(builder.context, builder.positiveColor));
+            dialog.positiveButton.setStackedSelector(dialog.getButtonSelector(DialogAction.POSITIVE, true));
+            dialog.positiveButton.setDefaultSelector(dialog.getButtonSelector(DialogAction.POSITIVE, false));
+            dialog.positiveButton.setTag(DialogAction.POSITIVE);
+            dialog.positiveButton.setOnClickListener(dialog);
+            dialog.positiveButton.setVisibility(View.VISIBLE);
+        }
+
+        if (dialog.negativeButton != null && builder.negativeText != null) {
+            MDButton negativeTextView = dialog.negativeButton;
+            dialog.setTypeface(negativeTextView, builder.mediumFont);
+            negativeTextView.setAllCapsCompat(textAllCaps);
+            negativeTextView.setText(builder.negativeText);
+            negativeTextView.setTextColor(getActionTextStateList(builder.context, builder.negativeColor));
+            dialog.negativeButton.setStackedSelector(dialog.getButtonSelector(DialogAction.NEGATIVE, true));
+            dialog.negativeButton.setDefaultSelector(dialog.getButtonSelector(DialogAction.NEGATIVE, false));
+            dialog.negativeButton.setTag(DialogAction.NEGATIVE);
+            dialog.negativeButton.setOnClickListener(dialog);
+            dialog.negativeButton.setVisibility(View.VISIBLE);
+        }
+
+        if (dialog.neutralButton != null && builder.neutralText != null) {
+            MDButton neutralTextView = dialog.neutralButton;
+            dialog.setTypeface(neutralTextView, builder.mediumFont);
+            neutralTextView.setAllCapsCompat(textAllCaps);
+            neutralTextView.setText(builder.neutralText);
+            neutralTextView.setTextColor(getActionTextStateList(builder.context, builder.neutralColor));
+            dialog.neutralButton.setStackedSelector(dialog.getButtonSelector(DialogAction.NEUTRAL, true));
+            dialog.neutralButton.setDefaultSelector(dialog.getButtonSelector(DialogAction.NEUTRAL, false));
+            dialog.neutralButton.setTag(DialogAction.NEUTRAL);
+            dialog.neutralButton.setOnClickListener(dialog);
+            dialog.neutralButton.setVisibility(View.VISIBLE);
+        }
+
+        // Load default list item text color
+        if (builder.itemColorSet) {
+            dialog.defaultItemColor = builder.itemColor;
+        } else if (builder.theme == Theme.LIGHT) {
+            dialog.defaultItemColor = Color.BLACK;
+        } else {
+            dialog.defaultItemColor = Color.WHITE;
+        }
+
         // Setup list dialog stuff
         if (builder.listCallbackMultiChoice != null)
             dialog.selectedIndicesList = new ArrayList<>();
         if (dialog.listView != null && (builder.items != null && builder.items.length > 0 || builder.adapter != null)) {
             dialog.listView.setSelector(dialog.getListSelector());
 
-            if (builder.title != null) {
-                // Cancel out top padding if there's a title
-                dialog.listView.setPadding(dialog.listView.getPaddingLeft(), 0,
-                        dialog.listView.getPaddingRight(), dialog.listView.getPaddingBottom());
-            }
-            if (dialog.hasActionButtons()) {
-                // No bottom padding if there's action buttons
-                dialog.listView.setPadding(dialog.listView.getPaddingLeft(), 0,
-                        dialog.listView.getPaddingRight(), 0);
-            }
-
             // No custom adapter specified, setup the list with a MaterialDialogAdapter.
             // Which supports regular lists and single/multi choice dialogs.
             if (builder.adapter == null) {
@@ -224,7 +284,6 @@ class DialogInit {
         setupProgressDialog(dialog);
 
         if (builder.customView != null) {
-            dialog.invalidateCustomViewAssociations();
             FrameLayout frame = (FrameLayout) dialog.view.findViewById(R.id.customViewFrame);
             dialog.customViewFrame = frame;
             View innerView = builder.customView;
@@ -234,16 +293,8 @@ class DialogInit {
                 final Resources r = dialog.getContext().getResources();
                 final int framePadding = r.getDimensionPixelSize(R.dimen.md_dialog_frame_margin);
                 final ScrollView sv = new ScrollView(dialog.getContext());
-                int paddingTop;
-                int paddingBottom;
-                if (dialog.titleFrame.getVisibility() != View.GONE)
-                    paddingTop = r.getDimensionPixelSize(R.dimen.md_content_vertical_padding);
-                else
-                    paddingTop = r.getDimensionPixelSize(R.dimen.md_dialog_frame_margin);
-                if (dialog.hasActionButtons())
-                    paddingBottom = r.getDimensionPixelSize(R.dimen.md_content_vertical_padding);
-                else
-                    paddingBottom = r.getDimensionPixelSize(R.dimen.md_dialog_frame_margin);
+                int paddingTop = r.getDimensionPixelSize(R.dimen.md_content_padding_top);
+                int paddingBottom = r.getDimensionPixelSize(R.dimen.md_content_padding_bottom);
                 sv.setClipToPadding(false);
                 if (innerView instanceof EditText) {
                     // Setting padding to an EditText causes visual errors, set it to the parent instead
@@ -260,8 +311,6 @@ class DialogInit {
             }
             frame.addView(innerView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                     ViewGroup.LayoutParams.WRAP_CONTENT));
-        } else {
-            dialog.invalidateCustomViewAssociations();
         }
 
         // Setup user listeners
@@ -275,20 +324,10 @@ class DialogInit {
             dialog.setOnKeyListener(builder.keyListener);
 
         // Other internal initialization
-        dialog.updateFramePadding();
         dialog.invalidateList();
         dialog._setOnShowListenerInternal();
         dialog._setViewInternal(dialog.view);
         dialog.checkIfListInitScroll();
-        dialog.view.getViewTreeObserver().addOnGlobalLayoutListener(
-                new ViewTreeObserver.OnGlobalLayoutListener() {
-                    @Override
-                    public void onGlobalLayout() {
-                        if (dialog.view.getMeasuredWidth() > 0) {
-                            dialog.invalidateCustomViewAssociations();
-                        }
-                    }
-                });
     }
 
     private static void setupProgressDialog(final MaterialDialog dialog) {
@@ -325,14 +364,21 @@ class DialogInit {
                 dialog.mProgressLabel.setText("0%");
             }
 
-            if (builder.title == null) {
-                // Redistribute main frame's bottom padding to the top padding if there's no title
-                final View mainFrame = dialog.view.findViewById(R.id.mainFrame);
-                mainFrame.setPadding(mainFrame.getPaddingLeft(),
-                        mainFrame.getPaddingBottom(),
-                        mainFrame.getPaddingRight(),
-                        mainFrame.getPaddingBottom());
-            }
         }
     }
+
+    private static ColorStateList getActionTextStateList(Context context, int newPrimaryColor) {
+        final int fallBackButtonColor = DialogUtils.resolveColor(context, android.R.attr.textColorPrimary);
+        if (newPrimaryColor == 0) newPrimaryColor = fallBackButtonColor;
+        int[][] states = new int[][]{
+                new int[]{-android.R.attr.state_enabled}, // disabled
+                new int[]{} // enabled
+        };
+        int[] colors = new int[]{
+                DialogUtils.adjustAlpha(newPrimaryColor, 0.4f),
+                newPrimaryColor
+        };
+        return new ColorStateList(states, colors);
+    }
+
 }

+ 30 - 1
library/src/main/java/com/afollestad/materialdialogs/GravityEnum.java

@@ -1,5 +1,34 @@
 package com.afollestad.materialdialogs;
 
+import android.os.Build;
+import android.view.Gravity;
+import android.view.View;
+
 public enum GravityEnum {
-    START, CENTER, END
+    START, CENTER, END;
+
+    private static final boolean HAS_RTL = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1;
+    public int getGravityInt() {
+        switch (this) {
+            case START:
+                return HAS_RTL ? Gravity.START : Gravity.LEFT;
+            case CENTER:
+                return Gravity.CENTER_HORIZONTAL;
+            case END:
+                return HAS_RTL ? Gravity.END : Gravity.RIGHT;
+            default:
+                throw new IllegalStateException("Invalid gravity constant");
+        }
+    }
+
+    public int getTextAlignment() {
+        switch (this) {
+            case CENTER:
+                return View.TEXT_ALIGNMENT_CENTER;
+            case END:
+                return View.TEXT_ALIGNMENT_VIEW_END;
+            default:
+                return View.TEXT_ALIGNMENT_VIEW_START;
+        }
+    }
 }

+ 25 - 530
library/src/main/java/com/afollestad/materialdialogs/MaterialDialog.java

@@ -1,14 +1,9 @@
 package com.afollestad.materialdialogs;
 
 import android.annotation.SuppressLint;
-import android.annotation.TargetApi;
 import android.app.AlertDialog;
 import android.content.Context;
-import android.content.DialogInterface;
-import android.content.res.ColorStateList;
-import android.content.res.Resources;
 import android.graphics.Paint;
-import android.graphics.Rect;
 import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
@@ -23,15 +18,12 @@ import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.StringRes;
 import android.support.v4.content.res.ResourcesCompat;
-import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
 import android.util.Log;
-import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
 import android.view.WindowManager;
-import android.webkit.WebView;
 import android.widget.AdapterView;
 import android.widget.Button;
 import android.widget.CheckBox;
@@ -41,20 +33,18 @@ import android.widget.LinearLayout;
 import android.widget.ListAdapter;
 import android.widget.ListView;
 import android.widget.ProgressBar;
-import android.widget.RelativeLayout;
-import android.widget.ScrollView;
 import android.widget.TextView;
 
 import com.afollestad.materialdialogs.base.DialogBase;
+import com.afollestad.materialdialogs.internal.MDButton;
+import com.afollestad.materialdialogs.internal.MDRootLayout;
 import com.afollestad.materialdialogs.util.DialogUtils;
-import com.afollestad.materialdialogs.util.RecyclerUtil;
 import com.afollestad.materialdialogs.util.TypefaceHelper;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
-import java.util.Locale;
 
 /**
  * @author Aidan Follestad (afollestad)
@@ -62,7 +52,7 @@ import java.util.Locale;
 public class MaterialDialog extends DialogBase implements
         View.OnClickListener, AdapterView.OnItemClickListener {
 
-    protected final View view;
+    protected final MDRootLayout view;
     protected final Builder mBuilder;
     protected ListView listView;
     protected ImageView icon;
@@ -74,10 +64,9 @@ public class MaterialDialog extends DialogBase implements
     protected TextView mProgressMinMax;
     protected TextView content;
 
-    protected View positiveButton;
-    protected View neutralButton;
-    protected View negativeButton;
-    protected boolean isStacked;
+    protected MDButton positiveButton;
+    protected MDButton neutralButton;
+    protected MDButton negativeButton;
     protected int defaultItemColor;
     protected ListType listType;
     protected List<Integer> selectedIndicesList;
@@ -87,45 +76,10 @@ public class MaterialDialog extends DialogBase implements
         super(DialogInit.getTheme(builder));
         mBuilder = builder;
         final LayoutInflater inflater = LayoutInflater.from(mBuilder.context);
-        this.view = inflater.inflate(DialogInit.getInflateLayout(builder), null);
+        view = (MDRootLayout) inflater.inflate(DialogInit.getInflateLayout(builder), null);
         DialogInit.init(this);
     }
 
-    @SuppressLint("RtlHardcoded")
-    protected static int gravityEnumToGravity(GravityEnum gravity) {
-        switch (gravity) {
-            case CENTER:
-                return Gravity.CENTER_HORIZONTAL;
-            case END:
-                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1)
-                    return Gravity.RIGHT;
-                return Gravity.END;
-            default:
-                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1)
-                    return Gravity.LEFT;
-                return Gravity.START;
-        }
-    }
-
-    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
-    protected static int gravityEnumToTextAlignment(GravityEnum gravity) {
-        switch (gravity) {
-            case CENTER:
-                return View.TEXT_ALIGNMENT_CENTER;
-            case END:
-                return View.TEXT_ALIGNMENT_VIEW_END;
-            default:
-                return View.TEXT_ALIGNMENT_VIEW_START;
-        }
-    }
-
-    @Override
-    public void onShow(DialogInterface dialog) {
-        super.onShow(dialog); // calls any external show listeners
-        checkIfStackingNeeded();
-        invalidateCustomViewAssociations();
-    }
-
     protected final void setTypeface(TextView text, Typeface t) {
         if (t == null) return;
         int flags = text.getPaintFlags() | Paint.SUBPIXEL_TEXT_FLAG;
@@ -178,138 +132,6 @@ public class MaterialDialog extends DialogBase implements
         });
     }
 
-    /**
-     * To account for scrolling content and overscroll glows, the frame padding/margins sometimes
-     * must be set on inner views. This is dependent on the visibility of the title bar and action
-     * buttons. This method determines where the padding or margins are needed and applies them.
-     */
-    protected final void updateFramePadding() {
-        Resources r = getContext().getResources();
-        int framePadding = r.getDimensionPixelSize(R.dimen.md_dialog_frame_margin);
-
-        View contentScrollView = view.findViewById(R.id.contentScrollView);
-        if (contentScrollView != null) {
-            int paddingTop = contentScrollView.getPaddingTop();
-            int paddingBottom = contentScrollView.getPaddingBottom();
-            if (!hasActionButtons())
-                paddingBottom = framePadding;
-            if (titleFrame.getVisibility() == View.GONE)
-                paddingTop = framePadding;
-            contentScrollView.setPadding(contentScrollView.getPaddingLeft(), paddingTop,
-                    contentScrollView.getPaddingRight(), paddingBottom);
-        }
-
-        if (listView != null) {
-            // Padding below title is reduced for divider.
-            final int titlePaddingBottom = (int) mBuilder.context.getResources().getDimension(R.dimen.md_title_frame_margin_bottom_list);
-            titleFrame.setPadding(titleFrame.getPaddingLeft(),
-                    titleFrame.getPaddingTop(),
-                    titleFrame.getPaddingRight(),
-                    titlePaddingBottom);
-        }
-    }
-
-    /**
-     * Invalidates visibility of views for the presence of a custom view or list content
-     */
-    protected final void invalidateCustomViewAssociations() {
-        if (view.getMeasuredWidth() == 0) {
-            return;
-        }
-        View contentScrollView = view.findViewById(R.id.contentScrollView);
-        final int contentHorizontalPadding = (int) mBuilder.context.getResources()
-                .getDimension(R.dimen.md_dialog_frame_margin);
-        if (content != null)
-            content.setPadding(contentHorizontalPadding, 0, contentHorizontalPadding, 0);
-
-        if (mBuilder.customView != null) {
-            boolean topScroll = canViewOrChildScroll(customViewFrame.getChildAt(0), false);
-            boolean bottomScroll = canViewOrChildScroll(customViewFrame.getChildAt(0), true);
-            setDividerVisibility(topScroll, bottomScroll);
-        } else if ((mBuilder.items != null && mBuilder.items.length > 0) || mBuilder.adapter != null) {
-            if (contentScrollView != null) {
-                contentScrollView.setVisibility(mBuilder.content != null
-                        && mBuilder.content.toString().trim().length() > 0 ? View.VISIBLE : View.GONE);
-            }
-            boolean canScroll = titleFrame.getVisibility() == View.VISIBLE &&
-                    (canListViewScroll() || canContentScroll());
-            setDividerVisibility(canScroll, canScroll);
-        } else {
-            if (contentScrollView != null)
-                contentScrollView.setVisibility(View.VISIBLE);
-            boolean canScroll = canContentScroll();
-            if (canScroll) {
-                if (content != null) {
-                    final int contentVerticalPadding = (int) mBuilder.context.getResources()
-                            .getDimension(R.dimen.md_title_frame_margin_bottom);
-                    content.setPadding(contentHorizontalPadding, contentVerticalPadding,
-                            contentHorizontalPadding, contentVerticalPadding);
-                }
-
-                // Same effect as when there's a ListView. Padding below title is reduced for divider.
-                final int titlePaddingBottom = (int) mBuilder.context.getResources().getDimension(R.dimen.md_title_frame_margin_bottom_list);
-                titleFrame.setPadding(titleFrame.getPaddingLeft(),
-                        titleFrame.getPaddingTop(),
-                        titleFrame.getPaddingRight(),
-                        titlePaddingBottom);
-            }
-            setDividerVisibility(canScroll, canScroll);
-        }
-    }
-
-    /**
-     * Set the visibility of the bottom divider and adjusts the layout margin,
-     * when the divider is visible the button bar bottom margin (8dp from
-     * http://www.google.com/design/spec/components/dialogs.html#dialogs-specs )
-     * is removed as it makes the button bar look off balanced with different amounts of padding
-     * above and below the divider.
-     */
-    private void setDividerVisibility(boolean topVisible, boolean bottomVisible) {
-        topVisible = topVisible && titleFrame.getVisibility() == View.VISIBLE;
-        bottomVisible = bottomVisible && hasActionButtons();
-
-        if (mBuilder.dividerColor == 0)
-            mBuilder.dividerColor = DialogUtils.resolveColor(mBuilder.context, R.attr.md_divider_color);
-        if (mBuilder.dividerColor == 0)
-            mBuilder.dividerColor = DialogUtils.resolveColor(getContext(), R.attr.md_divider);
-
-        View titleBarDivider = view.findViewById(R.id.titleBarDivider);
-        if (topVisible) {
-            titleBarDivider.setVisibility(View.VISIBLE);
-            titleBarDivider.setBackgroundColor(mBuilder.dividerColor);
-        } else {
-            titleBarDivider.setVisibility(View.GONE);
-        }
-
-        View buttonBarDivider = view.findViewById(R.id.buttonBarDivider);
-        if (bottomVisible) {
-            buttonBarDivider.setVisibility(View.VISIBLE);
-            buttonBarDivider.setBackgroundColor(mBuilder.dividerColor);
-            setVerticalMargins(view.findViewById(R.id.buttonStackedFrame), 0, 0);
-            setVerticalMargins(view.findViewById(R.id.buttonDefaultFrame), 0, 0);
-        } else {
-            Resources r = getContext().getResources();
-            buttonBarDivider.setVisibility(View.GONE);
-
-            final int bottomMargin = r.getDimensionPixelSize(R.dimen.md_button_frame_vertical_padding);
-
-            /* Only enable the bottom margin if our available window space can hold the margin,
-               we don't want to enable this and cause the content to scroll, which is bad
-               experience itself but it also causes a vibrating window as this will keep getting
-               enabled/disabled over and over again.
-             */
-            Rect maxWindowFrame = new Rect();
-            getWindow().getDecorView().getWindowVisibleDisplayFrame(maxWindowFrame);
-            int currentHeight = getWindow().getDecorView().getMeasuredHeight();
-            if (currentHeight + bottomMargin < maxWindowFrame.height()) {
-                setVerticalMargins(view.findViewById(R.id.buttonStackedFrame),
-                        bottomMargin, bottomMargin);
-                setVerticalMargins(view.findViewById(R.id.buttonDefaultFrame),
-                        bottomMargin, bottomMargin);
-            }
-        }
-    }
-
     /**
      * Sets the dialog ListView's adapter and it's item click listener.
      */
@@ -322,99 +144,6 @@ public class MaterialDialog extends DialogBase implements
             listView.setOnItemClickListener(this);
     }
 
-    /**
-     * Find the view touching the bottom of this ViewGroup. Non visible children are ignored,
-     * however getChildDrawingOrder is not taking into account for simplicity and because it behaves
-     * inconsistently across platform versions.
-     *
-     * @return View touching the bottom of this viewgroup or null
-     */
-    @Nullable
-    private static View getBottomView(ViewGroup viewGroup) {
-        if (viewGroup == null)
-            return null;
-        View bottomView = null;
-        for (int i = viewGroup.getChildCount() - 1; i >= 0; i--) {
-            View child = viewGroup.getChildAt(i);
-            if (child.getVisibility() == View.VISIBLE && child.getBottom() == viewGroup.getBottom()) {
-                bottomView = child;
-                break;
-            }
-        }
-        return bottomView;
-    }
-
-    @Nullable
-    private static View getTopView(ViewGroup viewGroup) {
-        if (viewGroup == null)
-            return null;
-        View topView = null;
-        for (int i = viewGroup.getChildCount() - 1; i >= 0; i--) {
-            View child = viewGroup.getChildAt(i);
-            if (child.getVisibility() == View.VISIBLE && child.getTop() == viewGroup.getTop()) {
-                topView = child;
-                break;
-            }
-        }
-        return topView;
-    }
-
-    private static boolean canViewOrChildScroll(View view, boolean atBottom) {
-        if (view == null)
-            return false;
-        if (view instanceof ScrollView) {
-            ScrollView sv = (ScrollView) view;
-            if (sv.getChildCount() == 0)
-                return false;
-            final int childHeight = sv.getChildAt(0).getMeasuredHeight();
-            return sv.getMeasuredHeight() < childHeight;
-        } else if (view instanceof AdapterView) {
-            return canAdapterViewScroll((AdapterView) view);
-        } else if (view instanceof WebView) {
-            return canWebViewScroll((WebView) view);
-        } else if (view instanceof RecyclerView) {
-            return RecyclerUtil.canRecyclerViewScroll(view);
-        } else if (view instanceof ViewGroup) {
-            if (atBottom) {
-                return canViewOrChildScroll(getBottomView((ViewGroup) view), true);
-            } else {
-                return canViewOrChildScroll(getTopView((ViewGroup) view), false);
-            }
-        } else {
-            return false;
-        }
-    }
-
-    private static boolean canWebViewScroll(WebView view) {
-        return view.getMeasuredHeight() > view.getContentHeight();
-    }
-
-    private static boolean canAdapterViewScroll(AdapterView lv) {
-        /* Force it to layout it's children */
-        if (lv.getLastVisiblePosition() == -1)
-            return false;
-
-        /* We can scroll if the first or last item is not visible */
-        boolean firstItemVisible = lv.getFirstVisiblePosition() == 0;
-        boolean lastItemVisible = lv.getLastVisiblePosition() == lv.getCount() - 1;
-
-        if (firstItemVisible && lastItemVisible) {
-            /* Or the first item's top is above or own top */
-            if (lv.getChildAt(0).getTop() < lv.getPaddingTop())
-                return true;
-
-            /* or the last item's bottom is beyond our own bottom */
-            return lv.getChildAt(lv.getChildCount() - 1).getBottom() >
-                    lv.getHeight() - lv.getPaddingBottom();
-        }
-
-        return true;
-    }
-
-    private boolean canListViewScroll() {
-        return canAdapterViewScroll(listView);
-    }
-
     @Override
     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
         if (mBuilder.listCallbackCustom != null) {
@@ -498,55 +227,6 @@ public class MaterialDialog extends DialogBase implements
         }
     }
 
-    /**
-     * Detects whether or not the content TextView can be scrolled.
-     */
-    private boolean canContentScroll() {
-        final ScrollView scrollView = (ScrollView) view.findViewById(R.id.contentScrollView);
-        if (scrollView == null) return false;
-        final int childHeight = content.getMeasuredHeight();
-        return scrollView.getMeasuredHeight() < childHeight;
-    }
-
-    /**
-     * Measures the action button's and their text to decide whether or not the button should be stacked.
-     */
-    private void checkIfStackingNeeded() {
-        invalidateActions();
-        if (numberOfActionButtons() <= 1) {
-            return;
-        } else if (mBuilder.forceStacking) {
-            isStacked = true;
-        } else {
-            isStacked = false;
-            int buttonsWidth = 0;
-
-            positiveButton.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
-            neutralButton.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
-            negativeButton.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
-
-            if (mBuilder.positiveText != null && positiveButton.getVisibility() == View.VISIBLE) {
-                buttonsWidth += positiveButton.getMeasuredWidth();
-            }
-            if (mBuilder.neutralText != null && neutralButton.getVisibility() == View.VISIBLE) {
-                buttonsWidth += neutralButton.getMeasuredWidth();
-            }
-            if (mBuilder.negativeText != null && negativeButton.getVisibility() == View.VISIBLE) {
-                buttonsWidth += negativeButton.getMeasuredWidth();
-            }
-
-            final int buttonFrameWidth = view.findViewById(R.id.buttonDefaultFrame).getMeasuredWidth();
-            isStacked = buttonsWidth > buttonFrameWidth;
-        }
-
-        if (isStacked) {
-            invalidateActions();
-            positiveButton.setVisibility(mBuilder.positiveText != null ? View.VISIBLE : View.GONE);
-            neutralButton.setVisibility(mBuilder.neutralText != null ? View.VISIBLE : View.GONE);
-            negativeButton.setVisibility(mBuilder.negativeText != null ? View.VISIBLE : View.GONE);
-        }
-    }
-
     protected final Drawable getListSelector() {
         if (mBuilder.listSelector != 0)
             return ResourcesCompat.getDrawable(mBuilder.context.getResources(), mBuilder.listSelector, null);
@@ -555,7 +235,7 @@ public class MaterialDialog extends DialogBase implements
         return DialogUtils.resolveDrawable(getContext(), R.attr.md_list_selector);
     }
 
-    private Drawable getButtonSelector(DialogAction which) {
+    /* package */ Drawable getButtonSelector(DialogAction which, boolean isStacked) {
         if (isStacked) {
             if (mBuilder.btnSelectorStacked != 0)
                 return ResourcesCompat.getDrawable(mBuilder.context.getResources(), mBuilder.btnSelectorStacked, null);
@@ -589,168 +269,6 @@ public class MaterialDialog extends DialogBase implements
         }
     }
 
-    /**
-     * Invalidates the positive/neutral/negative action buttons. Hides the action button frames if
-     * no action buttons are visible. Updates the action button references based on whether stacking
-     * is enabled. Sets up text color, selectors, and other properties of visible action buttons.
-     */
-    public final boolean invalidateActions() {
-        if (!hasActionButtons()) {
-            // If the dialog is a plain list dialog, no buttons are shown.
-            view.findViewById(R.id.buttonDefaultFrame).setVisibility(View.GONE);
-            view.findViewById(R.id.buttonStackedFrame).setVisibility(View.GONE);
-            invalidateList();
-            return false;
-        }
-
-        if (isStacked) {
-            view.findViewById(R.id.buttonDefaultFrame).setVisibility(View.GONE);
-            view.findViewById(R.id.buttonStackedFrame).setVisibility(View.VISIBLE);
-        } else {
-            view.findViewById(R.id.buttonDefaultFrame).setVisibility(View.VISIBLE);
-            view.findViewById(R.id.buttonStackedFrame).setVisibility(View.GONE);
-        }
-
-        boolean textAllCaps;
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
-            textAllCaps = DialogUtils.resolveBoolean(mBuilder.context, android.R.attr.textAllCaps, true);
-            if (textAllCaps)
-                textAllCaps = DialogUtils.resolveBoolean(mBuilder.context, R.attr.textAllCaps, true);
-        } else {
-            textAllCaps = DialogUtils.resolveBoolean(mBuilder.context, R.attr.textAllCaps, true);
-        }
-
-        positiveButton = view.findViewById(
-                isStacked ? R.id.buttonStackedPositive : R.id.buttonDefaultPositive);
-        if (mBuilder.positiveText != null && positiveButton.getVisibility() == View.VISIBLE) {
-            TextView positiveTextView = (TextView) ((FrameLayout) positiveButton).getChildAt(0);
-            setTypeface(positiveTextView, mBuilder.mediumFont);
-            positiveTextView.setText(textAllCaps ?
-                    mBuilder.positiveText.toString().toUpperCase(Locale.getDefault()) : mBuilder.positiveText);
-            positiveTextView.setTextColor(getActionTextStateList(mBuilder.positiveColor));
-            setBackgroundCompat(positiveButton, getButtonSelector(DialogAction.POSITIVE));
-            positiveButton.setTag(POSITIVE);
-            positiveButton.setOnClickListener(this);
-            if (isStacked) {
-                positiveTextView.setGravity(gravityEnumToGravity(mBuilder.btnStackedGravity));
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
-                    //noinspection ResourceType
-                    positiveTextView.setTextAlignment(gravityEnumToTextAlignment(mBuilder.btnStackedGravity));
-                }
-            } else {
-                RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
-                        RelativeLayout.LayoutParams.WRAP_CONTENT, (int) getContext().getResources().getDimension(R.dimen.md_button_height));
-                // Positive button is on the right when button gravity is start, left when end
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
-                    params.addRule(mBuilder.buttonsGravity == GravityEnum.START || mBuilder.buttonsGravity == GravityEnum.CENTER ?
-                            RelativeLayout.ALIGN_PARENT_END : RelativeLayout.ALIGN_PARENT_START);
-                } else {
-                    params.addRule(mBuilder.buttonsGravity == GravityEnum.START || mBuilder.buttonsGravity == GravityEnum.CENTER ?
-                            RelativeLayout.ALIGN_PARENT_RIGHT : RelativeLayout.ALIGN_PARENT_LEFT);
-                }
-                positiveButton.setLayoutParams(params);
-            }
-        }
-
-        neutralButton = view.findViewById(
-                isStacked ? R.id.buttonStackedNeutral : R.id.buttonDefaultNeutral);
-        if (mBuilder.neutralText != null && neutralButton.getVisibility() == View.VISIBLE) {
-            TextView neutralTextView = (TextView) ((FrameLayout) neutralButton).getChildAt(0);
-            setTypeface(neutralTextView, mBuilder.mediumFont);
-            neutralTextView.setTextColor(getActionTextStateList(mBuilder.neutralColor));
-            setBackgroundCompat(neutralButton, getButtonSelector(DialogAction.NEUTRAL));
-            neutralTextView.setText(textAllCaps ?
-                    mBuilder.neutralText.toString().toUpperCase(Locale.getDefault()) : mBuilder.neutralText);
-            neutralButton.setTag(NEUTRAL);
-            neutralButton.setOnClickListener(this);
-            if (isStacked) {
-                neutralTextView.setGravity(gravityEnumToGravity(mBuilder.btnStackedGravity));
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
-                    //noinspection ResourceType
-                    neutralTextView.setTextAlignment(gravityEnumToTextAlignment(mBuilder.btnStackedGravity));
-                }
-            } else {
-                // Neutral button follows buttonsGravity literally.
-                RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
-                        RelativeLayout.LayoutParams.WRAP_CONTENT, (int) getContext().getResources().getDimension(R.dimen.md_button_height));
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
-                    if (mBuilder.buttonsGravity == GravityEnum.CENTER) {
-                        params.addRule(RelativeLayout.CENTER_HORIZONTAL);
-                        params.addRule(RelativeLayout.START_OF, R.id.buttonDefaultPositive);
-                        params.addRule(RelativeLayout.END_OF, R.id.buttonDefaultNegative);
-                        ((FrameLayout.LayoutParams) neutralTextView.getLayoutParams()).gravity = Gravity.CENTER;
-                    } else {
-                        params.addRule(mBuilder.buttonsGravity == GravityEnum.START ?
-                                RelativeLayout.ALIGN_PARENT_START : RelativeLayout.ALIGN_PARENT_END);
-                    }
-                } else {
-                    if (mBuilder.buttonsGravity == GravityEnum.CENTER) {
-                        params.addRule(RelativeLayout.CENTER_HORIZONTAL);
-                        params.addRule(RelativeLayout.LEFT_OF, R.id.buttonDefaultPositive);
-                        params.addRule(RelativeLayout.RIGHT_OF, R.id.buttonDefaultNegative);
-                        neutralTextView.setGravity(Gravity.CENTER_HORIZONTAL);
-                    } else {
-                        params.addRule(mBuilder.buttonsGravity == GravityEnum.START ?
-                                RelativeLayout.ALIGN_PARENT_LEFT : RelativeLayout.ALIGN_PARENT_RIGHT);
-                    }
-                }
-                neutralButton.setLayoutParams(params);
-            }
-        }
-
-        negativeButton = view.findViewById(
-                isStacked ? R.id.buttonStackedNegative : R.id.buttonDefaultNegative);
-        if (mBuilder.negativeText != null && negativeButton.getVisibility() == View.VISIBLE) {
-            TextView negativeTextView = (TextView) ((FrameLayout) negativeButton).getChildAt(0);
-            setTypeface(negativeTextView, mBuilder.mediumFont);
-            negativeTextView.setTextColor(getActionTextStateList(mBuilder.negativeColor));
-            setBackgroundCompat(negativeButton, getButtonSelector(DialogAction.NEGATIVE));
-            negativeTextView.setText(textAllCaps ?
-                    mBuilder.negativeText.toString().toUpperCase(Locale.getDefault()) : mBuilder.negativeText);
-            negativeButton.setTag(NEGATIVE);
-            negativeButton.setOnClickListener(this);
-
-            if (isStacked) {
-                negativeTextView.setGravity(gravityEnumToGravity(mBuilder.btnStackedGravity));
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
-                    //noinspection ResourceType
-                    negativeTextView.setTextAlignment(gravityEnumToTextAlignment(mBuilder.btnStackedGravity));
-                }
-            } else {
-                RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
-                        RelativeLayout.LayoutParams.WRAP_CONTENT, (int) getContext().getResources().getDimension(R.dimen.md_button_height));
-                if (mBuilder.buttonsGravity == GravityEnum.CENTER) {
-                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
-                        params.addRule(RelativeLayout.ALIGN_PARENT_START);
-                    else
-                        params.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
-                } else {
-                    if (mBuilder.positiveText != null && positiveButton.getVisibility() == View.VISIBLE) {
-                        // There's a positive button, it goes next to that
-                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
-                            params.addRule(mBuilder.buttonsGravity == GravityEnum.START ?
-                                    RelativeLayout.START_OF : RelativeLayout.END_OF, R.id.buttonDefaultPositive);
-                        } else {
-                            params.addRule(mBuilder.buttonsGravity == GravityEnum.START ?
-                                    RelativeLayout.LEFT_OF : RelativeLayout.RIGHT_OF, R.id.buttonDefaultPositive);
-                        }
-                    } else {
-                        // Negative button replaces positive button position if there's no positive button
-                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
-                            params.addRule(mBuilder.buttonsGravity == GravityEnum.START ?
-                                    RelativeLayout.ALIGN_PARENT_END : RelativeLayout.ALIGN_PARENT_START);
-                        } else {
-                            params.addRule(mBuilder.buttonsGravity == GravityEnum.START ?
-                                    RelativeLayout.ALIGN_PARENT_RIGHT : RelativeLayout.ALIGN_PARENT_LEFT);
-                        }
-                    }
-                }
-                negativeButton.setLayoutParams(params);
-            }
-        }
-        return true;
-    }
-
     private boolean sendSingleChoiceCallback(View v) {
         CharSequence text = null;
         if (mBuilder.selectedIndex >= 0) {
@@ -772,7 +290,7 @@ public class MaterialDialog extends DialogBase implements
 
     @Override
     public final void onClick(View v) {
-        String tag = (String) v.getTag();
+        DialogAction tag = (DialogAction) v.getTag();
         switch (tag) {
             case POSITIVE: {
                 if (mBuilder.callback != null)
@@ -1459,6 +977,7 @@ public class MaterialDialog extends DialogBase implements
     public void show() {
         if (Looper.myLooper() != Looper.getMainLooper())
             throw new IllegalStateException("Dialogs can only be shown from the UI thread.");
+
         try {
             super.show();
         } catch (WindowManager.BadTokenException e) {
@@ -1466,20 +985,6 @@ public class MaterialDialog extends DialogBase implements
         }
     }
 
-    private ColorStateList getActionTextStateList(int newPrimaryColor) {
-        final int fallBackButtonColor = DialogUtils.resolveColor(getContext(), android.R.attr.textColorPrimary);
-        if (newPrimaryColor == 0) newPrimaryColor = fallBackButtonColor;
-        int[][] states = new int[][]{
-                new int[]{-android.R.attr.state_enabled}, // disabled
-                new int[]{} // enabled
-        };
-        int[] colors = new int[]{
-                DialogUtils.adjustAlpha(newPrimaryColor, 0.4f),
-                newPrimaryColor
-        };
-        return new ColorStateList(states, colors);
-    }
-
     /**
      * Retrieves the view of an action button, allowing you to modify properties such as whether or not it's enabled.
      * Use {@link #setActionButton(DialogAction, int)} to change text, since the view returned here is not
@@ -1489,24 +994,13 @@ public class MaterialDialog extends DialogBase implements
      * @return The view from the dialog's layout representing this action button.
      */
     public final View getActionButton(@NonNull DialogAction which) {
-        if (isStacked) {
-            switch (which) {
-                default:
-                    return view.findViewById(R.id.buttonStackedPositive);
-                case NEUTRAL:
-                    return view.findViewById(R.id.buttonStackedNeutral);
-                case NEGATIVE:
-                    return view.findViewById(R.id.buttonStackedNegative);
-            }
-        } else {
-            switch (which) {
-                default:
-                    return view.findViewById(R.id.buttonDefaultPositive);
-                case NEUTRAL:
-                    return view.findViewById(R.id.buttonDefaultNeutral);
-                case NEGATIVE:
-                    return view.findViewById(R.id.buttonDefaultNegative);
-            }
+        switch (which) {
+            default:
+                return view.findViewById(R.id.buttonDefaultPositive);
+            case NEUTRAL:
+                return view.findViewById(R.id.buttonDefaultNeutral);
+            case NEGATIVE:
+                return view.findViewById(R.id.buttonDefaultNegative);
         }
     }
 
@@ -1581,18 +1075,20 @@ public class MaterialDialog extends DialogBase implements
         switch (which) {
             default:
                 mBuilder.positiveText = title;
-                if (title == null) positiveButton.setVisibility(View.GONE);
+                positiveButton.setText(title);
+                positiveButton.setVisibility(title == null ? View.GONE : View.VISIBLE);
                 break;
             case NEUTRAL:
                 mBuilder.neutralText = title;
-                if (title == null) neutralButton.setVisibility(View.GONE);
+                neutralButton.setText(title);
+                neutralButton.setVisibility(title == null ? View.GONE : View.VISIBLE);
                 break;
             case NEGATIVE:
                 mBuilder.negativeText = title;
-                if (title == null) negativeButton.setVisibility(View.GONE);
+                negativeButton.setText(title);
+                negativeButton.setVisibility(title == null ? View.GONE : View.VISIBLE);
                 break;
         }
-        invalidateActions();
     }
 
     /**
@@ -1658,7 +1154,7 @@ public class MaterialDialog extends DialogBase implements
 
     public final void setContent(CharSequence content) {
         this.content.setText(content);
-        invalidateCustomViewAssociations(); // invalidates padding in content area scroll (if needed)
+        this.content.setVisibility(TextUtils.isEmpty(content) ? View.GONE : View.VISIBLE);
     }
 
     /**
@@ -1681,7 +1177,6 @@ public class MaterialDialog extends DialogBase implements
         }
         mBuilder.items = items;
         listView.setAdapter(mBuilder.adapter);
-        invalidateCustomViewAssociations();
     }
 
     public final int getCurrentProgress() {

+ 1 - 1
library/src/main/java/com/afollestad/materialdialogs/MaterialDialogAdapter.java

@@ -79,7 +79,7 @@ class MaterialDialogAdapter extends ArrayAdapter<CharSequence> {
     @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
     private void setupGravity(ViewGroup view) {
         final LinearLayout itemRoot = (LinearLayout) view;
-        final int gravityInt = MaterialDialog.gravityEnumToGravity(itemGravity);
+        final int gravityInt = itemGravity.getGravityInt();
         itemRoot.setGravity(gravityInt | Gravity.CENTER_VERTICAL);
 
         if (view.getChildCount() == 2) {

+ 0 - 14
library/src/main/java/com/afollestad/materialdialogs/base/DialogBase.java

@@ -3,8 +3,6 @@ package com.afollestad.materialdialogs.base;
 import android.app.AlertDialog;
 import android.content.Context;
 import android.content.DialogInterface;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
 import android.os.Message;
 import android.view.View;
 import android.view.ViewGroup;
@@ -13,10 +11,6 @@ import android.view.ViewGroup;
  * @author Aidan Follestad (afollestad)
  */
 public class DialogBase extends AlertDialog implements DialogInterface.OnShowListener {
-
-    protected final static String POSITIVE = "POSITIVE";
-    protected final static String NEGATIVE = "NEGATIVE";
-    protected final static String NEUTRAL = "NEUTRAL";
     private OnShowListener mShowListener;
 
     protected DialogBase(Context context) {
@@ -102,12 +96,4 @@ public class DialogBase extends AlertDialog implements DialogInterface.OnShowLis
             mShowListener.onShow(dialog);
     }
 
-    protected void setBackgroundCompat(View view, Drawable d) {
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
-            //noinspection deprecation
-            view.setBackgroundDrawable(d);
-        } else {
-            view.setBackground(d);
-        }
-    }
 }

+ 100 - 0
library/src/main/java/com/afollestad/materialdialogs/internal/MDButton.java

@@ -0,0 +1,100 @@
+package com.afollestad.materialdialogs.internal;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.support.v7.internal.text.AllCapsTransformationMethod;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.widget.TextView;
+
+import com.afollestad.materialdialogs.GravityEnum;
+import com.afollestad.materialdialogs.R;
+import com.afollestad.materialdialogs.util.DialogUtils;
+
+/**
+ * @author Kevin Barry (teslacoil) 4/02/2015
+ */
+public class MDButton extends TextView {
+
+    private boolean mStacked = false;
+    private GravityEnum mStackedGravity;
+
+    private int mStackedEndPadding;
+    private Drawable mStackedBackground;
+    private Drawable mDefaultBackground;
+
+    public MDButton(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context, attrs, 0, 0);
+    }
+
+    public MDButton(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(context, attrs, defStyleAttr, 0);
+    }
+
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    public MDButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        init(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        mStackedEndPadding = context.getResources()
+                .getDimensionPixelSize(R.dimen.md_dialog_frame_margin);
+        mStackedGravity = GravityEnum.END;
+    }
+
+    /**
+     * Set if the button should be displayed in stacked mode.
+     * This should only be called from MDRootLayout's onMeasure, and we must be measured
+     * after calling this.
+     */
+    /* package */ void setStacked(boolean stacked, boolean force) {
+        if (mStacked != stacked || force) {
+
+            setGravity(stacked ? (Gravity.CENTER_VERTICAL | mStackedGravity.getGravityInt()) : Gravity.CENTER);
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+                //noinspection ResourceType
+                setTextAlignment(stacked ? mStackedGravity.getTextAlignment() : TEXT_ALIGNMENT_CENTER);
+            }
+
+            DialogUtils.setBackgroundCompat(this, stacked ? mStackedBackground : mDefaultBackground);
+            if (stacked) {
+                setPadding(mStackedEndPadding, getPaddingTop(), mStackedEndPadding, getPaddingBottom());
+            } /* Else the padding was properly reset by the drawable */
+
+        }
+    }
+
+    public void setStackedGravity(GravityEnum gravity) {
+        mStackedGravity = gravity;
+    }
+
+    public void setStackedSelector(Drawable d) {
+        mStackedBackground = d;
+        if (mStacked)
+            setStacked(mStacked, true);
+    }
+
+    public void setDefaultSelector(Drawable d) {
+        mDefaultBackground = d;
+        if (!mStacked)
+            setStacked(mStacked, true);
+    }
+
+    public void setAllCapsCompat(boolean allCaps) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+            setAllCaps(allCaps);
+        } else {
+            if (allCaps) {
+                setTransformationMethod(new AllCapsTransformationMethod(getContext()));
+            } else {
+                setTransformationMethod(null);
+            }
+        }
+    }
+
+}

+ 442 - 0
library/src/main/java/com/afollestad/materialdialogs/internal/MDRootLayout.java

@@ -0,0 +1,442 @@
+package com.afollestad.materialdialogs.internal;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.os.Build;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.webkit.WebView;
+import android.widget.AdapterView;
+import android.widget.ScrollView;
+
+import com.afollestad.materialdialogs.GravityEnum;
+import com.afollestad.materialdialogs.R;
+import com.afollestad.materialdialogs.util.RecyclerUtil;
+
+/**
+ * @author Kevin Barry (teslacoil) 4/02/2015
+ *
+ * This is the top level view for all MaterialDialogs
+ * It handles the layout of:
+ *  titleFrame (md_stub_titleframe)
+ *  content (text, custom view, listview, etc)
+ *  buttonDefault... (either stacked or horizontal)
+ */
+public class MDRootLayout extends ViewGroup {
+    private static final String TAG = "MD.RootView";
+
+    private View mTitleBar;
+    private View mContent;
+
+    private static final int INDEX_NEUTRAL = 0;
+    private static final int INDEX_NEGATIVE = 1;
+    private static final int INDEX_POSITIVE = 2;
+    private boolean mDrawTopDivider = false;
+    private boolean mDrawBottomDivider = false;
+    private MDButton[] mButtons = new MDButton[3];
+    private boolean mForceStack = false;
+    private boolean mIsStacked = false;
+    private boolean mUseFullPadding = true;
+
+    private int mNoTitlePaddingFull;
+    private int mButtonPaddingFull;
+    private int mButtonBarHeight;
+
+    private GravityEnum mButtonGravity = GravityEnum.START;
+
+    /* Margin from dialog frame to first button */
+    private int mButtonHorizontalEdgeMargin;
+
+    private Paint mDividerPaint;
+
+    public MDRootLayout(Context context) {
+        super(context);
+        init(context, null, 0);
+    }
+
+    public MDRootLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context, attrs, 0);
+    }
+
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+    public MDRootLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(context, attrs, defStyleAttr);
+    }
+
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    public MDRootLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        init(context, attrs, defStyleAttr);
+    }
+
+    private void init(Context context, AttributeSet attrs, int defStyleAttr) {
+        Resources r = context.getResources();
+        mNoTitlePaddingFull = r.getDimensionPixelSize(R.dimen.md_notitle_vertical_padding);
+        mButtonPaddingFull = r.getDimensionPixelSize(R.dimen.md_button_frame_vertical_padding);
+
+        mButtonHorizontalEdgeMargin = r.getDimensionPixelSize(R.dimen.md_button_padding_frame_side);
+        mButtonBarHeight = r.getDimensionPixelSize(R.dimen.md_button_height);
+
+        mDividerPaint = new Paint();
+        mDividerPaint.setStrokeWidth(r.getDimensionPixelSize(R.dimen.md_divider_height));
+        mDividerPaint.setStyle(Paint.Style.STROKE);
+        setWillNotDraw(false);
+    }
+
+    @Override
+    public void onFinishInflate() {
+        super.onFinishInflate();
+
+        for (int i = 0; i < getChildCount(); i++) {
+            View v = getChildAt(i);
+            if (v.getId() == R.id.titleFrame) {
+                mTitleBar = v;
+            } else if (v.getId() == R.id.buttonDefaultNeutral) {
+                mButtons[INDEX_NEUTRAL] = (MDButton) v;
+            } else if (v.getId() == R.id.buttonDefaultNegative) {
+                mButtons[INDEX_NEGATIVE] = (MDButton) v;
+            } else if (v.getId() == R.id.buttonDefaultPositive) {
+                mButtons[INDEX_POSITIVE] = (MDButton) v;
+            } else {
+                mContent = v;
+            }
+        }
+    }
+
+    @Override
+    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int width = MeasureSpec.getSize(widthMeasureSpec);
+        int height = MeasureSpec.getSize(heightMeasureSpec);
+
+        mUseFullPadding = true;
+        boolean hasButtons = false;
+
+        final boolean stacked;
+        if (!mForceStack) {
+            int buttonsWidth = 0;
+            for (int i = 0; i < mButtons.length; i++) {
+                MDButton button = mButtons[i];
+                if (button != null && button.getVisibility() != View.GONE) {
+                    button.setStacked(false, false);
+                    measureChild(button, widthMeasureSpec, heightMeasureSpec);
+                    buttonsWidth += button.getMeasuredWidth();
+                    hasButtons = true;
+                }
+            }
+            Log.v(TAG, "buttonWidth " + buttonsWidth + " " + mButtonBarHeight);
+
+            int buttonBarPadding = getContext().getResources()
+                    .getDimensionPixelSize(R.dimen.md_neutral_button_margin);
+            final int buttonFrameWidth = width - 2 * buttonBarPadding;
+            stacked = buttonsWidth > buttonFrameWidth;
+        } else {
+            stacked = mForceStack;
+        }
+
+        int stackedHeight = 0;
+        mIsStacked = stacked;
+        if (stacked) {
+            for (int i = 0; i < mButtons.length; i++) {
+                MDButton button = mButtons[i];
+                if (button != null && button.getVisibility() != View.GONE) {
+                    button.setStacked(true, false);
+                    measureChild(button, widthMeasureSpec, heightMeasureSpec);
+                    stackedHeight += button.getMeasuredHeight();
+                }
+            }
+        }
+
+        int availableHeight = height;
+        int fullPadding = 0;
+        int minPadding = 0;
+        if (hasButtons) {
+            if (mIsStacked) {
+                availableHeight -= stackedHeight;
+                fullPadding += 2*mButtonPaddingFull;
+                minPadding += 2*mButtonPaddingFull;
+            } else {
+                availableHeight -= mButtonBarHeight;
+                fullPadding += 2 * mButtonPaddingFull;
+                /* No minPadding */
+            }
+        } else {
+            /* Content has 8dp, we add 16dp and get 24dp, the frame margin */
+            fullPadding += 2*mButtonPaddingFull;
+        }
+
+        if (mTitleBar != null && mTitleBar.getVisibility() != View.GONE) {
+            mTitleBar.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+                    MeasureSpec.UNSPECIFIED);
+            availableHeight -= mTitleBar.getMeasuredHeight();
+        } else {
+            fullPadding += mNoTitlePaddingFull;
+        }
+
+        if (mContent != null && mContent.getVisibility() != View.GONE) {
+            mContent.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+                    MeasureSpec.makeMeasureSpec(availableHeight - minPadding, MeasureSpec.AT_MOST));
+
+            if (mContent.getMeasuredHeight() < availableHeight - fullPadding) {
+                mUseFullPadding = true;
+                availableHeight -= mContent.getMeasuredHeight() + fullPadding;
+            } else {
+                mUseFullPadding = false;
+                availableHeight = 0;
+                mDrawTopDivider =  mTitleBar != null && mTitleBar.getVisibility() != View.GONE &&
+                        canViewOrChildScroll(mContent, false);
+                mDrawBottomDivider =  hasButtons &&
+                        canViewOrChildScroll(mContent, true);
+            }
+
+        }
+
+        setMeasuredDimension(width, height - availableHeight);
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        if (mContent != null) {
+            if (mDrawTopDivider) {
+                int y = mContent.getTop();
+                canvas.drawLine(0, y, getMeasuredWidth(), y, mDividerPaint);
+            }
+
+            if (mDrawBottomDivider) {
+                int y = mContent.getBottom();
+                canvas.drawLine(0, y, getMeasuredWidth(), y, mDividerPaint);
+            }
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, final int l, int t, final int r, int b) {
+        if (mTitleBar != null && mTitleBar.getVisibility() != View.GONE) {
+            int height = mTitleBar.getMeasuredHeight();
+            mTitleBar.layout(l, t, r, t + height);
+            t += height;
+        } else if (mUseFullPadding) {
+            t += mNoTitlePaddingFull;
+        }
+
+        if (mContent != null && mContent.getVisibility() != View.GONE) {
+            mContent.layout(l, t, r, t + mContent.getMeasuredHeight());
+        }
+
+        if (mIsStacked) {
+            b -= mButtonPaddingFull;
+            for (int i = 0; i < mButtons.length; i++) {
+                if (mButtons[i] != null && mButtons[i].getVisibility() != View.GONE) {
+                    mButtons[i].layout(l, b - mButtons[i].getMeasuredHeight(), r, b);
+                    b -= mButtons[i].getMeasuredHeight();
+                }
+            }
+        } else {
+            int barTop;
+            int barBottom = b;
+            if (mUseFullPadding) {
+                barBottom -= mButtonPaddingFull;
+            }
+            barTop = barBottom - mButtonBarHeight;
+            /* START:
+               Neutral   Negative  Positive
+
+               CENTER:
+               Negative  Neutral   Positive
+
+               END:
+               Positive  Negative  Neutral
+
+               (With no Positive, Negative takes it's place except for CENTER)
+             */
+            int offset = mButtonHorizontalEdgeMargin;
+
+            /* Used with CENTER gravity */
+            int neutralLeft = -1;
+            int neutralRight = -1;
+
+            if (mButtons[INDEX_POSITIVE] != null && mButtons[INDEX_POSITIVE].getVisibility() != View.GONE) {
+                int bl, br;
+
+                if (mButtonGravity == GravityEnum.END) {
+                    bl = l + offset;
+                    br = bl + mButtons[INDEX_POSITIVE].getMeasuredWidth();
+                } else { /* START || CENTER */
+                    br = r - offset;
+                    bl = br - mButtons[INDEX_POSITIVE].getMeasuredWidth();
+                    neutralRight = bl;
+                }
+
+                mButtons[INDEX_POSITIVE].layout(bl, barTop, br, barBottom);
+
+                offset += mButtons[INDEX_POSITIVE].getMeasuredWidth();
+            }
+
+            if (mButtons[INDEX_NEGATIVE] != null && mButtons[INDEX_NEGATIVE].getVisibility() != View.GONE) {
+
+                int bl, br;
+
+                if (mButtonGravity == GravityEnum.END) {
+                    bl = l + offset;
+                    br = bl + mButtons[INDEX_NEGATIVE].getMeasuredWidth();
+                } else if (mButtonGravity == GravityEnum.START) {
+                    br = r - offset;
+                    bl = br - mButtons[INDEX_NEGATIVE].getMeasuredWidth();
+                } else { /* CENTER */
+                    bl = l + mButtonHorizontalEdgeMargin;
+                    br = bl + mButtons[INDEX_NEGATIVE].getMeasuredWidth();
+                    neutralLeft = br;
+                }
+
+                mButtons[INDEX_NEGATIVE].layout(bl, barTop, br, barBottom);
+            }
+
+            if (mButtons[INDEX_NEUTRAL] != null && mButtons[INDEX_NEUTRAL].getVisibility() != View.GONE) {
+                int bl, br;
+                if (mButtonGravity == GravityEnum.END) {
+                    br = r - mButtonHorizontalEdgeMargin;
+                    bl = br - mButtons[INDEX_NEUTRAL].getMeasuredWidth();
+                } else if (mButtonGravity == GravityEnum.START) {
+                    bl = l + mButtonHorizontalEdgeMargin;
+                    br = bl + mButtons[INDEX_NEUTRAL].getMeasuredWidth();
+                } else { /* CENTER */
+                    if (neutralLeft == -1 && neutralRight != -1) {
+                        neutralLeft = neutralRight - mButtons[INDEX_NEUTRAL].getMeasuredWidth();
+                    } else if (neutralRight == -1 && neutralLeft != -1) {
+                        neutralRight = neutralLeft + mButtons[INDEX_NEUTRAL].getMeasuredWidth();
+                    } else if (neutralRight == -1 && neutralLeft == -1) {
+                        neutralLeft = (r - l)/2 - mButtons[INDEX_NEUTRAL].getMeasuredWidth()/2;
+                        neutralRight = neutralLeft + mButtons[INDEX_NEUTRAL].getMeasuredWidth();
+                    }
+                    bl = neutralLeft;
+                    br = neutralRight;
+                }
+
+                mButtons[INDEX_NEUTRAL].layout(bl, barTop, br, barBottom);
+            }
+        }
+    }
+
+    public void setForceStack(boolean forceStack) {
+        mForceStack = forceStack;
+        invalidate();
+    }
+
+    public void setDividerColor(int color) {
+        mDividerPaint.setColor(color);
+        invalidate();
+    }
+
+    public void setButtonGravity(GravityEnum gravity) {
+        mButtonGravity = gravity;
+    }
+
+    public void setButtonStackedGravity(GravityEnum gravity) {
+        for (int i = 0; i < mButtons.length; i++) {
+            if (mButtons[i] != null)
+                mButtons[i].setStackedGravity(gravity);
+        }
+    }
+
+    private static boolean canViewOrChildScroll(View view, boolean atBottom) {
+        if (view == null)
+            return false;
+        if (view instanceof ScrollView) {
+            return canScrollViewScroll((ScrollView) view);
+        } else if (view instanceof AdapterView) {
+            return canAdapterViewScroll((AdapterView) view);
+        } else if (view instanceof WebView) {
+            return canWebViewScroll((WebView) view);
+        } else if (view instanceof RecyclerView) {
+            return RecyclerUtil.canRecyclerViewScroll(view);
+        } else if (view instanceof ViewGroup) {
+            if (atBottom) {
+                return canViewOrChildScroll(getBottomView((ViewGroup) view), true);
+            } else {
+                return canViewOrChildScroll(getTopView((ViewGroup) view), false);
+            }
+        } else {
+            return false;
+        }
+    }
+
+    private static boolean canScrollViewScroll(ScrollView sv) {
+        if (sv.getChildCount() == 0)
+            return false;
+        final int childHeight = sv.getChildAt(0).getMeasuredHeight();
+        return sv.getMeasuredHeight() - sv.getPaddingTop() - sv.getPaddingBottom() < childHeight;
+    }
+
+    private static boolean canWebViewScroll(WebView view) {
+        return view.getMeasuredHeight() > view.getContentHeight();
+    }
+
+    private static boolean canAdapterViewScroll(AdapterView lv) {
+        /* Force it to layout it's children */
+        if (lv.getLastVisiblePosition() == -1)
+            return false;
+
+        /* We can scroll if the first or last item is not visible */
+        boolean firstItemVisible = lv.getFirstVisiblePosition() == 0;
+        boolean lastItemVisible = lv.getLastVisiblePosition() == lv.getCount() - 1;
+
+        if (firstItemVisible && lastItemVisible) {
+            /* Or the first item's top is above or own top */
+            if (lv.getChildAt(0).getTop() < lv.getPaddingTop())
+                return true;
+
+            /* or the last item's bottom is beyond our own bottom */
+            return lv.getChildAt(lv.getChildCount() - 1).getBottom() >
+                    lv.getHeight() - lv.getPaddingBottom();
+        }
+
+        return true;
+    }
+
+    /**
+     * Find the view touching the bottom of this ViewGroup. Non visible children are ignored,
+     * however getChildDrawingOrder is not taking into account for simplicity and because it behaves
+     * inconsistently across platform versions.
+     *
+     * @return View touching the bottom of this viewgroup or null
+     */
+    @Nullable
+    private static View getBottomView(ViewGroup viewGroup) {
+        if (viewGroup == null)
+            return null;
+        View bottomView = null;
+        for (int i = viewGroup.getChildCount() - 1; i >= 0; i--) {
+            View child = viewGroup.getChildAt(i);
+            if (child.getVisibility() == View.VISIBLE && child.getBottom() == viewGroup.getMeasuredHeight()) {
+                bottomView = child;
+                break;
+            }
+        }
+        return bottomView;
+    }
+
+    @Nullable
+    private static View getTopView(ViewGroup viewGroup) {
+        if (viewGroup == null)
+            return null;
+        View topView = null;
+        for (int i = viewGroup.getChildCount() - 1; i >= 0; i--) {
+            View child = viewGroup.getChildAt(i);
+            if (child.getVisibility() == View.VISIBLE && child.getTop() == 0) {
+                topView = child;
+                break;
+            }
+        }
+        return topView;
+    }
+}

+ 11 - 0
library/src/main/java/com/afollestad/materialdialogs/util/DialogUtils.java

@@ -4,7 +4,9 @@ import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Color;
 import android.graphics.drawable.Drawable;
+import android.os.Build;
 import android.support.annotation.AttrRes;
+import android.view.View;
 
 import com.afollestad.materialdialogs.GravityEnum;
 
@@ -107,4 +109,13 @@ public class DialogUtils {
         double darkness = 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255;
         return darkness >= 0.5;
     }
+
+    public static void setBackgroundCompat(View view, Drawable d) {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
+            //noinspection deprecation
+            view.setBackgroundDrawable(d);
+        } else {
+            view.setBackground(d);
+        }
+    }
 }

+ 17 - 32
library/src/main/res/layout/md_dialog_basic.xml

@@ -1,46 +1,31 @@
-<LinearLayout
+<com.afollestad.materialdialogs.internal.MDRootLayout
     android:orientation="vertical"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools">
 
-    <LinearLayout
-        android:id="@+id/mainFrame"
-        android:orientation="vertical"
-        android:layout_width="match_parent"
-        android:layout_height="0dp"
-        android:layout_weight="1">
-
-        <include layout="@layout/md_stub_titleframe" />
+    <include layout="@layout/md_stub_titleframe" />
 
-        <View
-            android:id="@+id/titleBarDivider"
-            android:layout_width="match_parent"
-            android:layout_height="1dp"
-            android:layout_marginTop="@dimen/md_content_vertical_padding"
-            android:layout_marginBottom="-1dp"
-            android:visibility="gone" />
+    <ScrollView
+        android:id="@+id/contentScrollView"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:clipToPadding="false"
+        android:paddingTop="@dimen/md_content_padding_top"
+        android:paddingBottom="@dimen/md_content_padding_bottom">
 
-        <ScrollView
-            android:id="@+id/contentScrollView"
+        <TextView
+            android:id="@+id/content"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:visibility="gone"
-            android:clipToPadding="false"
-            android:paddingBottom="@dimen/md_content_vertical_padding">
-
-            <TextView
-                android:id="@+id/content"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:textSize="@dimen/md_content_textsize"
-                tools:text="Content" />
-
-        </ScrollView>
+            android:paddingLeft="@dimen/md_dialog_frame_margin"
+            android:paddingRight="@dimen/md_dialog_frame_margin"
+            android:textSize="@dimen/md_content_textsize"
+            tools:text="Content" />
 
-    </LinearLayout>
+    </ScrollView>
 
     <include layout="@layout/md_stub_actionbuttons" />
 
-</LinearLayout>
+</com.afollestad.materialdialogs.internal.MDRootLayout>

+ 7 - 24
library/src/main/res/layout/md_dialog_custom.xml

@@ -1,33 +1,16 @@
-<LinearLayout
+<com.afollestad.materialdialogs.internal.MDRootLayout
     android:orientation="vertical"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <LinearLayout
-        android:id="@+id/mainFrame"
-        android:orientation="vertical"
-        android:layout_width="match_parent"
-        android:layout_height="0dp"
-        android:layout_weight="1">
-
-        <include layout="@layout/md_stub_titleframe" />
-
-        <View
-            android:id="@+id/titleBarDivider"
-            android:layout_width="match_parent"
-            android:layout_height="1dp"
-            android:layout_marginTop="@dimen/md_content_vertical_padding"
-            android:layout_marginBottom="-1dp"
-            android:visibility="gone" />
+    <include layout="@layout/md_stub_titleframe" />
 
-        <FrameLayout
-            android:id="@+id/customViewFrame"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content" />
-
-    </LinearLayout>
+    <FrameLayout
+        android:id="@+id/customViewFrame"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
 
     <include layout="@layout/md_stub_actionbuttons" />
 
-</LinearLayout>
+</com.afollestad.materialdialogs.internal.MDRootLayout>

+ 12 - 21
library/src/main/res/layout/md_dialog_list.xml

@@ -1,39 +1,31 @@
-<LinearLayout
+<com.afollestad.materialdialogs.internal.MDRootLayout
     android:orientation="vertical"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools">
 
+    <include layout="@layout/md_stub_titleframe" />
+
     <LinearLayout
-        android:id="@+id/mainFrame"
         android:orientation="vertical"
         android:layout_width="match_parent"
-        android:layout_height="0dp"
-        android:layout_weight="1">
-
-        <include layout="@layout/md_stub_titleframe" />
-
-        <View
-            android:id="@+id/titleBarDivider"
-            android:layout_width="match_parent"
-            android:layout_height="1dp"
-            android:layout_marginTop="@dimen/md_content_vertical_padding"
-            android:layout_marginBottom="-1dp"
-            android:visibility="gone" />
+        android:layout_height="match_parent">
 
         <ScrollView
             android:id="@+id/contentScrollView"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:visibility="gone"
-            android:clipToPadding="false"
-            android:paddingBottom="@dimen/md_content_vertical_padding">
+            android:clipToPadding="false">
 
             <TextView
                 android:id="@+id/content"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
+                android:paddingLeft="@dimen/md_dialog_frame_margin"
+                android:paddingTop="@dimen/md_content_padding_top"
+                android:paddingRight="@dimen/md_dialog_frame_margin"
+                android:paddingBottom="@dimen/md_content_padding_bottom"
                 android:textSize="@dimen/md_content_textsize"
                 tools:text="Content" />
 
@@ -44,7 +36,6 @@
             android:layout_width="match_parent"
             android:layout_height="wrap_content">
 
-            <!-- Top padding cancelled out in code if there's a title -->
             <ListView
                 android:id="@+id/contentListView"
                 android:layout_width="match_parent"
@@ -53,8 +44,8 @@
                 android:divider="@null"
                 android:dividerHeight="0dp"
                 android:clipToPadding="false"
-                android:paddingTop="12dp"
-                android:paddingBottom="12dp" />
+                android:paddingTop="@dimen/md_content_padding_top"
+                android:paddingBottom="@dimen/md_content_padding_bottom" />
 
         </FrameLayout>
 
@@ -62,4 +53,4 @@
 
     <include layout="@layout/md_stub_actionbuttons" />
 
-</LinearLayout>
+</com.afollestad.materialdialogs.internal.MDRootLayout>

+ 9 - 14
library/src/main/res/layout/md_dialog_progress.xml

@@ -1,26 +1,21 @@
-<LinearLayout
+<com.afollestad.materialdialogs.internal.MDRootLayout
     android:orientation="vertical"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     xmlns:android="http://schemas.android.com/apk/res/android">
 
+    <include layout="@layout/md_stub_titleframe" />
+
     <LinearLayout
-        android:id="@+id/mainFrame"
         android:orientation="vertical"
         android:layout_width="match_parent"
         android:layout_height="0dp"
         android:layout_weight="1"
-        android:paddingBottom="@dimen/md_title_frame_margin_bottom">
-
-        <include layout="@layout/md_stub_titleframe" />
-
-        <View
-            android:id="@+id/titleBarDivider"
-            android:layout_width="match_parent"
-            android:layout_height="1dp"
-            android:layout_marginTop="@dimen/md_content_vertical_padding"
-            android:layout_marginBottom="-1dp"
-            android:visibility="gone" />
+        android:paddingLeft="@dimen/md_dialog_frame_margin"
+        android:paddingRight="@dimen/md_dialog_frame_margin"
+        android:paddingTop="@dimen/md_content_padding_top"
+        android:paddingBottom="@dimen/md_content_padding_bottom"
+        >
 
         <include layout="@layout/md_stub_progress" />
 
@@ -28,4 +23,4 @@
 
     <include layout="@layout/md_stub_actionbuttons" />
 
-</LinearLayout>
+</com.afollestad.materialdialogs.internal.MDRootLayout>

+ 4 - 22
library/src/main/res/layout/md_dialog_progress_indeterminate.xml

@@ -1,31 +1,13 @@
-<LinearLayout
+<com.afollestad.materialdialogs.internal.MDRootLayout
     android:orientation="vertical"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <LinearLayout
-        android:id="@+id/mainFrame"
-        android:orientation="vertical"
-        android:layout_width="match_parent"
-        android:layout_height="0dp"
-        android:layout_weight="1"
-        android:paddingBottom="@dimen/md_dialog_frame_margin">
+    <include layout="@layout/md_stub_titleframe" />
 
-        <include layout="@layout/md_stub_titleframe" />
-
-        <View
-            android:id="@+id/titleBarDivider"
-            android:layout_width="match_parent"
-            android:layout_height="1dp"
-            android:layout_marginTop="@dimen/md_content_vertical_padding"
-            android:layout_marginBottom="-1dp"
-            android:visibility="gone" />
-
-        <include layout="@layout/md_stub_progress_indeterminate" />
-
-    </LinearLayout>
+    <include layout="@layout/md_stub_progress_indeterminate" />
 
     <include layout="@layout/md_stub_actionbuttons" />
 
-</LinearLayout>
+</com.afollestad.materialdialogs.internal.MDRootLayout>

+ 21 - 105
library/src/main/res/layout/md_stub_actionbuttons.xml

@@ -1,111 +1,27 @@
 <merge xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools">
 
-    <View
-        android:id="@+id/buttonBarDivider"
-        android:layout_width="match_parent"
-        android:layout_height="1dp"
-        android:layout_marginTop="-1dp"
-        android:visibility="gone" />
 
-    <RelativeLayout
-        android:id="@+id/buttonDefaultFrame"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_marginLeft="@dimen/md_neutral_button_margin"
-        android:layout_marginStart="@dimen/md_neutral_button_margin"
-        android:layout_marginRight="@dimen/md_button_padding_frame_side"
-        android:layout_marginEnd="@dimen/md_button_padding_frame_side"
-        android:layout_marginBottom="@dimen/md_button_frame_vertical_padding">
-
-        <FrameLayout
-            android:id="@+id/buttonDefaultNeutral"
-            style="@style/MD_ActionButton"
-            tools:layout_alignParentLeft="true"
-            tools:layout_alignParentStart="true">
-
-            <TextView
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                tools:text="Neutral"
-                style="@style/MD_ActionButton.Text" />
-
-        </FrameLayout>
-
-        <FrameLayout
-            android:id="@+id/buttonDefaultNegative"
-            style="@style/MD_ActionButton"
-            tools:toLeftOf="@+id/buttonDefaultPositive"
-            tools:toStartOf="@+id/buttonDefaultPositive">
-
-            <TextView
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                tools:text="Negative"
-                style="@style/MD_ActionButton.Text" />
-
-        </FrameLayout>
-
-        <FrameLayout
-            android:id="@+id/buttonDefaultPositive"
-            style="@style/MD_ActionButton"
-            tools:layout_alignParentRight="true"
-            tools:layout_alignParentEnd="true">
-
-            <TextView
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                tools:text="Positive"
-                style="@style/MD_ActionButton.Text" />
-
-        </FrameLayout>
-
-    </RelativeLayout>
-
-    <LinearLayout
-        android:id="@+id/buttonStackedFrame"
-        android:orientation="vertical"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="@dimen/md_button_frame_vertical_padding"
-        android:layout_marginBottom="@dimen/md_button_frame_vertical_padding">
-
-        <FrameLayout
-            android:id="@+id/buttonStackedPositive"
-            style="@style/MD_ActionButtonStacked">
-
-            <TextView
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                tools:text="Positive"
-                style="@style/MD_ActionButton.Text" />
-
-        </FrameLayout>
-
-        <FrameLayout
-            android:id="@+id/buttonStackedNegative"
-            style="@style/MD_ActionButtonStacked">
-
-            <TextView
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                tools:text="Negative"
-                style="@style/MD_ActionButton.Text" />
-
-        </FrameLayout>
-
-        <FrameLayout
-            android:id="@+id/buttonStackedNeutral"
-            style="@style/MD_ActionButtonStacked">
-
-            <TextView
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                tools:text="Neutral"
-                style="@style/MD_ActionButton.Text" />
-
-        </FrameLayout>
-
-    </LinearLayout>
+    <com.afollestad.materialdialogs.internal.MDButton
+        android:id="@+id/buttonDefaultNeutral"
+        style="@style/MD_ActionButton.Text"
+        tools:text="Neutral"
+        />
+
+    <com.afollestad.materialdialogs.internal.MDButton
+        android:id="@+id/buttonDefaultNegative"
+        style="@style/MD_ActionButton.Text"
+        tools:layout_alignParentLeft="true"
+        tools:layout_alignParentStart="true"
+        tools:text="Negative"
+        />
+
+    <com.afollestad.materialdialogs.internal.MDButton
+        android:id="@+id/buttonDefaultPositive"
+        style="@style/MD_ActionButton.Text"
+        tools:layout_alignParentLeft="true"
+        tools:layout_alignParentStart="true"
+        tools:text="Positive"
+        />
 
 </merge>

+ 7 - 3
library/src/main/res/layout/md_stub_progress_indeterminate.xml

@@ -4,14 +4,16 @@
     android:orientation="horizontal"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
+    android:paddingLeft="@dimen/md_dialog_frame_margin"
+    android:paddingRight="@dimen/md_dialog_frame_margin"
+    android:paddingTop="@dimen/md_content_padding_top"
+    android:paddingBottom="@dimen/md_content_padding_top"
     android:gravity="end|center_vertical">
 
     <ProgressBar
         android:id="@android:id/progress"
         android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginLeft="@dimen/md_dialog_frame_margin"
-        android:layout_marginStart="@dimen/md_dialog_frame_margin" />
+        android:layout_height="wrap_content" />
 
     <TextView
         android:id="@+id/content"
@@ -21,6 +23,8 @@
         android:textSize="16sp"
         tools:text="Message"
         tools:ignore="UnusedAttribute"
+        android:paddingLeft="16dp"
+        android:paddingStart="16dp"
         android:gravity="start"
         android:textAlignment="viewStart" />
 

+ 42 - 12
library/src/main/res/values/dimens.xml

@@ -1,29 +1,58 @@
 <resources>
     <!-- See http://www.google.com/design/spec/components/dialogs.html#dialogs-specs -->
+
+    <!-- Notes:
+
+        The specs specify 16dp between the content and the buttons and imply 16dp between
+        the title and the content. Also 24dp above the title and 8dp below the buttons.
+
+        Additionally, we interpret:
+         16dp between title and content
+         24dp above content if there is no title
+         24dp below content if there is no button bar
+
+        When dialog is large enough to scroll, some padding is removed to make space and
+        keep things symmetrical.
+        Reduced paddings are:
+         8dp above content if there is no title (0dp when scrolled)
+         8dp below content if there is no button bar (0dp when scrolled)
+         8dp between content and button bar
+         0dp below button bar
+
+        For implementation:
+         Content view gets 8dp vertical padding
+         Title bar gets 24dp top, 8dp bottom
+         Buttons get 8dp bottom
+         Stacked buttons get 24dp end padding, the spec's 16 plus the spec's 8dp button padding
+
+         MDRootLayout takes care of margins when no title or not button bar,
+         and the margin between buttons and content when not scrolling.
+    -->
+
     <!-- Margin around the dialog, excluding the button bar -->
     <dimen name="md_dialog_frame_margin">24dp</dimen>
-    <dimen name="md_title_frame_margin_bottom">16dp</dimen>
-    <dimen name="md_title_frame_margin_bottom_list">4dp</dimen>
+    <!-- Total title margin bottom is 16, but we split this between content and title -->
+    <dimen name="md_title_frame_margin_bottom">8dp</dimen>
+    <!-- The desired padding when no title is visible,
+         This plus md_content_padding_top should equals md_dialog_frame_margin -->
+    <dimen name="md_notitle_vertical_padding">16dp</dimen>
+
+    <dimen name="md_content_padding_top">8dp</dimen>
+
     <dimen name="md_button_min_width">42dp</dimen>
-    <!-- Above and below buttons, 36+12=48 for the height of the button frame -->
+    <!-- Above and below buttons, 36+6+6=48 for the height of the button frame -->
     <dimen name="md_button_inset_vertical">6dp</dimen>
     <dimen name="md_button_inset_horizontal">4dp</dimen>
     <dimen name="md_button_textpadding_horizontal">1dp</dimen>
     <dimen name="md_button_padding_horizontal">8dp</dimen>
     <dimen name="md_button_padding_vertical">4dp</dimen>
     <dimen name="md_button_padding_horizontal_internalexternal">32dp</dimen>
-    <!-- 16dp - 4dp (inset) -->
+    <!-- 16dp - 4dp (inset from background drawable) -->
     <dimen name="md_button_padding_frame_side">12dp</dimen>
     <dimen name="md_neutral_button_margin">12dp</dimen>
-    <!--
-            Applied to content scrollview/listview and customview, the spec calls for 16dp between the
-             bottom of the content and the top of the button bar, we split this between the two for
-             ease of layouts and adjustments. Likewise the spec implies 16dp between the title and
-             content and we split that as well.
 
-             Splitting makes it easier to have consistent padding against the dividers.
-    -->
-    <dimen name="md_content_vertical_padding">8dp</dimen>
+    <!-- actual content padding bottom is 16dp, but we split between button bar and content -->
+    <dimen name="md_content_padding_bottom">8dp</dimen>
     <dimen name="md_button_frame_vertical_padding">8dp</dimen>
     <dimen name="md_button_height">48dp</dimen>
     <dimen name="md_title_textsize">18sp</dimen>
@@ -36,5 +65,6 @@
     <dimen name="md_icon_max_size">48dp</dimen>
     <dimen name="md_listitem_margin_left">24dp</dimen>
     <dimen name="md_action_corner_radius">2dp</dimen>
+    <dimen name="md_divider_height">1dp</dimen>
 
 </resources>

+ 0 - 1
library/src/main/res/values/styles.xml

@@ -49,7 +49,6 @@
         <item name="android:minWidth">@dimen/md_button_min_width</item>
         <item name="android:paddingLeft">@dimen/md_button_textpadding_horizontal</item>
         <item name="android:paddingRight">@dimen/md_button_textpadding_horizontal</item>
-        <item name="android:duplicateParentState">true</item>
     </style>
 
     <!-- Light dialog theme for devices prior Honeycomb -->