【Unity】カレンダーのフリックScrollを考える

2021/02/26タイトル変更・一部修正


体重管理アプリが完成して、
嫁にアプリを使って貰っているのですが…

:今時フリックでめくれないの?

カレンダーの月切り替えがボタン仕様なので、
こんな会話になりました。

ありがたい嫁クレームと言う事で、
カレンダーのフリック切り替えを考えてみたいと思います。(T-T) グスッ

ただ注意点として、
カレンダーが表示だけの仕様なので、
日付毎に入力ボタンなどを実装する場合には、
使えないって事をあらかじめお伝えしておきます。



まず、フリックでカレンダーを切り替えるにあたり
二つの事を考えました。

・常時フリック入力を監視するのか?
・月表示だけを切り替えるのか?

常時フリック監視は、Update内で出来るのですが、
ボタンなどの操作を考えると、あまり宜しくない状態になるので、
カレンダー上をフリックした時だけ反応するようにしたい。

月の切り替えは、表示だけ変更するより、
横から前後の月がスクロールしてくる方が見た目が良い。

こんな感じで処理を考える事に…


まず、フリック操作になるんですが、
カレンダーの枠ImageにEventtriggerを仕込んで、
タッチ操作に反応できるようにすればOKなんですが…

前後月も同時にスライドする必要があるので、
単純に表示月の枠を移動させるだけでは動かないなと…

それに前後月も用意する必要があります。

後、カレンダーなんで無限にスライドし続けます。
全てのカレンダー枠を用意するなんて不可能なんで、
この辺りも踏まえて設定していきます。

まず、UIの設定なんですが
カレンダー枠を切り替えを含めて3枚用意します。
Fカレンダー①.jpg
分かりにくいので色を付けています。
このカラーパネルがカレンダー枠だと思って下さい。
サイズは、横720×縦1000にしています。
ちなみに横サイズは画面の横サイズと同じにしています。

枠を3枚用意したら枠ペアレントにまとめておきます。

各枠は、移動させるのでRigidbody2DとBoxCollider2Dをアタッチしておきます。
Colliderサイズは、100×100くらいでいいかと思います。
Fカレンダー③.jpg
追加したRigidbodyは、Transformを操作するのでキネマティックに設定しておきます。
この設定は必ず行って下さい。

ちなみにキネマティックにしないと落ちていきます(^ё^) ♪♪

操作時に、この3枚のパネルを同時に移動させる必要があるので、
移動用のImageを用意して、タッチパネルとでもリネームしておきます。
色は透明です。
Fカレンダー②.jpg
サイズは3枚の枠が収まるようにしておけばOKだと思います。

このパネルをタッチした時に動作が必要になるので、
各Eventtriggerを設定しておきます。
Fカレンダー④.jpg
・PointerDown
・Drag
・Drop

三つのEventを用意します。

ここまで準備ができたらスクリプトを組みます。
DragManager

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DragManager : MonoBehaviour
{
[Header("各枠")]
public GameObject[] frameObj;


// Start is called before the first frame update
void Start()
{

}

// Update is called once per frame
void Update()
{

}

//ドラッグ時の処理
public void OnMouseDrag()
{
frameObj[0].transform.SetParent(this.transform, false);
frameObj[1].transform.SetParent(this.transform, false);
frameObj[2].transform.SetParent(this.transform, false);
}
}

スクリプトをタッチパネルにアタッチして、
設定した変数に各枠をアタッチします。
このメソッドをEventtriggerのDragに割り当てます。

これでドラッグ時に各枠がタッチパネルに移動するようになります。

続いて、タッチパネルの移動になるんですが、
オブジェクトをドラッグする際、必ずオブジェクトの中心点を
掴む事になります。

画面の端でドラッグするとオブジェクトの中心が、
そのポイントに移動するので、枠がワープしたみたいになります。

なので、ドラッグしたポイントとオブジェクトの中心のオフセットを出す必要があります。

