しっぽを追いかけて

ぐるぐるしながら考えています

Unity と猫の話題が中心   掲載内容は個人の私見であり、所属組織の見解ではありません

Unity で UI のドラッグ中にあらかじめ挿入先スペースを明示する

※ これは 2020/11/20 時点の Unity 2020.1.14f1 の情報です

最新版では動作が異なる可能性がありますのでご注意ください

前回の UI のドラッグ移動の続き

2つのドロップ位置によって挿入位置が入れ替わるようにしたが、どのあたりに挿入されるかわかりにくいので、ドラッグ中にもプレースホルダーとして挿入位置にスペースが入るようにしたい

プレースホルダ

やったことはコード変更のみ

using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

[RequireComponent(typeof(CanvasGroup))]
public class Draggable : MonoBehaviour
{
    private Transform root;
    private Transform area;
    private Transform self;
    private CanvasGroup canvasGroup = null;
    private GameObject placeholder = null;

    public void Awake()
    {
        this.self = this.transform;
        this.area = this.self.parent;
        this.root = this.area.parent;
        this.canvasGroup = this.GetComponent<CanvasGroup>();
    }

    public void OnBeginDrag(BaseEventData eventData)
    {
        // プレースホルダー生成
        this.placeholder = new GameObject();

        var element = this.placeholder.AddComponent<LayoutElement>();
        element.preferredWidth = 100f;
        element.preferredHeight = 100f;
        
        // 元の場所にプレースホルダーを残す
        this.placeholder.transform.SetParent(this.area);
        this.placeholder.transform.SetSiblingIndex(this.self.GetSiblingIndex());

        // ドラッグできるよういったん DropArea の上位に移動する
        this.self.SetParent(this.root);

        // UI 機能を一時的無効化
        this.canvasGroup.blocksRaycasts = false;
    }

    public void OnDrag(BaseEventData eventData)
    {
        this.self.localPosition = GetLocalPosition(((PointerEventData)eventData).position, this.transform);

        AddToArea(this.placeholder.transform, this.self.position.x, (PointerEventData)eventData);
    }

    private static Vector3 GetLocalPosition(Vector3 position, Transform transform)
    {
        // 画面上の座標 (Screen Point) を RectTransform 上のローカル座標に変換
        RectTransformUtility.ScreenPointToLocalPointInRectangle(
            transform.parent.GetComponent<RectTransform>(),
            position,
            Camera.main,
            out var result);
        return new Vector3(result.x, result.y, 0);
    }

    private static void AddToArea(Transform item, float x, PointerEventData eventData)
    {
        // 座標位置に DropArea があったら挿入対象にする
        var dropArea = GetRaycastArea(eventData);
        if (dropArea == null)
        {
            return;
        }

        // 挿入位置を x 座標から計算
        var area = dropArea.transform;
        var index = area.childCount;
        for (var i = 0; i < area.childCount; i++)
        {
            if (x < area.GetChild(i).position.x)
            {
                index = i;
                if (item.GetSiblingIndex() < index)
                    index--;

                break;
            }
        }

        item.SetParent(area);
        item.SetSiblingIndex(index);
    }

    public void OnEndDrag(BaseEventData eventData)
    {
        // プレースホルダーを破棄
        Destroy(this.placeholder);

        // DropArea 移動
        AddToArea(this.self, this.self.position.x, (PointerEventData)eventData);
        this.area = this.self.parent;

        // UI 機能を復元
        this.canvasGroup.blocksRaycasts = true;
    }

    /// <summary>
    /// イベント発生地点の DropArea を取得する
    /// </summary>
    /// <param name="eventData">イベントデータ</param>
    /// <returns>DropArea</returns>
    private static DropArea GetRaycastArea(PointerEventData eventData)
    {
        var results = new List<RaycastResult>();
        EventSystem.current.RaycastAll(eventData, results);

        return results.Select(x => x.gameObject.GetComponent<DropArea>())
            .FirstOrDefault(x => x != null);
    }
}

ドラッグ開始時に UI と同じサイズになるよう LayoutElement を張り付けた GameObject を生成して、挿入予想位置に差し込むようにして、ドラッグ終了時にプレースホルダーは破棄して UI と入れ替えるだけ

お試し

プレースホルダーによる挿入位置明示

モーセの葦の海の奇跡のように、挿入位置がパカッと割れました!