60

I have been having this problem in an application I am building. Please ignore all of the design shortcomings and lack of best practice approaches, this is purely to show an example of what I cannot solve.

I have DialogFragment which returns a basic AlertDialog with a custom View set using AlertDialog.Builder.setView(). If this View has a specific size requirement, how do I get the Dialog to correctly resize itself to display all of the content in the custom View?

This is the example code I have been using:

package com.test.test;

import android.os.Bundle;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Use a button for launching
        Button b = new Button(this);
        b.setText("Launch");
        b.setOnClickListener(new OnClickListener() {    
            @Override
            public void onClick(View v) {
                // Launch the dialog
                myDialog d = new myDialog();
                d.show(getFragmentManager(), null);
            }
        });

        setContentView(b);
    }

    public static class myDialog extends DialogFragment {

        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {           
            // Create the dialog
            AlertDialog.Builder db = new AlertDialog.Builder(getActivity());
            db.setTitle("Test Alert Dialog:");
            db.setView(new myView(getActivity()));

            return db.create();
        }

        protected class myView extends View {
            Paint p = null;

            public myView(Context ct) {
                super(ct);

                // Setup paint for the drawing
                p = new Paint();
                p.setColor(Color.MAGENTA);
                p.setStyle(Style.STROKE);
                p.setStrokeWidth(10);
            }

            @Override
            protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
                setMeasuredDimension(800, 300);
            }

            @Override
            protected void onDraw(Canvas canvas) {
                // Draw a rectangle showing the bounds of the view
                canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), p);
            }
        }
    }
}

A Button is created, which opens the DialogFragment on a click. The custom View (myView) is required to have a width of 800 and height of 300 which is correctly set in an override of onMeasure(). This View, draws its measured bounds in magenta for debugging purposes.

The 800 width is wider than the default Dialog size on my device, but is clipped rather than stretching correctly.

I have looked through the following solutions:

I have deduced the following two coding approaches:

  1. Get the WindowManager.LayoutParams of the Dialog and override them using myDialog.getDialog().getWindow().get/setAttributes()
  2. Using the setLayout(w, h) method through myDialog.getDialog().getWindow().setLayout()

I have tried them everywhere I can think of (overriding onStart(), in a onShowListener, after the Dialog is created and shown, etc) and can generally get both methods to work correctly if the LayoutParams are supplied a specific value. But whenever WRAP_CONTENT is supplied, nothing happens.

Any suggestions?

EDIT:

Screenshot of the situation: enter image description here

Screenshot of a specific value (note 900 is entered here, 850 doesn't cover the entire width of the View, which makes sense given the entire window is being adjusted. So that provides - if another was needed - reason why WRAP_CONTENT is essential / fixed values are not appropriate): enter image description here

Community
  • 1
  • 1
btalb
  • 6,937
  • 11
  • 36
  • 44

8 Answers8

65

I have a working solution that to be honest, I think digs way too deep to obtain such a simple result. But here it is:

What exactly is happening:

By opening the Dialog layout with the Hierarchy Viewer, I was able to examine the entire layout of the AlertDialog and what exactly what was going on: enter image description here

The blue highlight is all of the high level parts (Window, frames for the Dialog visual style, etc) and from the end of the blue down is where the components for the AlertDialog are (red = title, yellow = a scrollview stub, maybe for list AlertDialogs, green = Dialog content i.e. custom view, orange = buttons).

From here it was clear that the 7-view path (from the start of the blue to the end of the green) was what was failing to correctly WRAP_CONTENT. Looking at the LayoutParams.width of each View revealed that all are given LayoutParams.width = MATCH_PARENT and somewhere (I guess at the top) a size is set. So if you follow that tree, it is clear that your custom View at the bottom of the tree, will never be able to affect the size of the Dialog.

So what were the existing solutions doing?

  • Both of the coding approaches mentioned in my question were simply getting the top View and modifying its LayoutParams. Obviously, with all View objects in the tree matching the parent, if the top level is set a static size, the whole Dialog will change size. But if the top level is set to WRAP_CONTENT, all the rest of the View objects in the tree are still looking up the tree to "MATCH their PARENT", as opposed to looking down the tree to "WRAP their CONTENT".

How to solve the problem:

Bluntly, change the LayoutParams.width of all View objects in the affecting path to be WRAP_CONTENT.

I found that this could only be done AFTER onStart lifecycle step of the DialogFragment is called. So the onStart is implemented like:

@Override
public void onStart() { 
    // This MUST be called first! Otherwise the view tweaking will not be present in the displayed Dialog (most likely overriden)
    super.onStart();

    forceWrapContent(myCustomView);
}

Then the function to appropriately modify the View hierarchy LayoutParams:

protected void forceWrapContent(View v) {
    // Start with the provided view
    View current = v;

    // Travel up the tree until fail, modifying the LayoutParams
    do {
        // Get the parent
        ViewParent parent = current.getParent();    

        // Check if the parent exists
        if (parent != null) {
            // Get the view
            try {
                current = (View) parent;
            } catch (ClassCastException e) {
                // This will happen when at the top view, it cannot be cast to a View
                break;
            }

            // Modify the layout
            current.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
        }
    } while (current.getParent() != null);

    // Request a layout to be re-done
    current.requestLayout();
}

And here is the working result: enter image description here

It confuses me why the entire Dialog would not want to be WRAP_CONTENT with an explicit minWidth set to handle all cases that fit inside the default size, but I'm sure there is a good reason for it the way it is (would be interested to hear it).

btalb
  • 6,937
  • 11
  • 36
  • 44
  • After *hours* of trying to find a better solution, this is the only one that works. – sulai Dec 10 '13 at 16:07
  • 1
    Nice solution, it saved me a lot of time! Making dialog smaller robbed several hours of my time. Implicitly changing layout params to `match_parent` is annoying. – Johnny Doe Mar 04 '14 at 06:55
  • 3
    What is myCustomView suppose to be? I passed in getCurrentFocus() Edit: Passed in the View that the dialog uses, got it! This is great! Edit2: I seem to have a bit of padding on the right – Zen Apr 01 '14 at 11:13
  • This solution worked for me. Thank you. Instead using `DialogFragment` would it be an answer to use Activity? using: `` Check this (end) http://developer.android.com/guide/topics/ui/dialogs.html#CustomLayout – Mert Gülsoy Jan 22 '15 at 21:44
  • 1
    Great job! I have managed to make width of my Support Library material-styled dialogs wrap by combining this approach with setting window layout params. I also had to set a minWidth and tinker with couple of `@dimen/` values, enforced upon the dialog by Support Library itself. – user1643723 Jun 26 '15 at 04:42
  • 1
    Worked for normal AlertDialog as well for me with some changes. I've spent days looking for solution. Thank you. – El_o.di Aug 12 '16 at 15:56
  • Did not work for me when I used android.support.v7.app.AlertDialog. Anyone got any ideas? – Henrik Jan 28 '17 at 11:36
  • @user1643723 would be great to add example for support dialogs here – user924 Dec 09 '17 at 19:42
  • @user924 all of those resource values are internal APIs, so they might just change to different names in next version of support lib. Open the source code of support library, find the dialog template and look yourself, it is pretty easy (there should be ~3 layout files, included into each other, that together constitute dialog layout). – user1643723 Dec 10 '17 at 01:57
  • @user1643723 for me `windowMinWidthMajor` & `windowMinWidthMinor` solved the problem (in custom dialog theme which extends some appcompat alert dialog light or dark) – user924 Dec 10 '17 at 10:01
  • I think this improves your code readability: `if (parent instanceof View)`, so you can cast into `View` and avoid the try and catch clauses. – Simple Jun 18 '20 at 20:40
35

After

dialog.show();

just use

dialog.getWindow().setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, yourHeight);

Very simple solution, but it works for me. I'm extending a dialog though but assumes that this will work for DialogFragment also.

sweggersen
  • 2,342
  • 1
  • 22
  • 27
  • 2
    I had a class that extended Dialog and this code worked in an override of show() just after super.show();. I also changed both of them to have WRAP_CONTENT; that was fine too. – Unknownweirdo Mar 10 '15 at 16:49
17

AlertDialog's use these two window attributes to define the smallest size they can be so that on Tablets they float with a reasonable width in the center of the screen.