オフセットの出し方を調べた所、
Unityでオブジェクトをドラッグ&ドロップにあわせて動かす(2D)
情報を公開されている方、この場を借りてお礼申し上げます。m(_ _)m

このスクリプトを参考にオフセットを設定してみたいと思います。
DragManager

//ドラッグの各変数
private Vector3 offset; //タッチ位置とのオフセット
private float axisY = 0f; //タップ時のY座標
private const float axisZ = 10f; //ドラッグ時のZ座標

//タップ時のオフセット取得
public void OnMouseDown()
{
axisY = transform.position.y;
this.offset = transform.position - Camera.main.ScreenToWorldPoint(Input.mousePosition);
}

//ドラッグ時の処理
public void OnMouseDrag()
{
frameObj[0].transform.SetParent(this.transform, false);
frameObj[1].transform.SetParent(this.transform, false);
frameObj[2].transform.SetParent(this.transform, false);

Vector3 currentScreenPoint = Input.mousePosition;
Vector3 currentPosition = Camera.main.ScreenToWorldPoint(currentScreenPoint) + this.offset;
currentPosition.y = axisY;
currentPosition.z = axisZ;
transform.position = currentPosition;
}

追加したOnMouseDownメソッドをEventtriggerのPointerDownに割り当てます。

内容の説明ですが、
タッチパネルをタップすると同時にマウスポイント座標とタッチパネル座標の
差を求めます。これがオフセット値となります。

今回の移動処理は、X座標のみを操作するので、Y座標は固定になります。
タップ時にY座標を取得しておいて、最後に代入すればY座標は変化しません。

続いて、ドラッグ時にOnMouseDownで取得したオフセット値を元に
補正すればドラッグポイントに連動した動きになってくれます。

Z軸についてですが、
恐らくWorld座標で設定したオブジェクトなら
Z軸を取得して再代入してやれば問題ないのでしょうが、
Canvasに設定しているオブジェクトの場合、ドラッグと同時に消えてしまいます。

なので、Z軸に10fの数値を代入して消えないようにしています。
この辺りはローカル座標の制約を受けてるのではないかなと思います。

これでタッチパネルをドラッグ操作できるようになったので、
枠の位置決めをします。

枠の位置決めは、TriggerEnterを使って行います。
Fカレンダー⑤.jpg
センサーになるオブジェクトを用意して(空のオブジェクト)
BoxCollider2Dをアタッチして、センサーペアレントにまとめておきます。


枠サイズを横720にしているので、センサーの設置位置は、
720づらした位置に5個用意します。

左から順に
・左外
・C1
・C2
・C3
・右外

にリネームします。

アタッチしたBoxColliderは、
Fカレンダー⑥.jpg
トリガーにするをチェックしておいきます。

このセンサーに接触させて、枠の位置指定とペアレント指定をします。
各枠に判断させる為のスクリプトを用意します。
FrameManager

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

public class FrameManager : MonoBehaviour
{
[Header("枠ペアレント")]
public GameObject frameParent;

//タグネーム一覧
private const string rightOut = "RightOut";
private const string leftOut = "LeftOut";
private const string c1 = "C1";
private const string c2 = "C2";
private const string c3 = "C3";

//座標データ
Vector3 c1Axis = new Vector3(-720f, 0f, 0f);
Vector3 c2Axis = new Vector3(0f, 0f, 0f);
Vector3 c3Axis = new Vector3(720f, 0f, 0f);

// Start is called before the first frame update
void Start()
{

}

// Update is called once per frame
void Update()
{

}

//何かに接触したら
private void OnTriggerEnter2D(Collider2D collision)
{
transform.SetParent(frameParent.transform, false);


if (collision.tag == rightOut || collision.tag == c1)
{
this.transform.localPosition = c1Axis;
}

if (collision.tag == leftOut || collision.tag == c3)
{
this.transform.localPosition = c3Axis;
}

if (collision.tag == c2)
{
this.transform.localPosition = c2Axis;
}
}
}

