うにてぃブログ

主にUnityとC#に関する記事を書いていきます

【Unity】Button を押した際に 縮小拡大のリアクションを追加した際の問題点

現象

ボタンを押した際に 縮小->拡大のリアクションをつけた場合
押した場所が縮小した際にボタンから出てしまうと、下動画のようにピクピクしてしまう

f:id:hacchi_man:20201124002330g:plain

上記の動作をするスクリプトは以下になる

public class ButtonTest : Button
{
    //  縮小させる
    private void Press()

    // もとに戻す
    private void Release()

    public override void OnPointerDown(PointerEventData eventData)
    {
        Press();
        base.OnPointerDown(eventData);
    }

    public override void OnPointerUp(PointerEventData eventData)
    {
        Release();
        base.OnPointerUp(eventData);
    }

    public override void OnPointerEnter(PointerEventData eventData)
    {
        if (IsPressed())
            Press();
        base.OnPointerEnter(eventData);
    }

    public override void OnPointerExit(PointerEventData eventData)
    {
        if (IsPressed())
            Release();
        base.OnPointerExit(eventData);
    }
}

原因

これが発生する原因としては、ボタンが縮小することにより、OnPointerExit が呼ばれてしまい
サイズがもとに戻る、その際に OnPointerEnter が呼ばれてを繰り返していることが原因になる

解決策

押したタイミングでボタンの Rect を取得し、OnPointerExit 時に Rect の範囲内かどうかの判定を行う

   private Rect _pressRect;
 
    public override void OnPointerDown(PointerEventData eventData)
    {
        Press();
        // ボタンの Rect を取得する
        _pressRect = GetRect(_rect, eventData);
        base.OnPointerDown(eventData);
    }
 
    public override void OnPointerExit(PointerEventData eventData)
    {
        // 押したタイミングの Rect範囲か確認
        if (!_pressRect.Contains(eventData.position) && IsPressed())
            Release();
        base.OnPointerExit(eventData);
    }

しかしながら、まだ問題があり、OnPointerExit は何度も呼ばれないため
押したあとに元あった範囲外に出たとしても Release が呼ばれずに縮小した状態になってしまいます

f:id:hacchi_man:20201124002932g:plain

そのために、OnDrag を追加し、押している最中の挙動を制御します ※Unity 2020.1.10f では OnDrag が呼ばれましたが旧Versionの場合呼ばれないことがありました

   public void OnDrag(PointerEventData eventData)
    {
        if (!IsPressed())
            return;
 
        if (_pressRect.Contains(eventData.position))
            Press();
        else
            Release();
    }

これにより範囲外から戻った際にも縮小するように対応できた

f:id:hacchi_man:20201124003548g:plain

最後にソースコードを貼っておくので、コードが見たい方はどうぞ

using DG.Tweening;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
 
public class ButtonTest : Button, IDragHandler
{
    private Tweener _tweener;
    private RectTransform _rect;
 
    protected override void Awake()
    {
        base.Awake();
        _rect = transform as RectTransform;
        onClick.AddListener(() => Debug.Log("Click"));
    }
 
    private void Tween(Vector3 to, string id)
    {
        if (_tweener != null && _tweener.stringId == id)
            return;

        _tweener?.Kill();
        _tweener = _rect.DOScale(to, 0.1f)
                .SetId(id)
            ;
    }
 
    private void Press()
    {
        Tween(Vector3.one * 0.7f, "press");
    }
 
    private void Release()
    {
        Tween(Vector3.one, "release");
    }
 
    private Rect _pressRect;
 
    public override void OnPointerDown(PointerEventData eventData)
    {
        Press();
        _pressRect = GetRect(_rect, eventData);
        base.OnPointerDown(eventData);
    }
 
    public override void OnPointerUp(PointerEventData eventData)
    {
        Release();
        base.OnPointerUp(eventData);
    }
 
    public override void OnPointerEnter(PointerEventData eventData)
    {
        if (IsPressed())
            Press();
        base.OnPointerEnter(eventData);
    }
 
    public override void OnPointerExit(PointerEventData eventData)
    {
        if (!_pressRect.Contains(eventData.position) && IsPressed())
            Release();
        base.OnPointerExit(eventData);
    }
 
    public void OnDrag(PointerEventData eventData)
    {
        if (!IsPressed())
            return;
 
        if (_pressRect.Contains(eventData.position))
            Press();
        else
            Release();
    }
 
    private static readonly Vector3[] cache = new Vector3[4];
 
    private static Rect GetRect(RectTransform rect, PointerEventData data)
    {
        rect.GetWorldCorners(cache);
 
        // Canvas が overlay の対応
        if (data.enterEventCamera != null)
        {
            // Screen座標 に変換 (左下, 左上, 右下, 右上)
            for (var i = 0; i < cache.Length; i++)
                cache[i] = RectTransformUtility.WorldToScreenPoint(data.enterEventCamera, cache[i]);
        }
 
        return new Rect(cache[0].x, cache[0].y, cache[2].x - cache[0].x, cache[2].y - cache[0].y);
    }
}