http://developer.android.com/reference/android/R.attr.html#windowMinWidthMajor http://developer.android.com/reference/android/R.attr.html#windowMinWidthMinor

You can extend a default Dialog style of your choice and change the values to apply your own logic.

Simon
  • 10,932
  • 50
  • 49
  • This is the only solution that works for me. You can apply the style through `AlertDialog.Builder(context, R.style.AppTheme_Dialog)`. – Joshua Aug 03 '17 at 09:46
6

This worked ok for me:

WindowManager.LayoutParams lp = new WindowManager.LayoutParams();
lp.copyFrom(dialog.getWindow().getAttributes());
lp.width = WindowManager.LayoutParams.WRAP_CONTENT;
lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
dialog.show();
dialog.getWindow().setAttributes(lp);
Suragch
  • 484,302
  • 314
  • 1,365
  • 1,393
Rony Tesler
  • 1,207
  • 15
  • 25
4

I found one issue with B T's answer. Dialog has left alligned (not at center of screen). To fix this I added changing gravity of parent layouts. See updated forceWrapContent() method.

protected void forceWrapContent(View v) {
    // Start with the provided view
    View current = v;

    // Travel up the tree until fail, modifying the LayoutParams
    do {
        // Get the parent
        ViewParent parent = current.getParent();

        // Check if the parent exists
        if (parent != null) {
            // Get the view
            try {
                current = (View) parent;
                ViewGroup.LayoutParams layoutParams = current.getLayoutParams();
                if (layoutParams instanceof FrameLayout.LayoutParams) {
                    ((FrameLayout.LayoutParams) layoutParams).
                            gravity = Gravity.CENTER_HORIZONTAL;
                } else if (layoutParams instanceof WindowManager.LayoutParams) {
                    ((WindowManager.LayoutParams) layoutParams).
                            gravity = Gravity.CENTER_HORIZONTAL;
                }
            } catch (ClassCastException e) {
                // This will happen when at the top view, it cannot be cast to a View
                break;
            }

            // Modify the layout
            current.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
        }
    } while (current.getParent() != null);

    // Request a layout to be re-done
    current.requestLayout();
}
Community
  • 1
  • 1
Yuriy
  • 1,466
  • 2
  • 14
  • 30
3

Dialog should be wrap content

You can make an parent layout with match_parent with transparent background and with gravity center. and put your main layout under it. so it will look like center positioned dialog.

By this method you can use Scrollview, RecyclerView and any type of layout in dialog.

public void showCustomDialog(Context context) {
    Dialog dialog = new Dialog(context);
    dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
    LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    view = inflater.inflate(R.layout.dialog_layout, null, false); 
    findByIds(view);  /*find your views by ids*/
    ((Activity) context).getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
    dialog.setContentView(view);
    final Window window = dialog.getWindow();
    window.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT);
    window.setBackgroundDrawableResource(R.color.colorTransparent);
    window.setGravity(Gravity.CENTER);
    dialog.show();
}
Khemraj Sharma
  • 57,232
  • 27
  • 203
  • 212
0

Use setStyle(STYLE_NORMAL, android.R.style.Theme_Holo_Light_Dialog);

Mickey Tin
  • 3,408
  • 10
  • 42
  • 71
-1

just use AppCompatDialog

import android.content.Context;
import android.os.Bundle;
import android.support.v7.app.AppCompatDialog;
import android.view.Window;
import android.widget.ProgressBar;

public class ProgressDialogCompat extends AppCompatDialog {

    public ProgressDialogCompat(Context context) {
        super(context);
        supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Context context = getContext();
        int padding = context.getResources().getDimensionPixelSize(R.dimen.x_medium);
        ProgressBar progressBar = new ProgressBar(context);
        progressBar.setPadding(padding, padding, padding, padding);
        setContentView(progressBar);
        setCancelable(false);
        super.onCreate(savedInstanceState);
    }
}
zlxrx
  • 139
  • 1
  • 6
  • Unfortunately, much of this is proprietary code and doesn't really explain which part of it is actually intended to resolve the issue. – Abandoned Cart May 06 '19 at 02:47