説明すると、
各センサーの座標を設定しておいて、
接触したセンサー位置に枠が移動するように処理しました。

外側二つのセンサーは、枠がそこに接触した場合、
逆方向に移動するようにしています。

これで3枚の枠しかなくても連続で枠があるように見せる事ができます。

TriggerEnterを用意したので、Collision用のタグを追加します。
Fカレンダー⑦.jpg
それぞれのセンサーと同じタグを設定します。
Fカレンダー⑧.jpg
左外なら”LeftOut”を選択する感じになります。

タグ設定が終わったら、3つの枠にスクリプトをアタッチします。
Fカレンダー⑨.jpg
変数に枠ペアレントをアタッチしてPLAYしてみます。

ドラッグして左右に動かすと…
ドラッグ中でもセンサーに当たると、そこで止まってしまいます。( ̄~ ̄;) ウーン

動きとしては、イマイチでっす。

できればドラッグ中は、枠が連動している方がいいので、
少し修正を加えます。
DragManager

[Header("センサーペアレント")]
public GameObject sensorParent;

//ドラッグ時の処理
public void OnMouseDrag()
{
sensorParent.SetActive(false);

   以下省略
}

//ドロップ時の処理
public void OnMouseUpAsButton()
{
sensorParent.SetActive(true);
}

これでドラッグ中はセンサーに反応しなくなります。
ドラッグの処理が出来たので、続いてドロップ時の処理を作っていきます。


ドロップの処理は、オブジェクトの状態を把握する必要があるので、
離した時の状態を考えてみます。

・枠が指定位置に収まっている
・タッチパネルが原点に戻っている

枠が指定位置に収まるには、センサーに掛らないと移動しないので、
ドロップの際に必ず、センサーに掛るようにする必要があります。

実際には、センサーに掛るような操作はしないので、
センサーから外れる事も考えられます。
なので、強制的にセンサーに掛る位置に誘導する必要があります。

