Browse Source

Refactor padding to be more efficient

The previous setup was causing lag when showing or updating the dialog,
because it needed to wait until everything was laid out before adjusting
the padding, which would trigger a second layout.

This method uses a custom ViewGroup (MDRootLayout) which determines
padding and button stacking onMeasure.

Changes were also made with how the padding was split up, to avoid having to
move padding between views.
Kevin Barry 10 years ago
parent
commit
7c092d78de

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