前回までに作ったコードを実際に試された方は気がつかれたと思いますが、”戻る” ボタンがちゃんと機能しない場合があります。今回はその辺りを修正してみたいと思います。
問題その1: 2つ前の画面に戻れない
いくつかのページを表示した後、”戻る” を押すと前の画面に戻ります。ところが、もう一回 ”戻る” を押しても同じページが表示されてしまいます。その後何回押しても同じページのままです。
これは、HTML コンポーネントの complete イベントで履歴を追加するようにしたことに原因があります。(Apollo でブラウザを作る の続き) 普通に新しいページをロードしているうちは問題ないのですが、”戻る”ボタンで前のページを表示したときにも、戻ったページの表示が完了すると complete イベントが発生するため戻ったページを改めて履歴に追加しなおすという一歩戻ってまた進むような動作をしてしまうのです。
これを解決するには complete イベントハンドラ内で、”戻る” の押下による表示かどうかを判定して動作を切り替えればよいはずです。そこで、状況を示すフラグを追加して、それによってハンドラの動作を切り替えるよう変更します。修正される箇所はフラグの追加を含め 3 箇所です。
private var isBack:int = 0; // 戻り中を示すフラグの宣言
private function back():void {
if (history.length > 1) {
history.pop(); // 現在の URL を廃棄
var prevURL:String = history[history.length - 1]; // 最終エントリを取得
html.location = prevURL; // HTML コンポーネントに URL を設定
isBack++; // フラグをセット
}
}
private function pushURL(e:Event):void {
if (isBack > 0) {
isBack--;
} else {
history.push(html.location); // 現在の location を配列に記録
}
inputTF.text = html.location;
fadeIn.target = html; // 効果のターゲットを設定したら開始
fadeIn.play();
}
まず、isBack という変数を宣言して、back() 内でその値を一つ増やしています(”戻る” の開始)。onComplete() 内では isBack が正の値なら一つ値を減らし(”戻る” の完了)、それ以外の場合(新規ページの表示)のみ通常の処理を行うよう変更しました。
これで、2つ前のページにも戻れるようになったと思います。
HTTP コンポーネントのイベント
問題その2: 表示完了前に ”戻る” ボタンを押すと2つ前の画面に飛んでしまう
”戻る” は一つ前の画面に戻るためのボタンです。ところがときどき2つ前の画面に戻ってしまうことがあります。これは、履歴の追加されるタイミングが complete イベント発生時、すなわちページ全体の描画完了時であるためです。
今の仕様だと、読み込んだページの描画が完全に終了するまで、履歴の状態は前のページが表示されていたときのままです。なので描画完了前に ”戻る” を押すと、一つ前のさらに前のページに移動してしまうわけです。
とすると画面に表示が開始されるまでには履歴の追加を済ませておきたいところです。では、都合よく使えそうなイベントは HTML コンポーネントにあるでしょうか。
以下が、HTML コンポーネントの描画に関連するイベントです。
locationChange: HTML コンポーネントの location プロパティが変更された domInitialize : HTML ドキュメントの読み込み開始に伴い DOM が初期化された htmlRender : HTML コンテンツの描画が完了した complete : 全てのコンテンツの読み込みと描画が完了した
このうち htmlRender は読み込むコンテンツが複数あるとそれぞれのコンテンツの描画完了に対して発生します。
今回は読み込み開始のタイミングで処理を行いたい訳ですから domInitialize を使うのがよさそうです。そこで complete イベントで呼び出している pushURL() メソッドから履歴操作に関連する分を取り分けます。
メソッドを分けたついでにそれぞれ onDomInitialize() と onComplete() という名前に変更します。そのまたついでに locationChange の処理も onLocationChange() というメソッドで行うことにします。
この変更で、呼び出されるメソッドの定義はそれぞれ以下のようになります。
private function onLocationChange(e:Event):void {
html.alpha=0
}
private function onDomInitialize(e:Event):void {
if (isBack > 0) {
isBack--;
} else {
history.push(html.location); // 現在の location を配列に記録
}
inputTF.text = html.location;
}
private function onComplete(e:Event):void {
fadeIn.target = html; // 効果のターゲットを設定したら開始
fadeIn.play();
}
いままで back() の中でも入力テキストフィールドに読み込む対象の URL を設定していましたが、どうやら onComplete() のタイミングだけでよさそうです。ということで back() メソッドからはその行を削除することにします。
最後に HTML コンポーネントからこれらのメソッドが呼び出されるよう記述を変更します。
<mx:HTML id="html" width="100%" height="100%" locationChange="onLocationChange(event)" domInitialize="onDomInitialize(event)" complete="onComplete(event)"/>
さて、これを実際に試してみましょう。描画完了前に ”戻る” を押しても2つ前には戻らなくなったかと思います。
UI コンポーネントの状態を変更する
問題その3: 履歴が無くても ”戻る” ボタンが押せる状態になっている
プログラムを起動した瞬間から ”戻る” ボタンがアクティブになっています。これも気になりますよね。
まず、初期表示時に ”戻る” ボタンを無効にしたいと思います。それには enabled プロパティを false にします。後でボタンを有効にするためこのプロパティを切り替える必要があるはずなので、id も指定しておきます。backBtn という名前にしてみました。
<mx:Button id="backBtn" label="戻る" click="back()" enabled="false"/>
ボタンを有効にできるのは履歴が2つ以上あるときです。履歴を追加したタイミングで履歴の数をチェックしてボタンの有効/無効を切り替えるようなロジックを追加します。
private function onDomInitialize(e:Event):void {
if (isBack > 0) {
isBack--;
} else {
history.push(html.location); // 現在の location を配列に記録
if (history.length > 1) { // 履歴が1より多い
backBtn.enabled = true;
}
}
inputTF.text = html.location;
}
逆に ”戻る” を押したときは履歴の数が1以下になったら無効に戻す処理が必要です。
private function back():void {
if (history.length > 1) {
...
if (history.length <= 1) { // 履歴が1以下
backBtn.enabled = false;
}
}
}
これでボタンの表示が戻る先の有り無しで切り替わるようになったと思います。
以下が今回の修正を反映したコードです。50 行余りになりました。
<?xml version="1.0" encoding="utf-8"?>
<mx:ApolloApplication xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:Script>
<![CDATA[
// この配列に現在表示中の URL までが記録されている
private var history:Array = new Array();
private var isBack:int = 0;
private function back():void {
if (history.length > 1) {
history.pop(); // 現在の URL を廃棄
var prevURL:String = history[history.length - 1]; // 最終エントリを取得
html.location = prevURL; // HTML コンポーネントに URL を設定
isBack++;
if (history.length <= 1) { // 履歴が1以下
backBtn.enabled = false;
}
}
}
private function loadURL():void {
html.location = inputTF.text;
}
private function onLocationChange(e:Event):void {
html.alpha=0
}
private function onDomInitialize(e:Event):void {
if (isBack > 0) {
isBack--;
} else {
history.push(html.location); // 現在の location を配列に記録
if (history.length > 1) { // 履歴が1より多い
backBtn.enabled = true;
}
}
inputTF.text = html.location;
}
private function onComplete(e:Event):void {
fadeIn.target = html; // 効果のターゲットを設定したら開始
fadeIn.play();
}
]]>
</mx:Script>
<mx:Style source="MyBrowser.css"/>
<mx:Fade id="fadeIn" duration="1000" alphaFrom="0.0" alphaTo="1.0"/>
<mx:HBox>
<mx:TextInput id="inputTF" width="225" text="http://" enter="loadURL()"/>
<mx:Button label="移動" click="loadURL()"/>
<mx:Button id="backBtn" label="戻る" click="back()" enabled="false"/>
</mx:HBox>
<mx:HTML id="html" width="100%" height="100%" locationChange="onLocationChange(event)"
domInitialize="onDomInitialize(event)" complete="onComplete(event)"/>
</mx:ApolloApplication>

Akihiro, thanks for all, for all you articles. It's very nice...