AlertDialog with custom view: Resize to wrap the view's content
android, android-alertdialog, android-dialogfragment, android-layout, java
Solution
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:
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 `AlertDialog`s, 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:
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).
Problem
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: - DialogFragment.getDialog returns null - How to control the width and height of the default Alert Dialog in Android? - Size of Alert Dialog or Custom Alert Dialog I have deduced the following two coding approaches: - Get the `WindowManager.LayoutParams` of the `Dialog` and override them using `myDialog.getDialog().getWindow().get/setAttributes()` - 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: 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):