How to properly implement ReorderableList expanding/collapsing in CustomPropertyDrawer?

Hey guys,

I’m still messing around with custom PropertyDrawers and Inspectors in order to make it a bit more comfortable for me to set stuff in the editor for my game. With your help I was already able to make some progress but I’m stuck on a small bit. I mananged to get my nested Reorderable List working but I cannot get the expand/collapse behavior right. Expanded, everything looks quite nice:

But when I collapse things, the elements of the lists are still kind of displayed and overlapping, or at least the list background is still being displayed. Also when I expand again after this somewhat screwed up state, the lists are not properly displayed in expanded mode and I have to for instance drag them around a bit to force them to look right again. So, possibly I need to do something to force a redraw or something like that, not sure. Here’s what it looks like after collapsing/expanding a bit:

The stuff handling property height and list height seems logical to me as I have implemented it but apparently I’m missing something. Here’s my code for the CustomPropertyDrawer:

[CustomPropertyDrawer(typeof(DestructionPhaseDefinition))]
public class DestructionPhaseDefinitionDrawer : PropertyDrawer
{
    private Dictionary<string, ReorderableList> lists = new Dictionary<string, ReorderableList>();

    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        SerializedProperty prop = property.FindPropertyRelative("destructionLoot");

        if (prop != null && prop.isExpanded)
        {
            
            if (prop.isExpanded)
            {
                return GetList(property, prop).GetHeight();

            } else
            {
                return EditorGUIUtility.singleLineHeight;
            }
        }

        return base.GetPropertyHeight(property, label);
    }

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.BeginProperty(position, label, property);

        int indent = EditorGUI.indentLevel;
        EditorGUI.indentLevel = 0;

        ReorderableList rList = GetList(property, property.FindPropertyRelative("destructionLoot"));
        rList.DoList(position);
        
        EditorGUI.indentLevel = indent;

        property.serializedObject.ApplyModifiedProperties();

        EditorGUI.EndProperty();
    }

    private ReorderableList GetList(SerializedProperty property, SerializedProperty listProperty)
    {
        if(lists.ContainsKey(property.propertyPath))
        {
            return lists[property.propertyPath];
        } else
        {
            ReorderableList list = new ReorderableList(property.serializedObject, listProperty, true, true, true, true);

            list.elementHeight = EditorGUIUtility.singleLineHeight;

            list.drawHeaderCallback = rect =>
                {
                    var newRect = new Rect(rect.x + 10, rect.y, rect.width - 10, rect.height);
                    listProperty.isExpanded = EditorGUI.Foldout(newRect, listProperty.isExpanded, listProperty.displayName, true);
                };

            list.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) => 
                {
                    if(listProperty.isExpanded)
                    {
                        EditorGUI.indentLevel = 1;
                        EditorGUI.PropertyField(rect, listProperty.GetArrayElementAtIndex(index), GUIContent.none);
                    }
                };
            
            list.elementHeightCallback = (int indexer) => 
                {
                    if (!listProperty.isExpanded)
                    {
                        return 0;
                    }
                    else
                    {
                        return list.elementHeight;
                    }
                };
            
            lists.Add(property.propertyPath, list);

            return lists[property.propertyPath];
        }
    }
}

I hope you guys can spot the mistake and give me some pointers, I’d greatly appreciate the help…can’t say I’m enjoying this part of Unity much. Thanks!

PS: I’m using Unity 2020.3.18f1

Hi there! This problem is quite… usual by now. Unfortunately, I didn’t find a “clean” solution for this, but there’s a simple workaround.

Force Redraw

First, about redrawing an inspector view. You can call Repain() to redraw an editor as long as you’re in an Editor or EditorWindow class. But if you need to redraw the view in a PropertyDrawer, your only option is to use EditorUtility.SetDirty :

public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
    EditorUtility.SetDirty(property.serializedObject.targetObject);
}

This will tell Unity that the object may have changed, so its inspector view will be redrawn.

The case of ReorderableList

In the case of ReorderableList, just redrawing the inspector may not be enough. A weird behavior is that the sizing is not good in the inspector, until you just resize it by dragging the edge of its window.

The only way I found is to “kill” the list and regenerate it completely. My advice is to use a getter to create and draw the list, so if it doesn’t exist, it re-create it.

Example

The following example illustrates the explanation above. Note that I grouped the asset and its editor in a file for convenience, but since it uses editor features, it won’t build.

using UnityEngine;
using UnityEditor;
using UnityEditorInternal;

// You can go to Assets > Create > Demo Asset to create an asset of this type, and so check the inspector view.
[CreateAssetMenu(fileName = "NewDemoAsset", menuName = "Demo Asset")]
public class DemoAsset : ScriptableObject
{
    public string[] playerNames = { };
}

// This part should be in a separate file. I grouped the asset and its editor for convenience here.
[CustomEditor(typeof(DemoAsset))]
public class DemoAssetEditor : Editor
{
    [SerializeField]
    private int _selectedItem = -1;

    private ReorderableList _playerNamesList = null;

    public override void OnInspectorGUI()
    {
        // Use the getter, not the field
        PlayerNamesList.DoLayoutList();
    }

    private ReorderableList PlayerNamesList
    {
        get
        {
            // Create the reorderable list if it doesn't exist
            if (_playerNamesList == null)
            {
                _playerNamesList = new ReorderableList(serializedObject, serializedObject.FindProperty("playerNames"));

                _playerNamesList.drawHeaderCallback += rect => EditorGUI.LabelField(rect, "Player Names", EditorStyles.boldLabel);
                
                // When an item is selected, update the folded out item index, and kill the list so it can be redrawn properly
                _playerNamesList.onSelectCallback += list =>
                {
                    _selectedItem = list.index;
                    //_playerNamesList = null; // Toggle this line to see how the editor behaves by default without killing the list
                };

                // Set the element height to 200px when selected, or default height otherwise
                _playerNamesList.elementHeightCallback += index => _selectedItem == index ? 200f : EditorGUIUtility.singleLineHeight;

                _playerNamesList.drawElementCallback += (rect, index, isActive, isFocused) =>
                {
                    if (_selectedItem == index)
                    {
                        Rect tmpRect = new Rect(rect);
                        tmpRect.height = EditorGUIUtility.singleLineHeight;
                        EditorGUI.Foldout(tmpRect, true, "Element " + index);

                        tmpRect.y += tmpRect.height + 2f;
                        tmpRect.height = rect.height - tmpRect.height - 2f;
                        EditorGUI.DrawRect(tmpRect, Color.black);
                    }
                    else
                        EditorGUI.Foldout(rect, false, "Element " + index);
                };
            }
            return _playerNamesList;
        }
    }
}