それとタッチパネルは、ドロップ時に原点に戻してやる必要があります。
ドロップした位置に置いておくと片側に送っていくと最終的に
画面外に移動していた、なんて事にでもなれば操作する事もできません。(^.^; オホホホ

そこを踏まえてドロップのスクリプトを組んでみます。
DragManager

//各座標データ
private Vector3 origin = new Vector3(0f, 0f, 0f); //原点
private Vector3 rightSide = new Vector3(720f, 0f, 0f); //右側
private Vector3 leftSide = new Vector3(-720f, 0f, 0f); //左側

//ドロップ時の処理
public void OnMouseUpAsButton()
{
//ドロップ時の処理
public void OnMouseUpAsButton()
{
sensorParent.SetActive(true);

if (transform.localPosition.x <= -200)
{
transform.localPosition = leftSide;
}
else if (transform.localPosition.x >= 200)
{
transform.localPosition = rightSide;
}
else
{
transform.localPosition = origin;
}
}

ドロップ時にタッチパネルのX座標がどこにあるかで分岐してみました。

X座標がローカル200以上なら右へ
X座標がローカル-200以下なら左へ
間ならセンターに戻すようにしました。

しかし、右や左に移動した際に戻れなくなるので、
戻す処理が必要になります。

そこで、Update内で戻ってなければ戻すように監視させます。
ただ、外れていれば戻すでは、ドラッグすらできない、枠が子に残っていると
枠まで移動するので、その辺りを工夫してみます。
DragManager

private bool originFlag = true; //タッチパネルが原点にあるか

// Update is called once per frame
void Update()
{

if (transform.childCount == 0 && originPos == false)
{
transform.localPosition = origin;
originPos = true;
}
}

//ドロップ時の処理
public void OnMouseUpAsButton()
{
sensorParent.SetActive(true);

if (transform.localPosition.x <= -200)
{
transform.localPosition = leftSide;
originFlag = false;
}
else if (transform.localPosition.x >= 200)
{
transform.localPosition = rightSide;
originFlag = false;
}
else
{
transform.localPosition = origin;
}
}

フラグを用意して、Updateから監視できるようにしました。

また、枠が入ったまま動くとマズいのでchildCount == 0を併用して
判別するようにしています。

ここまでくれば操作は問題ないので、カレンダーの月表示を作ります。

まず日付表示用のテキストを追加します。
Fカレンダー⑩.jpg
枠の上辺りに設置すればOK。

続いて、FrameManagerに追加します。
FrameManager

using System;
using UnityEngine.UI;

public class FrameManager : MonoBehaviour
{
[Header("年月表示テキスト")]
public Text dateDisp;

//日付用変数
private string thisDateStr = string.Empty; //枠の年月ストリング
private DateTime thisDate = DateTime.MinValue; //枠の年月データ

//枠名一覧
private const string frame1 = "枠1";
private const string frame3 = "枠3";

// Start is called before the first frame update
void Start()
{
thisDate = DateTime.Now.Date;

//枠番号に合わせて日付を取得
if (this.gameObject.name == frame1) thisDate = thisDate.AddMonths(-1);
else if (this.gameObject.name == frame3) thisDate = thisDate.AddMonths(1);

thisDateStr = thisDate.ToString("yyyy年M月");
}

//何かに接触したら
private void OnTriggerEnter2D(Collider2D collision)
{
transform.SetParent(frameParent.transform, false);


if (collision.tag == rightOut || collision.tag == c1)
{
this.transform.localPosition = c1Axis;

if (collision.tag == rightOut)
{
thisDate = thisDate.AddMonths(-3);
thisDateStr = thisDate.ToString("yyyy年M月");
}
}

if (collision.tag == leftOut || collision.tag == c3)
{
this.transform.localPosition = c3Axis;
if (collision.tag == leftOut)
{
thisDate = thisDate.AddMonths(3);
thisDateStr = thisDate.ToString("yyyy年M月");
}

}

if (collision.tag == c2)
{
this.transform.localPosition = c2Axis;
dateDisp.text = thisDateStr;
}
}
}

Startで起動日付を取得して、枠毎の日付を算出、
変数に保持しておきます。

TriggerEnterに接触する度に保持日付を更新しておけば、
C2センサーに重なった枠のデータを表示するだけでOKになります。
最後に年月表示テキストの変数にアタッチをして完了でっすヽ(´▽`)/~♪

この前、記事にしたカレンダーを移植して、実機にビルドします。
Android上でテストした動画をどうぞ。


無事に動いてくれています。

最後に完成版のスクリプトを
DragManager

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DragManager : MonoBehaviour
{
//パブリック
//------------------------------
[Header("各枠")]
public GameObject[] frameObj;
[Header("センサーペアレント")]
public GameObject sensorParent;



//プラベート
//------------------------------

private bool originFlag = true; //タッチパネルが原点にあるか

//ドラッグの各変数
private Vector3 offset; //タッチ位置とのオフセット
private float axisY = 0f; //タップ時のY座標
private const float axisZ = 10f; //ドラッグ時のZ座標

//各座標データ
private Vector3 origin = new Vector3(0f, 0f, 0f); //原点
private Vector3 rightSide = new Vector3(720f, 0f, 0f); //右側
private Vector3 leftSide = new Vector3(-720f, 0f, 0f); //左側


/// <summary>
/// タッチパネル制御メソッド
/// </summary>

// Start is called before the first frame update
void Start()
{

}

// Update is called once per frame
void Update()
{
//タッチパネルの原点出し
if (transform.childCount == 0 && originFlag == false)
{
transform.localPosition = origin;
originFlag = true;
}
}

//タップ時のオフセット取得
public void OnMouseDown()
{
axisY = transform.position.y;
this.offset = transform.position - Camera.main.ScreenToWorldPoint(Input.mousePosition);
}


//ドラッグ時の処理
public void OnMouseDrag()
{
sensorParent.SetActive(false);

frameObj[0].transform.SetParent(this.transform, false);
frameObj[1].transform.SetParent(this.transform, false);
frameObj[2].transform.SetParent(this.transform, false);

Vector3 currentScreenPoint = Input.mousePosition;
Vector3 currentPosition = Camera.main.ScreenToWorldPoint(currentScreenPoint) + this.offset;
currentPosition.y = axisY;
currentPosition.z = axisZ;
transform.position = currentPosition;
}

//ドロップ時の処理
public void OnMouseUpAsButton()
{
sensorParent.SetActive(true);

if (transform.localPosition.x <= -200)
{
transform.localPosition = leftSide;
originFlag = false;
}
else if (transform.localPosition.x >= 200)
{
transform.localPosition = rightSide;
originFlag = false;
}
else
{
transform.localPosition = origin;
}
}
}

FrameManagerは、カレンダーを組み込んだ物を載せておきます。
FrameManager

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

public class FrameManager : MonoBehaviour
{
//パブリック
//------------------------------

[Header("枠ペアレント")]
public GameObject frameParent;
[Header("年月表示テキスト")]
public Text dateDisp;


//プライベート
//------------------------------

//日付用変数
private string thisDateStr = string.Empty; //枠の年月ストリング
private DateTime thisDate = DateTime.MinValue; //枠の年月データ

//枠名一覧
private const string frame1 = "枠1";
private const string frame3 = "枠3";

//タグネーム一覧
private const string rightOut = "RightOut";
private const string leftOut = "LeftOut";
private const string c1 = "C1";
private const string c2 = "C2";
private const string c3 = "C3";

//座標データ
Vector3 c1Axis = new Vector3(-720f, 0f, 0f);
Vector3 c2Axis = new Vector3(0f, 0f, 0f);
Vector3 c3Axis = new Vector3(720f, 0f, 0f);


/// <summary>
/// 枠の制御メソッド
/// </summary>

// Start is called before the first frame update
void Start()
{
thisDate = DateTime.Now.Date;

//枠番号に合わせて日付を取得
if (this.gameObject.name == frame1) thisDate = thisDate.AddMonths(-1);
else if (this.gameObject.name == frame3) thisDate = thisDate.AddMonths(1);

thisDateStr = thisDate.ToString("yyyy年M月");


//日付パネル生成メソッド
CreateDayPanel();
}

// Update is called once per frame
void Update()
{

}

//何かに接触したら
private void OnTriggerEnter2D(Collider2D collision)
{
transform.SetParent(frameParent.transform, false);


if (collision.tag == rightOut || collision.tag == c1)
{
this.transform.localPosition = c1Axis;

if (collision.tag == rightOut)
{
thisDate = thisDate.AddMonths(-3);
thisDateStr = thisDate.ToString("yyyy年M月");
GetTheDate();
}
}

if (collision.tag == leftOut || collision.tag == c3)
{
this.transform.localPosition = c3Axis;
if (collision.tag == leftOut)
{
thisDate = thisDate.AddMonths(3);
thisDateStr = thisDate.ToString("yyyy年M月");
GetTheDate();
}

}

if (collision.tag == c2)
{
this.transform.localPosition = c2Axis;
dateDisp.text = thisDateStr;
}
}


///<summary>
///カレンダー内部の制御
///</summary>

//パブリック
//------------------------------
[Header("日付パネルプレハブ")]
public GameObject dayPanelPrefab;

//プライベート
//------------------------------

//日付パネル用変数
private Image[] dayPanelColor = new Image[42]; //日付パネルのイメージ
private Text[] dayText = new Text[42]; //日付テキスト

//曜日ごとの補正値を設定しておく
private enum WEEK
{
sunday = 0,
monday = -1,
tuesday = -2,
wednesday = -3,
thursday = -4,
friday = -5,
saturday = -6,
}

//カレンダーに必要な日付データ
private DateTime firstDay = DateTime.MinValue; //表示月の月初取得用
private DateTime nextMonth = DateTime.MinValue; //次月取得用
private DateTime firstPoint = DateTime.MinValue; //カレンダーの1枠目の日付取得用


//カラーコード設定
private Color green = new Color(0f, 0.4f, 0f, 1f);
private Color gray = new Color(0.9f, 0.9f, 0.9f, 1f);


/// <summary>
/// カレンダー制御メソッド
/// </summary>

//今の日付から表示月と次月の月初日を取得する
private void GetTheDate()
{
firstDay = new DateTime(thisDate.Year, thisDate.Month, 1);
nextMonth = firstDay.AddMonths(1);
SetCalendarDate();
}

//日付パネルの生成
private void CreateDayPanel()
{
for (int i = 0; i < 42; i++)
{
GameObject createPanel = (GameObject)Instantiate(dayPanelPrefab);
createPanel.transform.SetParent(transform, false);
dayPanelColor[i] = createPanel.GetComponent<Image>();
dayText[i] = createPanel.transform.GetChild(0).GetComponent<Text>();
}
GetTheDate();
}

//カレンダーの日付をセットする
private void SetCalendarDate()
{
DayOfWeek firstWeek = firstDay.DayOfWeek; //月初の曜日を取得
int diff = 0; //1枠目との差を取得する変数

//月初の曜日から1枠目との差を割出す
if (firstWeek == DayOfWeek.Sunday) diff = (int)WEEK.sunday;
if (firstWeek == DayOfWeek.Monday) diff = (int)WEEK.monday;
if (firstWeek == DayOfWeek.Tuesday) diff = (int)WEEK.tuesday;
if (firstWeek == DayOfWeek.Wednesday) diff = (int)WEEK.wednesday;
if (firstWeek == DayOfWeek.Thursday) diff = (int)WEEK.thursday;
if (firstWeek == DayOfWeek.Friday) diff = (int)WEEK.friday;
if (firstWeek == DayOfWeek.Saturday) diff = (int)WEEK.saturday;
//差分を計算する
firstPoint = firstDay.AddDays(diff);


//各日付テキストに代入
for (int i = 0; i < 42; i++)
{
DateTime temp = firstPoint.AddDays(i);

//前月・次月は表示しない、日付パネルをグレーにする
if (temp < firstDay || temp >= nextMonth)
{
dayText[i].text = null;
dayPanelColor[i].color = gray;
}
else
{
dayText[i].text = temp.Day.ToString();
dayPanelColor[i].color = Color.white;
}

//曜日で文字色を変更する
switch (temp.DayOfWeek)
{
case DayOfWeek.Sunday:
dayText[i].color = Color.red;
break;

case DayOfWeek.Saturday:
dayText[i].color = Color.blue;
break;

default:
dayText[i].color = green;
break;
}
}
}
}

ここまで出来たのですが問題点が二つ程…
ドラッグ中にタッチパネルから指やマウスが離れると
ドロップとはみなされない為、ドロップメソッドが呼び出されません。

なぜ?となりますが…
タッチパネル上で指やマウスが離れるとドロップと認識します。

しかし、ドラッグ中にタッチパネルから離れた所で指やマウスを
放すとタッチパネルが反応しません。

今回、上下動を設定していないので、
縦にフリックして、タッチパネル上に指がない場合、
ドロップを呼び出す事ができないんですよ~(T-T) グスッ

なので、ドラッグ中にタッチパネルから外れた所でドロップすると
Fカレンダー⑪.jpg
センサーにも掛からない位置なら、こんな感じで停止します。
これは放置できない部分なので、早急に処理方法を考えたいと思います。


もう一つは、最初にも触れた、
日付ボタンがある場合は、タッチパネルが邪魔をして、ボタンが使えなくなると言う点です。

この辺りは、今後の課題ですね~ヽ(´~`;)ウーン

一応の操作は出来るようになったので、今回はこの辺で。
問題点の修正とその他の改良などは次記事でまとめたいと思います。
それでは、(o・・o)/~マタネェ

この記事へのコメント