Scroller
以下は、Mac OS X 10.4.11 上の Xcode 2.5 で説明しています。このウィジェットは、BlankWidgetで解説したことは繰り返しませんので、そちらを参照してください。
1 ウィジェット
Scroller.wdgt は、以下のようなウィジェットです。
スクロール領域内に多量のテキストが表示されます。右のスクローラーをドラッグすれば、スクロール表示できます。灰色で表示されている 2 つの年のテキストをクリックすれば、テキスト内容が別のものに切り替わります。真ん中のものは、より少ないテキストで、右側のものはスクロールの必要がなく、スクロールバー自体が表示されません。
2 ファイル構成
このサンプルのファイル構成は以下のようになります。
AppleClasses 内には、AppleScrollArea.js と AppleScrollbar.js という 2 つの JavaScript ファイル、そして Images という画像フォルダがあり、これらのファイルがスクロール動作をになっています。Images 内にはスクロールを行うときに必要なコントロールの各部品を表す画像が多量に含まれていますが、ここでは開いて表示させていません。Apple クラスの組み込みと Info.plist については、Stretcher の「ファイル構成」 を見てください。主要 HTML は Scroller.html に指定されています。
それに加えて、『Dashboard プログラミングトピック』>「スクロール領域の使用」で書かれているように、AppleScrollbar.js 内の 画像パスを書き換える必要があります。これらの画像パスは、後で見るように、AppleVerticalScrollbar と AppleHorizontalScrollbar の 2 つのコンストラクタ内にあります。
Default.png は、ウィジェットの背景が含まれている画像です。pattern.png はそれと同じ背景をもち、正方形に切りとられたもっと小さい画像です。他の画像ファイルはスクロールバー部品などです。ここでは、直接的には使われていません。
3 Scroller.html
それでは、主要 HTML ファイルを見てみます(スクロール領域内に表示されるテキスト部分は量が多いため中略して詰めています)。
Scroller.html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv='Content-Type' content='text/html; charset=ISO-8859-1'>
<title>Scroller Example Widget</title>
<style type='text/css'>
@import "Scroller.css";
</style>
<!-- Pre-10.4.3 より前の互換性:
ウィジェットの一番上に AppleClass ディレクトリを含め、相対パスを使用 -->
<script type='text/javascript' src='AppleClasses/AppleScrollArea.js'></script>
<script type='text/javascript' src='AppleClasses/AppleScrollbar.js'></script>
<script type='text/javascript' src='Scripts/Scroller.js'></script>
</head>
<body onload='loaded();'>
<div id='front'>
<div id='topBar'>
<div class='contentControl' id='uscControl'
onmousedown='showConstitution();'>1787</div>
<div class='contentControl' id='gbaControl'
onmousedown='showGettysburg();'>1863</div>
<div class='contentControl' id='fearControl'
onmousedown='showFear();'>1933</div>
</div>
<hr class='separator' id='topSeparator'>
<!-- このサンプルは3つのスクロール可能内容divをもつ:
2つはスクリーン外の量が変更されたとき、サムがサイズ変更されることを示すためはみ出る
1つはスクロール可能な内容がない時、サムが表示・隠蔽されることを示すためはみ出ない -->
<div id='parentDiv'>
// 1
<div class='mainContent' id='gettysburg'>
<p>Fourscore and seven years ago ...
(中略)
... people shall not perish from the earth."</p>
</div>
<div class='mainContent' id='constitution'>
<P>We the People of the United States, ...
(中略)
... Note 16: Repealed by section 1 of amendment XXI.
</div>
<div class='mainContent' id='fear'>
The only thing we have to fear is fear itself.
</div>
</div>
<div id='myScrollBar'></div>
// 2
<hr class='separator' id='botSeparator'>
</div>
</body>
</html>
1 と 2 を見てください。1 の id 属性が parentDiv の div 要素がスクロール領域、2 の id 属性が myScrollBar の div 要素がスクロールバーに対応付けられます。
全体的な構造として、まず body 要素のなかに front という id 属性の div 要素があります。これは表の面ということで、環境設定ボタン等で裏面を表示する場合、id が back の div 要素を置きます。このほか、情報ボタン自体も body 直下に置かれ、3 つの div 要素が body に含まれているというのが基本パターンです。ここでは、そのうちの front だけが存在します。
front 内には、順番に、id がtopBar の div 要素、topSeparater という hr 要素、parentDiv というスクロール領域となる div 要素、myScrollBar というスクロールバーとなる div 要素、botSeparator という hr 要素があります。ここで、よく置かれている背景画像がないことに注意してください。背景画像は image 要素ではなく、CSS 内で背景として指定されています。
topBar の div 内には、3 つの div が含まれていて、これが内容を切り替えるボタンになっています。それぞれの要素からマウスクリックのハンドラとして別々の関数が呼ばれています。
parentDiv 内には、mainContent という同じクラスで、別々の id をもつ div 要素が含まれています。文章量が多いので、ここでは中略しています。ボタンで切り替える内容があらかじめすべて含まれていることに注意してください。
これで HTML 本体の構造はわかりました。つぎは CSS を見てみます。
4 Scroller.css
スタイルシートを見てみましょう。
Scroller.css
#front {
...
background: url(Default.png);
background-repeat: no-repeat;
}
/*
* parentDivはoverflow:hidden、mainContent divは通常の(見える)overflow
parentDivは実際にスクロールされるものになる
mainContentの子が切り替えられることで、
(切り替え可能なmainContent divの上にスクロールオブジェクトが作られるなら必要になる)
3つの部分を管理するかわりに
単一のAppleScrollAreaとAppleScrollbarオブジェクトを可能にする */
#parentDiv {
...
overflow: hidden;
}
.mainContent {
padding: 4px;
display: none;
}
#fear {
position: absolute;
top: 80px;
width: 100%;
text-align: center;
}
#myScrollBar {
position: absolute;
top: 68px;
bottom: 34px;
right: 20px;
width: 19px;
display: block;
}
通常、body 要素内に image 要素として配置されている背景画像が、CSS の background プロパティとして指定されていることに注意してください。background プロパティは、background-color(背景色)、background-image(背景画像)、background-repeat(繰り返し)、background-position(背景画像表示位置)、background-attachment(背景画像固定位置)の包括指定であることに注意してください。本来は background-image として指定すべきですが、URL が指定されているので自動的に背景画像として解釈されているのでしょう。
コメントにもありますが、スクロール領域である parentDiv は overflow:hidden 属性を持っています。『Dashboard プログラミングトピック』の「スクロール領域の使用」ではスクロール領域に対する CSS でこのことは説明されていません。このような overflow 属性は、Apple クラスのスクロール領域で適切に処理してくれていそうな気もします。そこでためしに overflow:hidden をコメントアウトして Safari で開いてみます。すると、動作は全く変わりません。overflow:visible としても変わりません。さらに子の div 要素の overflow 属性を hidden にしても変わりません。つまり、この div の CSS 属性は位置以外は気にすることはないようです。Apple クラス内で適切に設定してくれているようです。
#fear の ID セレクタにおいて、上からの距離等が設定されていますが、これはスクロールなしの場合に領域の中央にテキストを表示するためのものです。
myScrollBar の ID セレクタでは、width:19px となっています。これは Apple クラスでデフォルトで提供されているスクロールバー画像の幅です。このことは『Dashboard プログラミングトピック』>「スクロール領域の使用」でも注意されています。デフォルトの画像以外を使うなら、必ずしも 19px ではありません。ちなみに miScrollBar が設定されている div 要素はデフォルトでブロック要素です。最後の display:block は特に必要ありません。コメントアウトしても変わりません。
5 Scroller.js
Scroller.js ファイルでは、このサンプル独自の動作を行っています。これには、スクロール領域の使用も含まれます。まず body 要素の読み込み時に呼ばれる関数を見てみます。
Scroller.js > グローバル変数と loaded
var scrollbar, scrollArea;
function loaded() {
scrollbar = new AppleVerticalScrollbar(document.getElementById("myScrollBar"));
scrollArea = new AppleScrollArea(document.getElementById("parentDiv"), scrollbar);
scrollArea.scrollsHorizontally = false;
scrollArea.singlepressScrollPixels = 25;
scrollArea.focus(); // 最初に Safari 内に読み込まれた時のキーコントロールに対して
window.onfocus = function () { scrollArea.focus(); }
window.onblur = function () { scrollArea.blur(); }
showConstitution();
}
まずスクロールバーを構築します。垂直スクロールバーを作るため AppleVerticalScrollbar が使われていることに注意してください。水平なら AppleHorizontalScrollbar を使います。ここでは ID によって要素を取得し、それを使っていますが、DOM 要素なら違う形で取得し設定してもかまいません。
スクロールバーの構築が終われば、スクロール領域を構築します。ここでは、最初に構築したスクロールバーが引数として渡されていますが、AppleScrollArea(document.getElementById("parentDiv")); としてから、scrollArea.addScrollbar(scrollbar); によってスクロールバーを追加してもかまいません。スクロール領域には任意の数のスクロールバーを関連付けられるので、垂直スクロールバーを2つ関連付けることも可能です。ちなみに、コンストラクタにまとめてスクロールバーを渡すなら、2番目以降の引数に追加する分だけ多くのスクロールバーを渡せます。この例では、1つだけですが、垂直と水平の2つのスクロールバーを同時に渡すことも可能です。
それから、スクロール領域の特性を設定しています。まず、scrollsHorizontally に false を代入して、水平スクロールを不可にしています。つぎの singlepressScrollPixels は、矢印キーが押されたときにスクロールするピクセル数です。そして、スクロール領域にフォーカスを与えます。そしてウインドウのハンドラに、スクロール領域の関数を関連付けています。スクロール領域がキーボードフォーカスに反応してほしいなら、スクロール領域の設定とは別に、このサンプルのように、きちんと設定する必要があることに注意してください。
つぎの showConstitution は、最初に表示される内容である文章量の多いものを設定する関数です。これと、showGettysburg、showFear はほぼ同じ構造で、これは一番上に表示されているボタンにも設定されていたものです。
Scroller.js > showConstitution
function showConstitution() {
showContent(document.getElementById('constitution'),
document.getElementById('uscControl'));
}
単に ID を使って別の内容に切り替える関数に引数を渡しています。この関数は、ボタンから呼ばれる他の関数からも使われています。
Scroller.js > showConstitution
var oldContent;
var oldControl;
// 内容 DIV 間で切り替えを行う
function showContent(newContent, newControl) {
if (oldContent != null) {
oldContent.style.display = 'none';
oldControl.style.color = 'gray';
}
newControl.style.color = 'black';
newContent.style.display = 'block';
// スクロールサムのサイズ変更と再配置
scrollArea.reveal(newContent);
scrollArea.refresh();
oldContent = newContent;
oldControl = newControl;
}
まず古い内容があるなら、表示なしで色を灰色にしています。それが終わった後で、新しく表示する id をもつ div の色と表示を通常に戻します。それから Apple クラスで提供されているメソッドを使います。reveal はその要素が表示されるまでスクロールを行うメソッドです。最後に refresh で再描画させます。それから、新しく表示した内容を古い内容として保存しておきます。
6 まとめ
スクロール領域の使用そのものは、単純であることがわかったと思います。スクロール領域とスクロールバーに対して div を設定し、あとは Apple クラスが提供するメソッドを使って設定を行うだけです。『Dashboard プログラミングトピック』>「スクロール領域の使用」を見れば、他に使用できるメソッドについての解説があります。
7 AppleScrollArea.js
AppleScrollArea.js を調べてみます。まずコンストラクタです。
AppleScrollArea.js > AppleScrollArea
/*
* AppleScrollArea コンストラクタ
* content は、スクロールされる表示を収容している要素
* 追加の引数は、this.addScrollbar を使ってスクロールバーとして追加されることになる
*/
function AppleScrollArea(content)
{
/* オブジェクト */
this.content = content;
// 内容要素
/* パブリックプロパティ */
// これらは読み書きできる。必要に応じて設定
this.scrollsVertically = true;
// デフォルトで垂直水平スクロール可
this.scrollsHorizontally = true;
this.singlepressScrollPixels = 10;
// 矢印クリックで10ピクセル移動
// これらは読みとり専用
this.viewHeight = 0;
// スクロール領域の高さ
this.viewToContentHeightRatio = 1.0;
// 全体量に対するビューの高さの比
this.viewWidth = 0;
// スクロール領域の幅
this.viewToContentWidthRatio = 1.0;
// 全体量に対するビューの幅の比
/* 内部オブジェクト */
this._scrollbars = new Array();
// 追加されたスクロールバー配列
// JavaScript イベントハンドラに対して
var _self = this;
// このインスタンスを指すプライベート変数
/*
* 特権メソッド
* これらのイベントハンドラは、イベントハンドラ内では、
* クラスのインスタンスではなく、「this」がイベントを呼び出した要素を参照するので
* ここに置かれる必要がある
*/
// プライベート変数は特権メソッドでのみアクセス可
this._refreshHandler = function() { _self.refresh(); };
this._keyPressedHandler = function() { _self.keyPressed(event); };
this._mousewheelScrollHandler
= function(event) { _self.mousewheelScroll(event); };
// 確実にするため内容に対するスタイルをセットアップ
this.content.style.overflow = "hidden";
// 余分な内容を隠す
this.content.scrollTop = 0;
// 最初は左上隅表示
this.content.scrollLeft = 0;
// イベント監視オブジェクトを追加
this.content.addEventListener("mousewheel", this._mousewheelScrollHandler, true);
this.refresh();
// スクロールバーを追加
// 2つ以上の引数があった場合
var c = arguments.length;
for (var i = 1; i < c; ++i)
{
this.addScrollbar(arguments[i]);
}
}
構造自体は簡単なのですぐ理解できると思います。まず引数として渡された内容要素を代入するなど、必要な変数の初期化を行います。その後、スクロールバー配列を作成します。
_self はプライベートな変数です。コンストラクタ内でこのように宣言された変数はプライベート変数となり、コンストラクタ内で、このあと定義される特権メソッドによってだけアクセス可能になります。ここでは、コメントにもあるように、自分自身を保存しておき、this が使えない所で自身を参照できるようにしています。特権メソッド自体は、公開メソッド内など、どこでも使用可です。Objective-C 言語などのインスタンス変数とアクセサの関係と同じようなものです。
つぎに内容要素に対するスタイル指定をしています。それから addEventListener でイベント監視を登録しています。ここではマウスホイールによるスクロールに対応できるようにします。この関数については、Grid 解説の Grid.js で説明しています。それから refresh() で自身の更新を行います。
最後に追加の引数があれば、それをスクロールバーとして追加しています。引数の個数を取得し、1 から開始することで、2番目の引数から始めます。2番目の引数がない場合、for 文は一度も実行されずに抜けます。
つぎは、スクロールバーを追加するメソッドです。
AppleScrollArea.js > addScrollbar
AppleScrollArea.prototype.addScrollbar = function(scrollbar)
{
scrollbar.setScrollArea(this);
this._scrollbars.push(scrollbar);
scrollbar.refresh();
}
prototype を使って全インスタンスに共通なパブリックメソッドを定義しています。後で見る、スクロールバーメソッドで、この領域をスクロールバーに設定します。その後、スクロールバー配列に push を使ってスクロールバーを追加します。それからスクロールバーを更新しています。つぎは、逆にスクロールバーを除去するメソッドです。
AppleScrollArea.js > removeScrollbar
AppleScrollArea.prototype.removeScrollbar = function(scrollbar)
{
var scrollbars = this._scrollbars;
for (i in scrollbars)
{
if (scrollbars[i] == scrollbar)
{
scrollbars.splice(i, 1);
break;
}
}
}
まずこのインスタンスのスクロールバー配列内に渡されたスクロールバーがあるかチェックしています。あれば、それを除去します。ここで使われている splice は Array オブジェクト内の要素を置き換えるもので、3 番目の引数があればそれで置換しますが、ここでは指定されていないので除去になります。
つぎはスクロール領域そのものをウィジェットから除去するメソッドです。
AppleScrollArea.js > remove
AppleScrollArea.prototype.remove = function()
{
this.content.removeEventListener
("mousewheel", this._mousewheelScrollHandler, true);
var scrollbars = this._scrollbars;
for (i in scrollbars)
{
scrollbars[i].setScrollArea(null);
}
}
まずコンストラクタで登録していたイベント監視を解除します。それからスクロールバーの配列内の各インスタンスに対して、このスクロール領域との関連を解除させています。
つぎは、上のメソッドでも何度か呼ばれていた refresh メソッドです。
AppleScrollArea.js > refresh
/*
* refresh() メンバ関数
* 現在のスクロールバーの位置とサイズを更新
* これは内容要素が変更された時はいつでも呼ばれたほうがいい
*/
AppleScrollArea.prototype.refresh = function()
{
// 現在の実際のビューの高さを取得、割るので float
var style = document.defaultView.getComputedStyle(this.content, '');
if (style)
{
this.viewHeight = parseFloat(style.getPropertyValue("height"));
this.viewWidth = parseFloat(style.getPropertyValue("width"));
}
else
{
this.viewHeight = 0;
this.viewWidth = 0;
}
if (this.content.scrollHeight > this.viewHeight)
this.viewToContentHeightRatio = this.viewHeight / this.content.scrollHeight;
else
{
this.viewToContentHeightRatio = 1.0;
this.verticalScrollTo(0);
}
if (this.content.scrollWidth > this.viewWidth)
this.viewToContentWidthRatio = this.viewWidth / this.content.scrollWidth;
else
{
this.viewToContentWidthRatio = 1.0;
this.horizontalScrollTo(0);
}
var scrollbars = this._scrollbars;
for (i in scrollbars)
{
scrollbars[i].refresh();
}
}
まず、現在の表示状態で計算されたスタイルから領域の高さと幅を Float として取得しています。スタイルが計算されなければ、0 に設定されます。これはコンストラクタ内で最初に 0 が代入されていました。もし読み込み完了後のハンドラ内でコンストラクタが呼ばれていたら、コンストラクタの最後のほうでこのメソッドが呼ばれているので、ここで実際の高さや幅がきちんと設定されることになります。
scrollHeight と scrollWidth は、JavaScript(というより DOM)にあるプロパティで、スクロールされる領域全体の高さや幅です。これが表示されている高さや幅より大きければ、以下で表示領域の全体に対する割合を計算しなおしています。これはコンストラクタで 1.0 とされていたものです。もし全体の高さが表示領域内に収まるものなら、比を 1.0 にして、スクロール箇所を左上に移動させています。
その後で、スクロールバー配列内のスクロールバーそれぞれに更新を行わせています。
つぎはキーボードフォーカスを行う focus です。このサンプルでは、Scroller.js の読み込み完了ハンドラ内から呼ばれていました。
AppleScrollArea.js > focus
/*
* focus() メンバ関数
* スクロール領域にフォーカスが置かれていることを通知
* キー押下イベントを受けとることになり、矢印キーならそれによってスクロールする
*/
AppleScrollArea.prototype.focus = function()
{
document.addEventListener("keypress", this._keyPressedHandler, true);
}
コメントどおりです。単にキー押し下げ時のハンドラを設定しています。ここで設定されているのはコンストラクタ内で設定された特権メソッドです。これは自身のインスタンスの keyPressed メソッドを呼び直しています。
つぎは逆にキーフォーカスが外れた場合のメソッドです。このサンプルでは、その場合のイベントハンドラとして、読み込み完了ハンドラで登録されていました。
AppleScrollArea.js > blur
/*
* blur() メンバ関数
* スクロール領域にもはやフォーカスがないことを通知
* キー押下イベントを受けとることをやめることになる
*/
AppleScrollArea.prototype.blur = function()
{
document.removeEventListener("keypress", this._keyPressedHandler, true);
}
コメントどおり、focus メソッドで設定された監視を解除しています。
つぎは、特定の要素を表示するまでスクロールを行う reveal メソッドです。このサンプルでは内容を切り替えた後で呼ばれていました。このメソッドが呼ばれた後で、refresh() が行われていたことに注意してください。
AppleScrollArea.js > reveal
/*
* reveal(element) メンバ関数
* 内容要素内に含まれる要素を渡す
* 内容はその要素が表示されるようスクロールされることになる
*/
AppleScrollArea.prototype.reveal = function(element)
{
var offsetY = 0;
// まず垂直オフセット計算
var obj = element;
do
{
offsetY += obj.offsetTop;
obj = obj.offsetParent;
} while (obj && obj != this.content);
var offsetX = 0;
// つぎに水平オフセット計算
obj = element;
do
{
offsetX += obj.offsetLeft;
obj = obj.offsetParent;
} while (obj && obj != this.content);
this.verticalScrollTo(offsetY);
// 計算分スクロール
this.horizontalScrollTo(offsetX);
}
まず、垂直オフセットを計算しています。offsetY を最初 0 で初期化し、渡された要素の親要素内での垂直オフセットを足します。こうして、親要素を順番に計算していき、スクロール領域に設定された要素に至るまでオフセットを足しあわせていきます。結果として、内容要素内の左上からの渡された要素までの垂直距離が計算されます。その後、水平オフセットも同様にして計算されます。
実際のスクロールは、verticalScrollTo と horizontalScrollTo というメソッドで行っています。これら 2 つがスクロール領域内における実際のスクロール動作を担当することになります。
AppleScrollArea.js > verticalScrollTo
AppleScrollArea.prototype.verticalScrollTo = function(new_content_top)
{
if (!this.scrollsVertically)
// 垂直スクロール不可なら戻る
return;
var bottom = this.content.scrollHeight - this.viewHeight;
// 全体高から表示高を引く
if (new_content_top < 0)
{
new_content_top = 0;
// 負なら0に
}
else if (new_content_top > bottom)
{
new_content_top = bottom;
// 高さが超過しているなら最後に
}
this.content.scrollTop = new_content_top;
// スクロールトップ保存
var scrollbars = this._scrollbars;
// スクロールバーに通知
for (i in scrollbars)
{
scrollbars[i].verticalHasScrolled();
}
}
垂直スクロール不可なら何も行いません。つぎに全体高から表示高を引くことで、最後までスクロールした場合の開始高を計算します。それから引数をチェックし、負や最終位置を超えていたら適切に調整します。そして、scrollTop に新しい値を代入します。実際の内容表示はブラウザなどの組み込み機能にまかせています。それからスクロールバーに表示を更新するようにスクロールされたことを通知しています。
horizontalScrollTo も同様です。
つぎはキー押下時に設定されたイベントハンドラから呼ばれるメソッドです。
AppleScrollArea.js > keyPressed
/*********************
* Keypressed イベント
*/
AppleScrollArea.prototype.keyPressed = function(event)
{
var handled = true;
if (event.altKey)
return;
if (event.shiftKey)
return;
switch (event.keyIdentifier)
{
case "Home":
this.verticalScrollTo(0);
break;
case "End":
this.verticalScrollTo(this.content.scrollHeight - this.viewHeight);
break;
case "Up":
this.verticalScrollTo(this.content.scrollTop - this.singlepressScrollPixels);
break;
case "Down":
this.verticalScrollTo(this.content.scrollTop + this.singlepressScrollPixels);
break;
case "PageUp":
this.verticalScrollTo(this.content.scrollTop - this.viewHeight);
break;
case "PageDown":
this.verticalScrollTo(this.content.scrollTop + this.viewHeight);
break;
case "Left":
this.horizontalScrollTo(this.content.scrollLeft - this.singlepressScrollPixels);
break;
case "Right":
this.horizontalScrollTo(this.content.scrollLeft + this.singlepressScrollPixels);
break;
default:
handled = false;
}
if (handled)
{
event.stopPropagation();
event.preventDefault();
}
}
まず処理したかどうかを true にし、個々のキーによって適切にスクロールしているだけです。最後に処理を行った場合、これ以上のイベントの伝播を防いでいます。
最後はマウスホイールのイベントハンドラです。
AppleScrollArea.js > mousewheelScroll
/*********************
* スクロールホイールイベント
*/
AppleScrollArea.prototype.mousewheelScroll = function(event)
{
var deltaScroll = event.wheelDelta / 120 * this.singlepressScrollPixels;
this.verticalScrollTo(this.content.scrollTop - deltaScroll);
event.stopPropagation();
event.preventDefault();
}
まず wheelDelta を使って、ホイールの移動値を求めています。通常の前進の場合 120、後進の場合 120 が返ります。そのため 120 で割って、それに矢印クリック1回の移動量を掛けています。それからスクロール位置を調整しています。超過や負の場合は、verticalScrollTo 内で調整しているので、移動しすぎることはありません。そしてイベントの伝播を防いでいます。
これでスクロール領域の実装はすべて調べました。実際のスクロールにおける表示自体は、組み込みの機能にまかせていることがわかったと思います。主にスクロールバーを管理して、現在のスクロール場所の変数などを扱っています。
8 AppleScrollbar.js
8.1 継承関係とコンストラクタ
つぎに、AppleScrollbar.js を調べてみましょう。このファイルでの実装は、個々の画像の配置などの作業を行っているので、コード量がけっこうあります。このファイルでは、AppleScrollbar というオブジェクトを定義し、そのサブクラスとして、AppleVerticalScrollbar と AppleHorizontalScrollbar という 2 つのオブジェクトを定義していることに注意してください。方向とは関係ないメソッドを上位クラスで実装し、それを利用するという形になっています。まず AppleScrollbar から見てみます。
AppleScrollbar.js > AppleScrollbar
function AppleScrollbar(scrollbar)
{
}
コンストラクタは何も行っていません。初期化等はどうするのでしょうか。すぐ後に _init というメソッドが定義されています。このメソッドは、2 つのサブクラスのコンストラクタの最後で呼ばれていて、共通の初期化を行います。そのメソッドを説明する前に、抽象クラスだと具体的なイメージがわかないと思われるので、垂直スクロールバークラスを調べて説明していきます。コンストラクタと上位クラスとの関連付けは以下のようになっています。
AppleScrollbar.js > AppleScrollbar
function AppleVerticalScrollbar(scrollbar)
{
/* オブジェクト */
this.scrollarea = null;
this.scrollbar = scrollbar;
/* パブリックプロパティ */
// これらは読み書き可能、必要に応じて設定する
this.minThumbSize = 28;
// 最小サムサイズ
this.padding = -1;
// これらは読みとり専用。これらを設定するには設定メソッドを使う
this.autohide = true;
this.hidden = true;
this.size = 19; // 幅
this.trackStartPath = "AppleClasses/Images/scroll_track_vtop.png";
this.trackStartLength = 18; // 高さ
this.trackMiddlePath = "AppleClasses/Images/scroll_track_vmid.png";
this.trackEndPath = "AppleClasses/Images/scroll_track_vbottom.png";
this.trackEndLength = 18; // 高さ
this.thumbStartPath = "AppleClasses/Images/scroll_thumb_vtop.png";
this.thumbStartLength = 9; // 高さ
this.thumbMiddlePath = "AppleClasses/Images/scroll_thumb_vmid.png";
this.thumbEndPath = "AppleClasses/Images/scroll_thumb_vbottom.png";
this.thumbEndLength = 9; // 高さ
/* 内部オブジェクト */
this._track = null;
// _init 内で設定される
this._thumb = null;
// _init 内で設定される
/* 寸法 */
// これらは refresh() の間に設定される必要があるだけ
this._trackOffset = 0;
this._trackLength = 0;
this._numScrollablePixels = 0;
this._thumbLength = 0;
this._repeatType = "repeat-y";
// 背景の繰り返しタイプ
// 内容がスクロールされた時、これらが変更される
this._thumbStart = this.padding;
// JavaScript のイベントハンドラのため
var _self = this;
// このインスタンスを指すプライベート変数
this._captureEventHandler = function(event) { _self._captureEvent(event); };
this._mousedownThumbHandler = function(event) { _self._mousedownThumb(event); };
this._mousemoveThumbHandler = function(event) { _self._mousemoveThumb(event); };
this._mouseupThumbHandler = function(event) { _self._mouseupThumb(event); };
this._mousedownTrackHandler = function(event) { _self._mousedownTrack(event); };
this._mousemoveTrackHandler = function(event) { _self._mousemoveTrack(event); };
this._mouseoverTrackHandler = function(event) { _self._mouseoverTrack(event); };
this._mouseoutTrackHandler = function(event) { _self._mouseoutTrack(event); };
this._mouseupTrackHandler = function(event) { _self._mouseupTrack(event); };
this._init();
}
// AppleScrollbar から継承
AppleVerticalScrollbar.prototype = new AppleScrollbar(null);
まず、変数が設定されます。各部の画像パスとサイズも設定されていますが、後で設定メソッドを説明するので、くわしくはそれを見てください。システム内ではなく、このフォルダ内の画像を指すように修正済みであることに注意してください。
それから、イベントハンドラで利用するためのプライベート変数と特権メソッドを定義しています。これらのハンドラについては後で説明します。さて、最後に _init が呼ばれています。これは上位クラスで定義されたものです。
AppleScrollbar.js > AppleScrollbar._init
/*
* _init() メンバ関数
* スクロールバーの初期化
* 前: this.scrollbar
* 後: this._thumb, this._track + イベントハンドラ
*/
AppleScrollbar.prototype._init = function()
{
var style = null;
var element = null;
// スクロールバーのトラック
this._track = document.createElement("div");
style = this._track.style;
// 収容する div を満たす
style.height = "100%";
style.width = "100%";
this.scrollbar.appendChild(this._track);
// スクロールバーのトラックの一番上
element = document.createElement("div");
element.style.position = "absolute";
this._setObjectStart(element, 0);
this._track.appendChild(element);
// スクロールバーのトラックの中間
element = document.createElement("div");
element.style.position = "absolute";
this._track.appendChild(element);
// スクロールバーのトラックの一番下
element = document.createElement("div");
element.style.position = "absolute";
this._setObjectEnd(element, 0);
this._track.appendChild(element);
// スクロールバーのサム
this._thumb = document.createElement("div");
style = this._thumb.style;
style.position = "absolute";
style.height = this.minThumbSize + "px"; // デフォルトの高さ
this._track.appendChild(this._thumb);
// スクロールバーのサムの一番上
element = document.createElement("div");
element.style.position = "absolute";
this._setObjectStart(element, 0);
this._thumb.appendChild(element);
// スクロールバーのサムの中間
element = document.createElement("div");
element.style.position = "absolute";
this._thumb.appendChild(element);
// スクロールバーのサムの一番下
element = document.createElement("div");
element.style.position = "absolute";
this._setObjectEnd(element, 0);
this._thumb.appendChild(element);
// スタイルのセットアップ
this.setSize(this.size);
this.setTrackStart(this.trackStartPath, this.trackStartLength);
this.setTrackMiddle(this.trackMiddlePath);
this.setTrackEnd(this.trackEndPath, this.trackEndLength);
this.setThumbStart(this.thumbStartPath, this.thumbStartLength);
this.setThumbMiddle(this.thumbMiddlePath);
this.setThumbEnd(this.thumbEndPath, this.thumbEndLength);
// 更新するまでサムを隠す
this._thumb.style.display = "none";
// イベント監視を追加
this._track.addEventListener("mousedown", this._mousedownTrackHandler, false);
this._thumb.addEventListener("mousedown", this._mousedownThumbHandler, false);
// スクロール領域がかわりに更新を起動してくれることになる
}
まずトラック部分を作成しています。_track にトラック要素を作成・保存し、それをこの要素の子要素にしています。それから上・中・下各部を作成し、それらをトラック要素の子要素にしています。同様に、_thumb にサム要素を作成・保存し、子要素にして、上・中・下各部を作成し、それらをサム要素の子要素にします。ここで、_setObjectStart などがこのオブジェクトのメソッドとして定義されていないことに注意してください。これらは方向が関係するので、サブクラスのメソッドとしてだけ実装されています。すなわち、このオブジェクトだけでは動作せず、抽象クラスになっていることがわかります。
AppleScrollbar.js > AppleVerticalScrollbar._setObjectStart
AppleVerticalScrollbar.prototype._setObjectStart = function(object, start)
{ object.style.top = start + "px"; }
このようにスタイルを設定しているだけです。AppleHorizontalScrollbar の場合 top のかわりに left になります。_setObjectStart は、bottom あるいは right になるだけです。
それからスタイルのセットアップが始まります。setSize は AppleScrollbar のメソッドです。
AppleScrollbar.js > AppleScrollbar.setSize
AppleScrollbar.prototype.setSize = function(size)
{
this.size = size;
this._setObjectSize(this.scrollbar, size);
this._setObjectSize(this._track.children[1], size);
this._setObjectSize(this._thumb.children[1], size);
}
ここでも似たような構造が使われています。サイズを設定した後で、サブクラスで実装されているメソッドを使ってトラックやサムのサイズを設定します。
AppleScrollbar.js > AppleVerticalScrollbar._setObjectSize
AppleVerticalScrollbar.prototype._setObjectSize = function(object, size)
{ object.style.width = size + "px"; }
AppleHorizontalScrollbar の場合 width が height になるだけです。
それから、トラックとサムの開始・中間・終了部を設定しています。呼び出しに使われている trackStartPath のような画像パスや、trackStartLength のような変数は、先に見たサブクラスのコンストラクタ内で適切に設定されています。_init はサブクラスのコンストラクタの最後で呼ばれるので、これらの変数はすでに設定済みです。
AppleScrollbar.js > AppleScrollbar.setTrackStart
AppleScrollbar.prototype.setTrackStart = function(imgpath, length)
{
this.trackStartPath = imgpath;
this.trackStartLength = length;
var element = this._track.children[0];
element.style.background = "url(" + imgpath + ") no-repeat top left";
this._setObjectLength(element, length);
this._setObjectSize(element, this.size);
this._setObjectStart(this._track.children[1], length);
}
このように画像パスと長さを設定した後で、背景画像としてそれを設定し、要素のサイズを設定しています。setTrackMiddle、setTrackEnd、setThumbStart、setThumbMiddle、setThumbEnd でも同様のことが行われています。これら 6 つのメソッドは、独自の画像を設定したい時にも使うことができます。その場合、そうした画像へのパスと、サイズを指定してやります。トラック、サムそれぞれに対して一番上の画像、中間の画像、一番下の画像を用意すればいいことがわかるでしょう。このような形になっているのは、トラックのサイズが可変で、サムも全体量に対する表示部分の比によってサイズが可変になるからです。images フォルダ内の画像を見れば各部があることがわかります(サンプルの images フォルダ内にはスライダー用のものも含まれていることに注意してください。)。
_init メソッドに戻ると、つぎは、まだサムが表示されないようにしています。それからスクロールバー内でクリックが行われた時に対応できるよう、イベント監視を追加しています。これで _init メソッドが理解できました。
8.2 イベントハンドラ
最後に追加されたイベントハンドラを見てみましょう。これはサブクラスのコンストラクタ内で特権メソッドとして設定されたもので、_mousedownTrackHandler なら _mousedownTrack が呼ばれることになります。
AppleScrollbar.js > AppleScrollbar._mousedownTrack
AppleScrollbar.prototype._mousedownTrack = function(event)
{
this._track_mouse_temp = this._getMousePosition(event) - this._trackOffset;
// 変数にマウス位置からオフセットを引いたものを代入
if (event.altKey)
// オプションキーが押されていたら
{
// クリックされた位置へと移動
this.scrollTo(this._contentPositionForThumbPosition
(this._track_mouse_temp - (this._thumbLength / 2)));
delete this._track_mouse_temp;
}
else
{
this._track_scrolling = true;
// 追跡中に設定
// 一時的なイベントハンドラ
this._track.addEventListener("mousemove", this._mousemoveTrackHandler, true);
this._track.addEventListener("mouseover", this._mouseoverTrackHandler, true);
this._track.addEventListener("mouseout", this._mouseoutTrackHandler, true);
document.addEventListener("mouseup", this._mouseupTrackHandler, true);
this._trackScrollOnePage(this);
// とりあえず1回分スクロール
this._track_timer = setInterval(this._trackScrollDelay, 500, this);
}
event.stopPropagation();
event.preventDefault();
}
まずトラック内でのクリック位置を保存します。それからオプションキーが押されているかどうかを調べます。ウィジェットを調べればわかるように、トラック内でのクリックは通常、1ページ分スクロールされて、そのままドラッグすれば、その分スクロールされていきます。オプションを押せば、その位置まで一気にスクロールされますが、ドラッグしても反応しません。ここでの実装を見れば、その様子がわかります。
オプションを押していた場合は、その位置を計算してそこまでスクロールします。イベントハンドラ等が設定されるわけではなく、そのままおしまいです。
オプションを押していなければ、ドラッグの場合に対応できるよう、イベントハンドラが設定されています。そのままボタンが離された場合はアップのハンドラが呼ばれるので大丈夫です。それから1回分スクロールして、なめらかにスクロールを表示するためのタイマーを準備しています。それからイベントが伝わるのを防ぎます。
ここで使われているメソッドをまず調べ、それから一時的なイベントハンドラを調べてみましょう。まず最初にマウス位置を取得するために使われている _getMousePosition メソッドです。これは垂直・水平の各サブクラスで実装されています。
AppleScrollbar.js > AppleVerticalScrollbar._getMousePosition
AppleVerticalScrollbar.prototype._getMousePosition = function(event)
{
if (event != undefined)
return event.y;
else
return 0;
}
垂直スクロールバーなので垂直位置だけを返します。水平では x 位置だけが返されることになります。つぎは、オプションクリックの場合に使われている _contentPositionForThumbPosition です。これは AppleScrollbar で定義されています。
AppleScrollbar.js > AppleScrollbar._contentPositionForThumbPosition
AppleScrollbar.prototype._contentPositionForThumbPosition = function(thumb_pos)
{
// 現在全ての内容を表示しているなら、ビュー外部にしたくない
if (this._getViewToContentRatio() >= 1.0)
{
return 0;
}
else
{
return (thumb_pos - this.padding)
* ((this._getContentLength() - this._getViewLength())
/ this._numScrollablePixels);
}
}
まず、ビューの内容比を調べて、内容全体が収まっているなら、最初の位置である 0 を返しています。そうでなければ、渡された位置 thumb_pos に対する内容の位置を計算しています。このメソッドと逆なのが、_thumbPositionForContentPosition で、すぐにわかると思います。_getContentLength などはアクセサメソッドで、垂直・水平で異なる部分の量を返すためにアクセサになっています。つぎに1ページ分のスクロールを見てみますが、その前に、setInterval で 500(すなわち、500/1000 = 1/2 秒)で呼ばれるように設定された _trackScrollDelay を見てみます。
AppleScrollbar.js > AppleScrollbar._trackScrollDelay
AppleScrollbar.prototype._trackScrollDelay = function(self)
{
if (!self._track_scrolling) return;
clearInterval(self._track_timer);
self._trackScrollOnePage(self);
self._track_timer = setInterval(self._trackScrollOnePage, 150, self);
}
スクロール中でなければ戻ります。それから設定されたタイマーを解除し、1ページ分動かし、さらに 150/1000 秒ごとに _trackScrollOnePage を行うように設定しています。このメソッドにより、クリック後のドラッグ開始が少し遅れます。微妙ですが 1/2 秒なので、1 回分スクロールした後で少し休止してるのがわかると思います。_trackScrollOnePage を見てみましょう。
AppleScrollbar.js > AppleScrollbar._trackScrollOnePage
AppleScrollbar.prototype._trackScrollOnePage = function(self)
{
// これは setInterval から呼ばれる、そのため this に対するポインタが必要
if (!self._track_scrolling) return;
var deltaScroll = Math.round(self._trackLength * self._getViewToContentRatio());
if (self._track_mouse_temp < self._thumbStart)
self.scrollByThumbDelta(-deltaScroll);
else if (self._track_mouse_temp > (self._thumbStart + self._thumbLength))
self.scrollByThumbDelta(deltaScroll);
}
スクロール中でなければ戻ります。それからスクロールの移動量を出し、scrollByThumbDelta によってスクロールを行っています。
AppleScrollbar.js > AppleScrollbar.scrollByThumbDelta
AppleScrollbar.prototype.scrollByThumbDelta = function(deltaScroll)
{
if (deltaScroll == 0)
return;
this.scrollTo(this._contentPositionForThumbPosition(this._thumbStart + deltaScroll));
}
基本的に scrollTo によって、指定した位置までスクロールしているだけです。この scrollTo は、オプションクリックの場合も出てきました。このメソッドはサブクラス内で実装されています。
AppleScrollbar.js > AppleVerticalScrollbar.scrollTo
AppleVerticalScrollbar.prototype.scrollTo = function(pos)
{
this.scrollarea.verticalScrollTo(pos);
}
このように、垂直と水平でスクロール方向が異なるため、サブクラスで適切な方向に対してスクロール領域をスクロールしています。使われているメソッドは、先ほどのスクロール領域で説明したのでわかると思います。こうして、タイマーが設定され、ドラッグ中は連続的にスクロールメソッドが呼ばれることになります。
残りのイベントハンドラを見てみましょう。クリックした時に何が起こるかわかったので、ドラッグ中について調べてみます。_mouseoverTrackHandler は特権メソッドで結果的に _mousemoveTrack が呼ばれることになります。
AppleScrollbar.js > AppleScrollbar._mousemoveTrack
AppleScrollbar.prototype._mousemoveTrack = function(event)
{
this._track_mouse_temp = this._getMousePosition(event) - this._trackOffset;
event.stopPropagation();
event.preventDefault();
}
すぐわかるように、一時的なマウス位置を設定し、その後イベントの伝播を防いでいるだけです。スクロール自体は、タイマーで定期的に呼ばれるメソッド内で行われていたので、ここで設定された最後の位置が _trackScrollOnePage で使われることになります。
つぎはボタンが離された場合です。_mouseupTrackHandler は特権メソッドで結果的に _mouseupTrack が呼ばれることになります。
AppleScrollbar.js > AppleScrollbar._mouseupTrack
AppleScrollbar.prototype._mouseupTrack = function(event)
{
clearInterval(this._track_timer);
// 一時的なイベントハンドラを除去
this._track.removeEventListener("mousemove", this._mousemoveTrackHandler, true);
this._track.removeEventListener("mouseover", this._mouseoverTrackHandler, true);
this._track.removeEventListener("mouseout", this._mouseoutTrackHandler, true);
document.removeEventListener("mouseup", this._mouseupTrackHandler, true);
// 一時的なプロパティを除去
delete this._track_mouse_temp;
delete this._track_scrolling;
delete this._track_timer;
event.stopPropagation();
event.preventDefault();
}
非常に簡単です。一時的な変数やハンドラを片付けて、イベントの伝播を防いでいるだけです。
_mouseoutTrackHandler も特権メソッドで、_mouseoutTrack が呼ばれます。
AppleScrollbar.js > AppleScrollbar._mouseoutTrack
AppleScrollbar.prototype._mouseoutTrack = function(event)
{
this._track_scrolling = false;
event.stopPropagation();
event.preventDefault();
}
スクロール中でないようにして、スクロールを停止します。実際にウィジェットで操作してみれば効果を確認できます。ボタンを押したままトラックの外にポインタを移動させると、スクロールが停止します。ボタンを押したままトラック内に戻せばスクロールは再開されます。これは over イベントハンドラでスクロール中であるように設定しているからです。_mouseoverTrackHandler は特権メソッドで結果的に _mouseoverTrack が呼ばれることになります。
AppleScrollbar.js > AppleScrollbar._mouseoverTrack
AppleScrollbar.prototype._mouseoverTrack = function(event)
{
this._track_mouse_temp = this._getMousePosition(event) - this._trackOffset;
this._track_scrolling = true;
event.stopPropagation();
event.preventDefault();
}
これでイベントハンドラはだいたい理解できたと思います。トラックとは別にサムのイベントハンドラもほぼ似たような動作を行っていますが、マウスが上にある場合などのイベントハンドラにイベントの伝播を防くだけのメソッドを使っているところが違います。ここでの説明を参考にすれば容易に理解できると思います。
8.3 その他のメソッド
他にさまざまなメソッドがありますが、refresh 以外は十分わかると思います。
AppleScrollbar.js > AppleScrollbar.refresh
/*
* refresh()
* スクロールバーサイズとサム位置を更新
*/
AppleScrollbar.prototype.refresh = function()
{
this._trackOffset = this._computeTrackOffset();
this._trackLength = this._computeTrackLength();
var ratio = this._getViewToContentRatio();
if (ratio >= 1.0 || !this._canScroll())
// 内容が全部表示
{
if (this.autohide)
// 自動で隠す設定なら
{
// すべての内容か見えている、スクロールバーを隠す
this.hide();
}
// サムを隠す
this._thumb.style.display = "none";
this.scrollbar.style.appleDashboardRegion = "none";
}
else
// スクロールが必要なら
{
this._thumbLength = Math.max(Math.round(this._trackLength * ratio), this.minThumbSize);
this._numScrollablePixels = this._trackLength - this._thumbLength - (2 * this.padding);
this._setObjectLength(this._thumb, this._thumbLength);
// サムを表示
this._thumb.style.display = "block";
this.scrollbar.style.appleDashboardRegion = "dashboard-region(control rectangle)";
this.show();
}
// 位置が適切に更新されたことを確認
this.verticalHasScrolled();
this.horizontalHasScrolled();
}
ちょっと長いですが、構造は簡単です。まず内容が収まっているかどうかを調べ、自動設定ならトラックを隠し、そうでなくてもサムを隠します。内容が表示サイズより大きいなら、位置を計算しなおしてサムを表示しています。verticalHasScrolled と horizontalHasScrolled は、サブクラスでどちらか一方だけが実装され、もう一方は空になっています。自身がスクロールする方向だけ実装しているわけです。そのなかでは、スクロール位置にもとづいて他の変数を計算しなおしています。
まだ説明していないメソッドはありますが、簡単なのでソースを見れば十分わかると思います。たとえば、show や hide ではスタイル属性を修正して、表示したり隠したりしているだけです。
9 その後の修正
9.1 Mac OS X v10.4.11
Mac OS X v10.4.11 において、スクロール関連 Apple クラスが修正されています。そのため、ここでの解説で使っているものと少し違っています。変更点を調べてみます。これはもっと前のバージョンで修正されているのだと思いますが、正確にどのバージョンからかは不明です。
AppleScrollArea.js では、removeScrollbar、removeEventListener、refresh、verticalScrollTo、horizontalScrollTo で for 文が修正されています。ここでは、removeScrollbar で代表させてますが、他もスクロールバーの繰り返し部分で全く同じです。
サンプルの AppleScrollArea.js > removeScrollbar
AppleScrollArea.prototype.removeScrollbar = function(scrollbar)
{
...
for (i in scrollbars)
{
...
}
}
10.4.11 の AppleScrollArea.js > removeScrollbar
AppleScrollArea.prototype.removeScrollbar = function(scrollbar)
{
...
var c = scrollbars.length;
for (var i = 0; i < c; ++i)
{
...
}
}
AppleScrollbar.js では、_init メソッドが 1 か所だけ変更されています。
サンプルの AppleScrollbar.js > _init
AppleScrollbar.prototype._init = function()
{
...
// Scrollbar のサム
...
style.height = this.minThumbSize + "px"; // default height
...
}
10.4.11 の AppleScrollbar.js > _init
AppleScrollbar.prototype._init = function()
{
...
// Scrollbar のサム
...
this._setObjectSize(this._thumb, this.minThumbSize); // default size
...
}
古い方では、直接 height をいじっていますが、新しい方では、サブクラスで実装されている _setObjectSize を使って、height または width の選択をサブクラスにまかせています。
9.2 Mac OS X v10.5.1
AppleScrollArea.js で修正が行われていますが、if 文の {} を追加して else 以降との関係をはっきりさせているだけです。
AppleScrollbar.js では、_thumbPositionForContentPosition 内で修正が行われています。
10.4.11 の AppleScrollbar.js > _thumbPositionForContentPosition
AppleScrollbar.prototype._thumbPositionForContentPosition = function(page_pos)
{
...
else
{
return this.padding -
-(page_pos / ((this._getContentLength() - this._getViewLength())
/ this._numScrollablePixels));
}
...
}
10.5.1 の AppleScrollbar.js > _thumbPositionForContentPosition
AppleScrollbar.prototype._thumbPositionForContentPosition = function(page_pos)
{
...
else
{
var result = this.padding -
-(page_pos / ((this._getContentLength() - this._getViewLength())
/ this._numScrollablePixels));
if (isNaN(result))
result = 0;
return result;
}
...
}
すべての内容を表示しているわけではない場合が修正されています。古い方は計算結果を直接返していますが、新しい方は結果を変数に格納し、割算などのエラーで数値以外が返された場合、結果を 0 に修正して返しています。
管理人:神吉 秀典 E-mail: