スクロールビュー上に配置したUIオブジェクトをドラッグしたいと思ったことありませんでしょうか?
たとえば、リスト上にあるアイテムを削除するためにゴミ箱アイコンの上までドラッグするだったり、アイテムをビュー間で移動させるなどゲームで考えられるシチュエーションはあると思います。
これを実現するために、スクロールビュー上に配置したUIオブジェクトに対してIBeginDragHandler
,IDragHandler
,IEndDragHandler
を実装するとスクロールビューを構成するScrollRect
がスクロールできないという問題に直面してしまいます。
実はこの問題に頭を悩ます人が多いのではないかと勝手に推測しています。
これをどうにかしようというのが今回の内容となります。
参考画像
今回の内容で、どういうことが実現できるのかわかりやすいように画像を用意しました。
スクロールビューを上下にスクロールできることを確認しつつ、4番と6番のパネルを長押ししてドラッグを行っています。
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にて動作確認しています
//選択されているオブジェクトを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
を想定しています。
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オブジェクトを長押しでドラッグさせる方法を紹介しました。
・当たり判定はIPointerDownHandler
とIBeginDragHandler
を持ったオブジェクト別々になる
・ドラッグ対象のオブジェクトは切り替えれる
この2点が今回重要なポイントでした。
これがなにかの参考になれば幸いです。