かつて代官山らへんで働いてたengineerのUnityブログ

サーバサイドやってきたエンジニアがUnityとか触って遊ぶだけのブログ

【Unity】ScrollViewを魔改造してTwitter風ひっぱりリロード【uGUI】

スマホアプリなんかでよくある引っ張ってリロードするScrollViewをuGUIを改造して作ってみました。
twitterクライアントアプリとかに使われてるアレです。iOSとかandroidだとUIRefreshControlなんて言われてるやつですね。

ひっぱりリロードデモgif
↑こんなの

Unity側作業

1. UI→ScrollViewでスクロールビューを生成します。ScrollRectのHorizontalのチェックは外しておきましょう。

2. ScrollView/ViewPortの下に空のオブジェクトを作成します。名前は「OffsetGroup」としました。

3. ViewPort下にあるContentにVertical Layout Groupコンポーネントを設定しOffsetGroup下に移動します。

4. Content下にReloadPanelを作成します。リロードのクルクルが出てくるところです。

5. ReloadPanelにLayoutElementコンポーネントを設定し、PrefrredHeightを150にします。これがReloadPanelの高さになります。

6. ReloadPanel下にUI→ImageからLoadArrowImage(引っ張ると出てくる矢印)とLoadingImage(ロード中クルクマまわる画像)を生成し、テキトーな矢印とクルクルましたくなる画像をそれぞれに設定します。LoadingImageはactiveのチェックを外しておきます。

7. UI→PanelからPanelを生成し色をつけたらScrollView内のReloadPanelの部分がちょうど隠れるように被せます。上のgif画像デモでいうUnityマークがついた青い部分です。

以上でUnityでの作業は終わりです。こんな感じになってますでしょうか。
f:id:chroske:20160525161656p:plain

Script

ScrollViewに貼り付けるスクリプトです。
実は結構面倒なことをやっています。
ScrollView内のContentを下に引っ張った時に、Contentが動いた分と同じだけScrollViewも動かします。
しかしContentはScrollViewの子要素なので、同じだけ動かすとContentは2倍動いてしまう。
そこでOffsetGroupをContentが動いた方向とは逆方向に同じだけ動かすことで相殺し、Contentの動きにScrollViewが追従している感じになります。
多分動かして見たほうがわかると思うので、理解する前に動かすこと推奨。
ReloadPanelが隠していたPanelから出きったところでScrollViewの追従を止め、リロードを行います。
リロード完了したら追従を再開、elasticityで勝手に上に引っ張り戻る。

[SerializeField]
private GameObject content;
[SerializeField]
private GameObject offsetGroup;
[SerializeField]
private GameObject loadIcon;
[SerializeField]
private GameObject loadArrowIcon;

private bool listReloadFlag = true; //リロード可能フラグ
private bool listReloadEndFlag = false; //リロード終了確認用フラグ
private int scrollViewReloadHeight = -50; //ScrollViewがこのポジション以下まで下がったら更新開始 ReloadPanelの高さに準ずる
private int scrollViewDefaultHeight = 0; //ScrollViewのデフォルト位置
private float offsetGroupMoveRate = 0.80f; //戻るはやさに関係する
private float scrollRectDefaultElasticity; //デフォルトのelasticityを入れておく
private float pullBackscrollRectElasticity = 0.01f; //リロード開始までの引っ張って戻る速さに関係する

private ScrollRect scrollViewScrollRect;
private RectTransform scrollViewRectTransform;
private RectTransform contentRectTransform;
private RectTransform offsetGroupRectTransform;

void Start () {
	scrollViewScrollRect = GetComponent<ScrollRect> ();
	scrollViewRectTransform = GetComponent<RectTransform> ();
	contentRectTransform = content.GetComponent<RectTransform> ();
	offsetGroupRectTransform = offsetGroup.GetComponent<RectTransform> ();
	scrollRectDefaultElasticity = scrollViewScrollRect.elasticity;
}

void Update () {
	if (contentRectTransform.anchoredPosition.y < 0.0f && scrollViewRectTransform.anchoredPosition.y > scrollViewReloadHeight) {
		//ScrollViewのポジションが下がっているので戻る速度をあげてすばやく上にひっぱり戻す
		scrollViewScrollRect.elasticity = pullBackscrollRectElasticity;
		//ContentにScrollRectを追従させる
		scrollViewRectTransform.anchoredPosition = new Vector2 (0, contentRectTransform.anchoredPosition.y + scrollViewDefaultHeight);
		//offsetGroupをContentが移動した分だけ反対に移動させて子の追従を打ち消す
		offsetGroupRectTransform.anchoredPosition = new Vector2 (0, -contentRectTransform.anchoredPosition.y * offsetGroupMoveRate);
	} else if(scrollViewRectTransform.anchoredPosition.y <= scrollViewReloadHeight) {
		//reloadしてもいい状態か確認
		if (listReloadFlag) {
			listReloadEndFlag = false;
			listReloadFlag = false;

			//矢印を消してローディングを出す
			loadArrowIcon.SetActive(false);
			loadIcon.SetActive(true);

			//ScrollViewのポジションが下がらなくなるので戻る速度をデフォルトに戻す
			scrollViewScrollRect.elasticity = scrollRectDefaultElasticity;
			//ズレるので決め打ちで値を入れておく
			scrollViewRectTransform.anchoredPosition = new Vector2 (0, scrollViewReloadHeight);
			//リスト取得通信(デモなので実際の通信はしないよ)
			StartCoroutine (PullBackScrollView ());
		} else {
			//reload通信が終了していれば引き戻す処理
			if(listReloadEndFlag){
				scrollViewScrollRect.elasticity = pullBackscrollRectElasticity;
				scrollViewRectTransform.anchoredPosition = new Vector2 (0, contentRectTransform.anchoredPosition.y + scrollViewDefaultHeight);
				offsetGroupRectTransform.anchoredPosition = new Vector2 (0, -contentRectTransform.anchoredPosition.y * offsetGroupMoveRate);
			}
		}
	}
	//reloadした状態で上まで戻ったら再度reloadが行えるフラグをtrueにする
	if(!listReloadFlag){
		if(scrollViewRectTransform.anchoredPosition.y >=  scrollViewDefaultHeight-1){
			listReloadFlag = true;
		}
	}
	//loadIconを監視してactiveなら回す
	if(loadIcon.activeSelf){
		loadIcon.transform.eulerAngles += new Vector3 (0f, 0f, -8f);
	}
}

IEnumerator PullBackScrollView(){
	//とりあえず2秒クルクルさせる
	yield return new WaitForSeconds(2);

	/* ここらへんで通信してデータ取って来てリストに追加する */

	//ローディングから矢印に切り替え
	loadArrowIcon.SetActive(true);
	loadIcon.SetActive(false);
	listReloadEndFlag = true;

	yield break;
}

以上をスクリプトをScrollViewに貼り付けて各子オブジェクトとのひも付けを設定してやってください。

まとめ

今回はなるべくコードでアニメーションを制御せず、uGUIの動き利用しようということでこんな方法になりました。
もしかしたらScrollViewのmovement typeをUnrestrintedにして全部自前で制御した方が今後のメンテナンスを考えるといいのかもしれませんが、パパっと作るならこんなもんでもいいのかなーと
パパっとできてる感もあんまりないけど...
気が向いたらAssetStoreにでもサンプルを出してみようと思います。
需要あんのかなー