[an error occurred while processing this directive]
[an error occurred while processing this directive]
(5) 一覧表示モードを追加する
前回は場所が変わるときにふわっとなめらかに変わるようにしました。
今回は、新たに全部の写真が並んだ、一覧表示モードを追加してみます。
一見いままでのプログラムと同じ事をまた書かないといけないようですが、案外少しの追加で済みます。
前回 から変わったのは、以下の点です。
- 一覧表示モードが出来たため、その位置を表す ListPosition クラスが増えた
- モードを切り替えるためのボタンが増えた (左下) ので、それをあらわす Button クラスが増えた
Env: 色々な定数を入れるクラス
モード切替のボタンが増えたので、ボタンサイズを表す変数が増えました。
class Env {
public static int W, H, BTN_W;
public static int T_MAX;
public static int REFRESH;
public static double WH_RATIO;
public static String URL, BACKGROUND_URL;
/** 各種パラメータのデフォルト値を設定 */
static {
W = 800;
H = 600;
BTN_W = 30;
T_MAX = 20;
WH_RATIO = 1.333333;
REFRESH = 15;
URL = "http://funini.com/kei/ivy/narita/";
}
}
Ivylet: メインのアプレットのクラス
これは変化ありません。
public class Ivylet extends JApplet {
public void init() {
String[] imgPaths = extractPaths(Env.URL);
IvyPanel tp = new IvyPanel(imgPaths);
setContentPane(tp);
new Timer(Env.REFRESH, tp).start();
}
/** 指定されたパスから、画像の URL 一覧を取得 */
public String[] extractPaths(String urlStr) {
// urlBase には、URLの / 以前の部分が入る ( http://funini.com/kei/ など)
String urlBase = urlStr.substring(0, urlStr.lastIndexOf("/") + 1);
ArrayList files = new ArrayList();
try {
BufferedReader br = new BufferedReader(new InputStreamReader(
new URL(urlStr).openConnection().getInputStream()));
extractFiles(files, urlBase, br);
} catch (IOException ie) {
ie.printStackTrace();
}
return (String[]) files.toArray(new String[0]);
}
/** br から全体を読み込み、画像の URL 一覧を取得 */
void extractFiles(ArrayList files, String urlBase, BufferedReader br) throws IOException{
Pattern pattern = Pattern.compile("^(.+\\.(jpg|JPG))<.*");
for(;;) {
String line = br.readLine();
if(line == null) break;
String[] targets = line.split(">");
for(int i = 0; i < targets.length; i++) {
Matcher m = pattern.matcher(targets[i]);
if(m.matches()) files.add(urlBase + m.group(1));
}
}
}
}
IvyPanel: メインのパネル
ボタンが増えたことので、処理が増えています。あと、listMode という変数が増えました。これは一覧表示のとき true 、重ね表示のとき false です。
一番のポイントはクリック部分です。このボタンは、別にクリックされたら自動的にメソッドが呼ばれる…なんて機能はないので、
パネルでクリックイベントを扱っている mousePressed() の中でボタンがクリックされているか調べ、それに応じて処理もします。
ここでは、listMode を反転させ、initView() で表示をモードにあわせて変えています。
class IvyPanel extends JPanel implements ActionListener, MouseListener {
Button button; // ボタンです。クリックすると表示モードが変わります
IconList iconList; // 各画像の重ね順です。
boolean listmode; // 表示モードです。「一覧表示(true)」と「重ね表示(false)」があります
public IvyPanel(String[] urls) {
/** コンストラクタ 引数は画像が入っているパス */
setBackground(new Color(0x33, 0x33, 0x33)); // 背景色設定
setPreferredSize(new Dimension(Env.W, Env.H)); // サイズ設定
button = new Button(0, Env.H - Env.BTN_W, 210, Env.BTN_W); // ボタンを配置
iconList = new IconList(urls);
listmode = false;
addMouseListener(this); // マウスのイベントを扱えるようにする
}
public synchronized void mousePressed(MouseEvent e) {
/** マウスがクリックされたときのイベント */
Point p = e.getPoint();
if(button.hit(p)) { // ボタンがクリックされた場合
listmode = !listmode; // 一覧表示 <=> 重ね表示を切り替え
initView(); // 初期表示
return;
}
// ボタン以外がクリックされた時
for(int i = iconList.size() - 1; i >= 0; i--) { // クリックは表示と逆順で調べる
Icon icon = iconList.get(i); // i 番目のアイコンについて
if(icon.hit(p)){ // クリックされている場合
if(listmode)
iconList.listTop(icon); // icon をトップに表示 (一覧表示)
else
iconList.piledTop(icon); // icon をトップに表示 (重ね表示)
return;
}
}
// ボタンもアイコンもクリックされていない場合、初期表示に戻る
initView();
}
void initView() {
/** 初期表示を行う */
if(listmode) iconList.list(); // 一覧表示
else iconList.pile(); // 重ね表示
}
/** タイマイベントが発生したら、時間を進める */
public void actionPerformed(ActionEvent ae)
{ addTime(); }
/** 時間を進める (何度も呼ばれると困るから、synchronized にしました) */
synchronized void addTime() {
for(int i = 0; i < iconList.size(); i++) // 全てのアイコンについて
iconList.get(i).addTime(); // 時間を進める
repaint(); // 再描画
}
/** 描画を行う */
public void paintComponent(Graphics g) {
super.paintComponent(g); // ウィンドウを表示
Graphics2D g2 = (Graphics2D) g; // アンチエイリアスの設定など
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
button.draw(g2); // ボタンを表示
for(int i = 0; i < iconList.size(); i++)
iconList.get(i).draw(g2); // 各アイコンを表示
}
/** MouseListener を実装するために必要なメソッド群 */
public void mouseReleased(MouseEvent e) {}
public void mouseEntered(MouseEvent e) {}
public void mouseExited(MouseEvent e) {}
public void mouseClicked(MouseEvent e) {}
}
IconList: アイコンのリスト、重ね順、位置リストなどを保持するクラス
ListPosition という新しいクラスが増えました。これは、アイコンを一覧表示したときの位置を保持しているクラスです。
表示が今までの重ね表示だけのに比べて、一覧表示にアイコンを移動するメソッドが増えています。
class IconList {
int nIcons; // アイコンの数
Icon[] icons; // アイコン (画像1枚につき、一つのアイコンです)
int[] orders;
ListPosition listPos; // 一覧表示した時の各画像の位置
PiledPosition piledPos; // 重ねた表示の時の各画像の位置
public IconList(String[] imgPaths) {
/** 引数: ロードされた画像リスト */
nIcons = imgPaths.length;
icons = new Icon[nIcons];
for(int i = 0; i < nIcons; i++)
icons[i] = new Icon(imgPaths[i]);
orders = new int[nIcons];
piledPos = new PiledPosition(nIcons);// 一覧表示での位置を計算
listPos = new ListPosition(nIcons); // 重ね表示での位置を計算
pile(); // 初期表示(重ね表示)に移行
}
/** 一覧表示にアイコンを移動 */
public void list() {
for(int i = 0; i < nIcons; i++) {
orders[i] = nIcons - i - 1; // 順番に重ねる
icons[i].setDestination(listPos.getPos(i)); // アイコンの目的地をセットする
}
}
/** 重ね表示にアイコンを移動 */
public void pile() {
for(int i = 0; i < nIcons; i++) {
// 重ね順を設定。orders は描画順なので、一番新しい画像を最後にする
orders[i] = nIcons - i - 1;
icons[i].setDestination(piledPos.getPos(i));
}
}
/** アイコンのインデックス(何番目のアイコンか)を返す */
int searchIcon(Icon ic) {
for(int i = 0; i < nIcons; i++)
if(icons[i] == ic) return i;
return -1; // 見つからなかった場合
}
/** 指定されたインデックスが何番目に重なっているかを返す */
int searchOrder(int index) {
for(int i = 0; i < nIcons; i++)
if(orders[i] == index) return i;
return -1; // 見つからなかった場合
}
/** 一覧表示において、指定されたアイコンをトップに移動 */
public void listTop(Icon ic) {
int index = searchIcon(ic);
if(index != orders[nIcons - 1])
icons[orders[nIcons - 1]].setDestination(listPos.getPos(orders[nIcons - 1]));
icons[index].setDestination(listPos.getZoomedPos(index));
for(int i = searchOrder(index) + 1; i < nIcons; i++)
orders[i - 1] = orders[i];
orders[nIcons - 1] = index;
}
/** 重ね表示において、指定されたアイコンをトップに移動 */
public void piledTop(Icon ic) {
int index = searchIcon(ic);
// 指定されたアイコンより手前のものは、画面外に移動
for(int i = 0; i < index; i++)
icons[i].setDestination(piledPos.getOuterPos());
// 指定されたアイコン以後のものを手前から順番に並べる
for(int i = index; i < nIcons; i++)
icons[i].setDestination(piledPos.getPos(i - index));
}
/** アイコンの数を返すメソッド */
public int size() { return nIcons; }
/** 重ね順が(下から) i 番目のアイコンを返すメソッド */
public Icon get(int i){ return icons[orders[i]]; }
}
IconPosition: アイコンの位置を持つクラス
abstract class IconPosition {
/** 各アイコンの配置を示すクラス */
int[] X, Y; // アイコンの X, Y, W, H
int w, h;
public IconPosition(int n) {
/** 各アイコンの座標、大きさ */
X = new int[n];
Y = new int[n];
}
/** 指定されたインデックスのアイコン位置を返す */
public int[] getPos(int i)
{ return new int[] { X[i], Y[i], w, h }; }
/** 指定された幅/高さ 最大値を超えないように、幅・高さを設定する */
public void setWH(int wMax, int hMax) {
w = wMax; // アイコンの(最大)幅
h = (int) ((double) w / Env.WH_RATIO); // アイコンの(最大)高さ
if(h > hMax) { // 幅を基準にしたら、高さがhMaxをはみ出てしまった場合
h = hMax;
w = (int) (h * Env.WH_RATIO);
}
}
}
PiledPosition: 重ね表示 の時の位置情報を持つクラス
変化なしです。
class PiledPosition extends IconPosition {
/** 重ね表示 の時の位置情報を持つクラス */
int zw, zh;
int outerX, outerY;
/** コンストラクタ (引数: 画像の数) */
public PiledPosition(int n) {
super(n);
setWH(Env.W / 2, Env.H / 2);
X[0] = Env.W / 2 - Env.W / 10; // 一番手前のアイコンのX座標
Y[0] = Env.H / 2 - Env.H / 10; // 一番手前のアイコンのY座標
for(int i = 1; i < n; i++) {
X[i] = X[i - 1] - (int) ((10 - i) * (10 - i) * 0.9);
Y[i] = Y[i - 1] - (int) ((10 - i) * (10 - i) * 0.4 + 20);
}
outerX = Env.W;
outerY = Env.H;
}
/** フォーカスアウトして、画面からはみ出た位置を返す */
public int[] getOuterPos()
{ return new int[] { outerX, outerY, w, h }; }
}
[New] ListPosition: 一覧表示 の時の位置情報を持つクラス
タイルみたいに、今ある画像を敷き詰める表示位置です。全て新しく増えました。
コンストラクタで少しややこしいことをしているのは、縦・横に何枚の絵を配置するかを計算しているからです。
画面の縦と横のサイズは Env.W と Env.H で分かります。あと、画像の縦横はEnv.WH_RATIO で分かるから、
あとは適当に計算して、nw が横に並ぶ画像数、 nh が縦に並ぶ画像数です。
setWH() は、幅と高さを計算するための関数です。引数に幅と高さを取ってるんだけど、これは取り得る最大の幅・高さです。
それに対し、画像は元々の縦横比を維持しないといけないから、どちらかが最大値より小さくなります。
これを計算させているのがsetWH() です。
あと、getZoomedPos() は、画像がズームされた後の位置を返しています。
class ListPosition extends IconPosition {
int[] ZX, ZY; // ズームした後の位置
int zw, zh; // ズームした後の最大幅、高さ
public ListPosition(int n) {
super(n);
// 幅と高さを決める
int availableH = Env.H - Env.BTN_W;
double r = Math.sqrt(Env.W * availableH * Env.WH_RATIO / n);
int nw = (int) Math.round(Env.W / r); // 横に並ぶ画像数
int nh = (int) Math.ceil((double) n / nw);
setWH((int)(0.93 * Env.W / nw),(int)(0.93 * Env.H / nh));
// 一覧表示の X 座標、Y 座標を決める
int marginX = (Env.W - (nw * w)) / (nw + 1);
int marginY = (availableH - (nh * h)) / (nh + 1);
int index = 0;
outer:for(int cntY = marginY; cntY < availableH; cntY += marginY + h){
for(int cntX = marginX; cntX < Env.W; cntX += marginX + w){
X[index] = cntX;
Y[index] = cntY;
if(++index == n) break outer;
}
}
// ズーム時の位置を計算する
calcZ(n);
}
/** ズーム後の X 座標、Y 座標を決める。
* もし画面からはみ出るときは、適宜調整する */
void calcZ(int n){
ZX = new int[n];
ZY = new int[n];
zw = Env.W / 2; // ズーム後の幅
zh = (int) (zw / Env.WH_RATIO);
for(int index = 0; index < n; index++){
ZX[index] = limitTopBottom(0, X[index] - (zw - w) / 2, Env.W - zw);
ZY[index] = limitTopBottom(0, Y[index] - (zh - h) / 2, Env.H - zh);
}
}
/** v が v_min 未満なら v_mim、v_max 以上なら v_max、それ以外は v を返す */
int limitTopBottom(int vMin, int v, int vMax)
{ return (vMin >= v) ? vMin : (vMax <= v) ? vMax : v; }
/** ズームされた後の位置を返す */
public int[] getZoomedPos(int i)
{ return new int[] { ZX[i], ZY[i], zw, zh }; }
}
Rect: パネルに置く色んな部品の基本クラス
変化無しです。
abstract class Rect {
int[] P; // X,Y座標, 幅、高さ
int t; // 経過時間 (0から Env.E_MAX まで)
double state; // 経過時間を0.0 - 1.0の割合にしたもの。
//ふわっとした感じはこの state の更新がポイント
Image img;
int imgW, imgH; // 画像の幅、高さ
Color col;
public Rect(int x, int y, int w, int h) {
P = new int[] { x, y, w, h };
animationEnd();
}
/** 指定された座標 p が、この長方形の内部かどうかを返す */
public boolean hit(Point p) {
return (P[0] <= p.x && p.x <= P[0] + P[2]
&& P[1] <= p.y && p.y <= P[1] + P[3]);
}
/** アニメーションを開始状態にする */
public void animationStart() { t = 0; state = 0.0; }
/** アニメーションを終了状態にする */
public void animationEnd() { t = Env.T_MAX; state = 1.0; }
/** 時間を進める */
public void addTime() {
if(t >= Env.T_MAX) return;
t++;
state = 1 - Math.pow(((double) t / Env.T_MAX - 1), 2);
}
/** 画像をロード。path がhttp:// で始まる場合、HTTPで取得する */
public void loadImage(String path){
img = ImageLoader.load(path);
imgW = img.getWidth(null);
imgH = img.getHeight(null);
}
}
Icon: アイコンクラス
変化なしです。
class Icon extends Rect {
int[] dest, orig; // 移動先と移動元のx, y, w, hを持つ配列
public Icon(String imgPath) {
super(0, 0, 0, 0);
loadImage(imgPath);
col = new Color(0.85f, 0.85f, 0.75f, 0.3f);
dest = new int[] { 0, 0, 0, 0 };
orig = new int[] { 0, 0, 0, 0 };
}
/** アニメーションの目的地を設定 */
public void setDestination(int[] A) {
for(int i = 0; i < 4; i++) {
orig[i] = P[i];
dest[i] = A[i];
}
animationStart(); // 時間変数 t,state を初期化
}
/** 時間を進め、state に応じて、このオブジェクトを移動させる。
* 移動元は orig, 移動先は dest。*/
public void addTime() {
super.addTime();
for(int i = 0; i < 4; i++)
P[i] = (int) (orig[i] * (1 - state) + dest[i] * state);
}
/** アイコンを描画する */
void draw(Graphics2D g) {
// まずは、画像が半分以上画面外にはみ出ている場合は描画しない
if(P[0] + P[2] / 2 < 0 || P[1] + P[3] / 2 < 0
|| Env.W <= P[0] || Env.H <= P[1]) return;
g.setColor(col);
// アイコンの周りの、少し透明な枠を描画する
g.fillRect(P[0], P[1], P[2], P[3]);
// 少し余白をあけて、アイコンを描画する
int marginX = P[2] / 30;
int marginY = P[3] / 30;
g.drawImage(img, P[0] + marginX, P[1] + marginY, P[0] + P[2] - marginX,
P[1] + P[3] - marginY, 0, 0, imgW, imgH, null);
}
}
[New] Button: 表示モードを切り替えるためのボタン
新しく増えました。ボタンといっても、クリックしたときの動作は IvyPanelに書いてあるので、こっちは描画だけです。
もっと凝ったこともできるかもだけど、今回は黒い四角を書いてるだけです。
class Button extends Rect {
Font font;
public Button(int x, int y, int w, int h) {
super(x, y, w, h);
font = new Font("Century Gothic", Font.PLAIN, 24);
col = new Color(0f, 0f, 0f, 0.5f);
}
/** ボタンを描画する */
public void draw(Graphics2D g) {
g.setColor(col);
g.fillRect(P[0], P[1], P[2], P[3]);
g.setColor(Color.white);
g.setFont(font);
g.drawString("Mode Change", P[0] + 10, P[1] + P[3] - 8);
}
}
ImageLoader: 画像を読み込むためのクラス
前回から変更無しです。
class ImageLoader {
/** 画像をロード。path がhttp:// で始まる場合、HTTPで取得する */
public static Image load(String path) {
try {
if(path.startsWith("http://")){
URLConnection urlc = new URL(path).openConnection();
return ImageIO.read(urlc.getInputStream());
}
return ImageIO.read(new File(path));
} catch (IOException ie) { ie.printStackTrace();}
return null;
}
}
[an error occurred while processing this directive]