Indexを管理するモデル＠AS3版

Flashでモックつくっていてカルーセルとかスライドショー的なものを汎用化していったらIndex.asっていうのを作る感じになったので公開してみるです。

# 実装

## Index.as

    package  {
	import flash.events.EventDispatcher;
	import flash.events.Event;
	
	public class Index extends EventDispatcher {
		private var _index:int = 0;
		private var _min:int = 0;
		private var _max:int = 0;
		private var _length:int = 0;
		private var _loop:Boolean = false;

		public function Index(min:int = 0, max:int = 0, loop:Boolean = false) {
			updateRange(min, max);
			_loop = loop;
		}

		public function set index(newIndex:int):void {
			newIndex = validate(newIndex);
			
			if (_index != newIndex) {
				_index = newIndex;
				
				dispatchEvent(new Event(Event.CHANGE));
			}
		}
		public function get index():int {
			return _index;
		}
		
		public function next():void {
			index++;
		}
		
		public function prev():void {
			index--;
		}
		
		public function range(min:int, max:int, uselessRefresh:Boolean = false):void {
			updateRange(min, max);
			
			if (!uselessRefresh) {
				index = index;
			}
		}
		
		public function set loop(value):void {
			_loop = value;
		}
		public function get loop():Boolean {
			return _loop;
		}
		
		public function get isMin():Boolean {
			return index === _min;
		}
		
		public function get isMax():Boolean {
			return index === _max;
		}
		
		private function updateRange(min:int, max:int):void {
			_min = min;
			_max = max;
			_length = _max - _min + 1;
		}
		
		private function validate(value:int):int {
			if (isNaN(value)) return _index;
			
			if (_loop) {
				value -= _min;
				value %= _length;
				if (value < 0) value += _length;
				return  value + _min;
			}
			else {
				return Math.min(Math.max(value, _min), _max);
			}
		}
	}
	
    }

## 使い方

下の例では、ViewがIndexインスタンスのCHANGEイベントを受け取って更新されるようにしてます。

    package  {
	import flash.events.Event;
	import flash.display.Sprite;
	
	public class SlideBase extends Sprite {
		private var _model:Index;
		public function SlideBase(size:int = 3) {
			_model = new Index(0, size - 1, true); //loop == true
			_model.addEventListener(Event.CHANGE, onChangeIndex);
			// initialize
			onChangeIndex(null);
		}
		public function next(): void {
			_model.next();
		}
		public function prev(): void {
			_model.prev();
		}
		public function index(value:int): void {
			_model.index = value;
		}
		public function loop(value:Boolean): void {
			_model.loop = value;
		}
		private function onChangeIndex(event:Event):void {
			updateDisplay(_model.index);
		}
		protected function updateDisplay(index:int):void {
                        // indexを使って見た目を更新する
		}
	}
    }

あとはボタンのCLICKイベント、キーボードイベント、タイマーイベントなど何かしらのタイミングでmodelを操作すればViewが切り替わるので、かなり柔軟でしかもスッキリ書けるようになります。


    import flash.events.MouseEvent;

    var slide:SlideBase = new SlideBase();
    addChild(slide);

    setInterval(function () {
        slide.next();
    }, 10000);

    addEventListener(MouseEvent.CLICK, function (event:MouseEvent) {
        slide.next();
    });


# 解説

## 機能要件

- index、min、maxあたりのプロパティを持ってる
- next(), prev(), index()的なI/Fを持っている
- loop処理、 範囲に応じたtrim処理あたりをよしなにやってくれる
- イベント発行する機能を持っている

あと最大値かどうか、最小値かどうかを返すメソッドを追加したあたりでまとめてみました。

## 実装

- 各プロパティはカプセル化する。indexはget/setアクセサを提供
- loopはフラグにして、値が変更された時にmin/maxでtrimするかループさせるかをうまいことやる
- AS3なのでEventDispatcherを継承するだけ。index値に変化があればEvent.CHANGEイベントを発行

という感じ。

## 工夫したところ

特に難しいことはないですが、ループする場合にすこし工夫が必要でした。

普通はindex値を範囲の長さで割った余りでシンプルに表現できるんだけど、
index値がマイナスになっちゃったりするとちょっとややこしいです。

ループさせたい範囲が min 0 ~ max 2 (長さ3)の場合で考えてみます。

## 値をnextしていくとき(index > 0になりうる場合)

    var length = 3;
    var newIndex = index % length;
    // 0 % 3 => 0
    // 1 % 3 => 1
    // 2 % 3 => 2
    // 3 % 3 => 0
    // 4 % 3 => 1
    // ...


## 値をprevしていくとき(index < 0になりうる場合)

    var newIndex = index % length;
    //  0 % 3 => 0
    // -1 % 3 => -1 // ループ的には2であってほしい
    // -2 % 3 => -2 // ループ的には1であってほしい
    // -3 % 3 => 0 // 3の倍数の時は問題なさげ
    // ...

というふうに、0 から prev()したときに、期待する挙動としては 2に戻ってほしいのに -1を3で剰余すると−1になってしまってうまくいかない。

でもよく見ると、結果値がマイナスのとき、結果値から期待値を引くと常に-3になる。

結果値 - 期待値 = -3

これを期待する値に合わせて整理すると

期待値 = 3 + 結果値

という事になるので、もし結果値がマイナスだったら、剰余する値を1度足せばうまくループしてくれるぽい。

    var length = 3;
    var newIndex = index % length;
    if (newIndex < 0) newIndex += length;

## 検算

- 0 % 3 = 0
- -1 % 3 + 3 = -1 + 3 = 2
- -2 % 3 + 3 = -2 + 3 = 1
- -3 % 3 = 0 = 0
- -4 % 3 + 3 = -1 + 3 = 2
- -5 % 3 + 3 = -2 + 3 = 1
- ...

とりあえずいきなり大きいマイナス値を入れても何とかなりそうです。
FlashでもJSでも地味にややこしいところなので整理してみました。

こちらで[似たようなものを](http://yd-niku.hatenablog.com/entry/2013/06/27/190743)を書いていますが、次はもちょっとちゃんとした版としてJSに移植してみます。


この記事は[2013-08-20の記事の転載](http://yd-niku.hatenablog.com/entries/2013/08/20)です。