7
3

More than 1 year has passed since last update.

Unityでスクロールビュー(ScrollRect)上に配置したUIオブジェクトを長押しでドラッグさせる

Last updated at Posted at 2023-07-27

スクロールビュー上に配置したUIオブジェクトをドラッグしたいと思ったことありませんでしょうか?
たとえば、リスト上にあるアイテムを削除するためにゴミ箱アイコンの上までドラッグするだったり、アイテムをビュー間で移動させるなどゲームで考えられるシチュエーションはあると思います。

これを実現するために、スクロールビュー上に配置したUIオブジェクトに対してIBeginDragHandler,IDragHandler,IEndDragHandlerを実装するとスクロールビューを構成するScrollRectがスクロールできないという問題に直面してしまいます。
実はこの問題に頭を悩ます人が多いのではないかと勝手に推測しています。
これをどうにかしようというのが今回の内容となります。

参考画像

今回の内容で、どういうことが実現できるのかわかりやすいように画像を用意しました。
スクロールビューを上下にスクロールできることを確認しつつ、4番と6番のパネルを長押ししてドラッグを行っています。
scroll.gif

ScrollRectがスクロールできなくなる理由

UnityのEventSystem(InputModule)の実装にあります。
StandaloneInputModule.csを確認したところ、マウスダウンやタッチした際に、UIオブジェクトに対してレイキャスト(当たり判定)がIPointerHandler系とIDragHandler系に分かれて判定されます。
マウスダウン、タッチされた座標にあるScrollRectの上にIBeginDragHandlerインターフェイスを持ったUIオブジェクトがある場合は、当たり判定としてScrollRectより手前にあるUIオブジェクトが優先されます。
IBeginDragHandlerインターフェイスがなければ、UIオブジェクトにはIPointerDownHandlerが呼び出され、ScrollRectにはドラッグのしきい値を超えればIBeginDragHandlerの呼び出しが行われてスクロールが可能ということになるのです。

それぞれのハンドラごとに当たり判定が行われることから、ScrollRectより手前にIBeginDragHandlerインターフェイスを持ったオブジェクトが存在してればScrollRectが当たり判定にヒットせずスクロールができなくなるということになります。

おそらくですが、スクロールビューが他のUIに干渉せずスクロールできるようにこのような設計になっているのだと思います。

ドラッグもスクロールもしたい

何かしらの条件で、IBeginDragHandlerの対象を切り替える必要があります。
実装する画面仕様やレイアウトにもよるので必ずしも使えるテクニックとは限りませんが、私は今回の記事のタイトルにもあるように、ScrollRect上のUIオブジェクトを長押しすることでドラッグを実現させました。

やり方としては、ScrollRect上のUIオブジェクトに対してIBeginDragHandler,IDragHandler,IEndDragHandlerを実装して、長押し判定となる条件までにドラッグが行われれば、ScrollRectにDragHandlerが飛ぶようにしてスクロールを実現します。

ドラッグ対象を切り替える方法

今回ご紹介するメインの部分となります。
ScrollRect上のUIオブジェクトがドラッグイベントを受けて、長押し判定前にドラッグイベントが発生すればScrollRectにドラッグ対象を入れ替えます。

ドラッグ対象を入れ替えるにはPointerEventDataの内容を書き換えることで実現可能です。
※Unity 2022.3.4f1にて動作確認しています

PointerEventDataの書き換え
//選択されているオブジェクトをScrollRectへ変更する
var scrollRect = GetComponentInParent<ScrollRect>();
eventData.pointerDrag = scrollRect.gameObject;
EventSystem.current.SetSelectedGameObject(scrollRect.gameObject);

//ドラッグの初期化
scrollRect.OnInitializePotentialDrag(eventData);
//次のフレームからIdragHandlerの呼び出しが始まるのでBeginさせる
scrollRect.OnBeginDrag(eventData);

ソースコードの説明としては、ドラッグ対象だったUIオブジェクトのIDragHandler内でPointerEventDataの中身を親のScrollRectが選択された状態へ書き換えています。
UnityのEventSystemが保持するドラッグ対象がScrollRectになるので、次フレームよりScrollRectに対してIDragHandlerが送られることになります。

せっかくなので長押しも

長押しの実現は色々ありますが、私は以下のサイトを参考にさせていただきました。
UniRXで簡潔に記述されており、説明もあるのでわかりやすいです。

コード全体

以下が今回ScrollRect上に配置したUIオブジェクトを長押しでドラッグできるコードとなります。
このスクリプトをスクロールビューのドラッグしたいUIオブジェクトにAddComponentすることで長押しドラッグできるようになります。
注意点として、ドラッグする最低限なコードにしかなっていないのでドラッグ後のロジックやエラー処理など必要に応じて追加が必要な点と、インタラクティブなUIがドラッグ対象のUIの上に乗っている場合はインタラクティブなUI上での長押しなど検討する必要がある点にご注意ください。

また、CanvasのRenderModeはScreen Space-Cameraを想定しています。

DragObject.cs
using System;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using UniRx;
using UniRx.Triggers;

public class DragObject : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler, IPointerDownHandler, IPointerUpHandler
{
    /// 自分がドラッグ対象か?
    private bool isSelfDrag = false;

    /// しきい値計算に使用するダウン時の座標
    private Vector2 downPosition;

    /// オブジェクトをドラッグしたときの表示位置オフセット
    private Vector2 offset;

    /// ScrollRectへドラッグを切り替えるしきい値
    [SerializeField]
    private float changeScrollDragThreshold = 10.0f;

    /// 自身のRectTransform
    private RectTransform rectTransform = null;

    /// スクロール対象となるScrollRect(基本的に親)
    private ScrollRect scrollRect = null;

    /// 所属するキャンバス
    private Canvas canvas = null;

    /// ドラッグ中のオフセット計算に必要なCanvasのRectTransform
    private RectTransform canvasRectTransform = null;

    /// ドラッグ開始処理
    public void OnBeginDrag(PointerEventData eventData)
    {
    }

    /// ドラッグ中処理(位置変更もしくはドラッグかスクロールかの切り替え)
    public void OnDrag(PointerEventData eventData)
    {
        if(isSelfDrag)
        {
            //自分がドラッグ中なら位置を更新
            gameObject.transform.position = eventData.position - offset;
            return;
        }

        //長押し判定が行われる前に指定量動けばスクロールとみなす
        if (Mathf.Abs(downPosition.x - eventData.position.x) >= changeScrollDragThreshold ||
            Mathf.Abs(downPosition.y - eventData.position.y) >= changeScrollDragThreshold)
        {
            //選択されているオブジェクトをScrollRectへ変更する            
            eventData.pointerDrag = scrollRect.gameObject;
            EventSystem.current.SetSelectedGameObject(scrollRect.gameObject);

            //ドラッグの初期化
            scrollRect.OnInitializePotentialDrag(eventData);
            //次のフレームからIdragHandlerの呼び出しが始まるのでBeginさせる
            scrollRect.OnBeginDrag(eventData);
        }
    }

    /// ドラッグ終了
    public void OnEndDrag(PointerEventData eventData)
    {
    }

    /// ポインターダウン処理
    public void OnPointerDown(PointerEventData eventData)
    {
        isSelfDrag = false;
        downPosition = eventData.position;
    }

    /// 長押し
    private void OnLongClick(Unit unit)
    {
        //長押し判定後は自分がドラッグ対象とする
        isSelfDrag = true;

        //ドラッグ準備
        this.gameObject.transform.SetParent(canvas.transform);

        RectTransformUtility.ScreenPointToLocalPointInRectangle(
            rectTransform,
            downPosition,
            canvas.worldCamera,
            out offset);
    }

    /// ポインターアップ処理
    public void OnPointerUp(PointerEventData eventData)
    {
    }

    // Start is called before the first frame update
    void Start()
    {
        //GetComponentを必要以上に呼ばないよう事前にキャッシュする
        rectTransform = GetComponent<RectTransform>();
        scrollRect = GetComponentInParent<ScrollRect>();
        canvas = GetComponentInParent<Canvas>();
        canvasRectTransform = canvas.GetComponent<RectTransform>();

        //長押し判定(0.5秒)
        var eventTrigger = this.gameObject.AddComponent<ObservableEventTrigger>();

        //以下を参考
        // https://zenn.dev/nekomimi_daimao/articles/932e1f20ffca04 
        eventTrigger.OnPointerDownAsObservable().Select(_ => true)
            .Merge(eventTrigger.OnPointerUpAsObservable().Select(_ => false))
            .Throttle(TimeSpan.FromSeconds(0.5))
            .Where(b => b)
            .AsUnitObservable()
            .TakeUntilDestroy(this)
            .Subscribe(OnLongClick);

    }

}

まとめ

UnityのEventSystem周りを簡単に説明しつつ、スクロールビュー上のUIオブジェクトを長押しでドラッグさせる方法を紹介しました。
・当たり判定はIPointerDownHandlerIBeginDragHandlerを持ったオブジェクト別々になる
・ドラッグ対象のオブジェクトは切り替えれる
この2点が今回重要なポイントでした。

これがなにかの参考になれば幸いです。

7
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3