アニメーション

repaint()ではまとめられてしまう

●が動いて軌跡が残るプログラムを書いてみます。●●●と右に伸びていき最後には次の様になることを想定します。クリックする度に高さを変化させて右へ動いていくようにする予定です。

moveDisk

ボタンを3つにしたEventRandomRGB.javaを元に作りました。今のところ一番左のものしか使いませんが、あとの2つも生かしておきます。

ファイル名 AnimeDisk1.java

import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;

public class AnimeDisk1 extends JFrame implements ActionListener{
    JButton rbtn;
    JButton gbtn;
    JButton bbtn;
    MyPanel mypnl;
    //コンストラクタ
    public AnimeDisk1() {
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setTitle("Animation Disk");
        mypnl = new MyPanel(400,400);
        rbtn = new JButton("start"); //今回はこれだけ使用
        gbtn = new JButton("green");
        bbtn = new JButton("blue");
        JPanel btnpnl = new JPanel();
        btnpnl.setLayout(new GridLayout(1,3,0,0));
        btnpnl.add(rbtn);
        btnpnl.add(gbtn);
        btnpnl.add(bbtn);
        setLayout(new BorderLayout());
        add(mypnl, BorderLayout.CENTER);
        add(btnpnl,BorderLayout.SOUTH);
        rbtn.addActionListener(this);
        gbtn.addActionListener(this);
        bbtn.addActionListener(this);
        pack();
        setVisible(true);
    }
    //イベント処理
    @Override
    public void actionPerformed(ActionEvent e) {
        if (e.getSource() == rbtn) {
            mypnl.moveDisk();    //●を描くメソッド
        }
        if (e.getSource() == gbtn) {
            mypnl.drawToBuff('g');
        }
        if (e.getSource() == bbtn) {
            mypnl.drawToBuff('b');
        }
    }
    public static void main(String[] args){
        AnimeDisk1 myframe = new AnimeDisk1();
    }
    //内部クラス
    public class MyPanel extends JPanel{
       BufferedImage buffimg;
       Graphics2D bfg;
       Color bgcolor= new Color(255,255,191);  //背景の色
       public MyPanel(int width, int height){
            setPreferredSize(new Dimension(width,height));
            int r=2;
            buffimg = new BufferedImage(
                      width*r,height*r,BufferedImage.TYPE_INT_RGB);
            bfg = buffimg.createGraphics();
            bfg.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 
                                 RenderingHints.VALUE_ANTIALIAS_ON);
            bfg.setColor(bgcolor);
            bfg.fillRect(0, 0, buffimg.getWidth(), buffimg.getHeight());
       }
       @Override
       public void paintComponent(Graphics myg){
           //super.paintComponent(myg);
           int pnlw = getSize().width;
           int imgh = buffimg.getHeight() * pnlw / buffimg.getWidth();
           myg.drawImage(buffimg, 0, 0, pnlw, imgh, this);
           //getSize().widthはMyPanelのインスタンスの幅
       }
       //円を動かすメソッド
       public void moveDisk() {
               int x = buffimg.getWidth()/4;  //100
               int y = (int)(buffimg.getHeight()*Math.random());
               int d = buffimg.getWidth()/40;  //円の大きさ 10
               int dx = buffimg.getWidth()/40; //動きの大きさ 10
               bfg.setColor(Color.red);
               while ( buffimg.getWidth() > x ){
                    bfg.fillOval(x-d/2,y-d/2,d,d);
                    repaint();
                    x+=dx;
                    //100ms(0.1秒)停止
                    try {
                       Thread.sleep(100);
                    }
                    catch(InterruptedException ex) {
                       System.err.println(ex);
                    }
               }
       }
       public void drawToBuff(char rgb){
          for(int i=0; 10>i; i++){
                Color rcolor = randomColor(rgb);
                bfg.setColor(rcolor);
                int x = (int)(buffimg.getWidth()*Math.random());
                int y = (int)(buffimg.getHeight()*Math.random());
                int h = (int)(buffimg.getWidth()*Math.random()/8+buffimg.getWidth()/80);
                bfg.fillOval(x-h/2,y-h/2,h,h);
          }
          repaint();
       }
       public Color randomColor(char rgb){
          int r=0;
          int g=0;
          int b=0;
          int dc=256;
          int x = (int)(dc*Math.random());
          if (x > 255){
             x = 0;
           }
          if (rgb=='r') r=x;
          if (rgb=='g') g=x;
          if (rgb=='b') b=x;
          Color c = new Color(r,g,b);
          return c;
       }
    }
}

アニメーションの主要部分はmypnl.moveDisk()

強調部分が主要な変更です。redボタンがstartになっていて、クリックでmypnl.moveDisk()が呼ばれます。

mypnl.moveDisk()は円を一つ描いて、xを少し増やしてまた円を描くという動作を右端にくるまで繰り返します。

速すぎると動いていくところが見えませんので、描く度に100ms(0.1秒)停止させます。Thread.sleep(100)が停止の部分です。

動かない

ところが、このプログラムはうまく動きません。

startボタンが押したままになり、円がでません。

moveDisk

しばらくすると、startボタンが戻ると同時に円が一度に出てきます。

moveDisk

まず、一度に出てきてしまう問題。これはrepaint()が再描画が必要なことを知らせる仕組みなのですぐには描画せず、複数回の書き替えを一度にまとめられてしまったのです。

これはrepaint()の代わりにpaintImmediately()を使うことで解消できます。

ファイル名 AnimeDisk1.java の一部

//円を動かすメソッド
public void moveDisk() {
    int x = buffimg.getWidth()/4;  //100
    int y = (int)(buffimg.getHeight()*Math.random());
    int d = buffimg.getWidth()/40;  //円の大きさ 10
    int dx = buffimg.getWidth()/40; //動きの大きさ 10
    bfg.setColor(Color.red);
    while ( buffimg.getWidth() > x ){
        //buffimg.getWidth()はbuffimgの幅
        bfg.fillOval(x-d/2,y-d/2,d,d);
        //repaint();
        paintImmediately(0,0,getSize().width,getSize().height);
        x+=dx;
        //100ms(0.1秒)停止
        try {
           Thread.sleep(100);
        }
        catch(InterruptedException ex) {
           System.err.println(ex);
        }
    }
}

moveDisk

paintImmediately()はすぐにpaintComponentを実行することを要求します。()内の引数は mypnl の全部の領域を表します。

startボタンが押したままになる

さらにstartボタンが押したままになる問題もあります。実はこちらの方が大きな問題で、paintImmediatelyでも解決しません。

イベント処理で長時間占有してはいけない

startボタンが押したままになるのはstartボタンで要求した作業が終わっていないからでプログラムの動作は正当です。

でもこのままでは作業が終わるまで他のボタンは反応しないし「閉じる」ボタンも効きません。(実際には受け付けられて保留になります。作業が終わってから他のボタンの処理をし、「閉じる」の処理もします)

この問題は次のページで解決します。

よけいな話

次に示す解決策では paintImmediately() を使わないので、どうでもよいことですが付け加えておきます。

paintImmediately()は何度もやらせるとコンピュータの動作が重たくなる可能性があるので、上記の様に画面全体を書き直すのではなく、画像の変更のあったところだけを矩形(四角)で指定する方がよいのです。無駄な作業をさせない配慮です。

しかし、mypnl の拡大・縮小を許しているので buffimg と mypnl では変更のあった場所がずれます。

bfg.fillOval(x-d/2,y-d/2,d,d);
paintImmediately(x-d/2,y-d/2,d,d);

としたのでは、mypnl の拡大・縮小をしないときだけうまくいきますが、そうでないときは書き換えられなくなります。

(x-d/2,y-d/2,d,d)は buffimg に対するものです。mypnl 用に計算しなければなりません。

intで計算すると誤差がでるので消し残しが出るので汚くなります。そこでdoubleで計算し、さらにMath.ceilで切り上げ処理をして少し大きめにしています。そのたるかなり面倒になりました。

ファイル名 AnimeDisk1.java の一部

//paintImmediately(x-d/2,y-d/2,d,d);ではだめなので、次のようにします。
double rx = 1.0*getSize().width/buffimg.getWidth();
double ry = 1.0*getSize().height/buffimg.getHeight();
paintImmediately(
   (int)Math.ceil((x-d/2)*rx),
   (int)Math.ceil((y-d/2)*ry),
   (int)Math.ceil(d*rx),
   (int)Math.ceil(d*ry)
);

課題

1.

repaint() を使って円がまとめて表示されることを確認しなさい。

ファイル名 AnimeDisk1.java

2.

repaint() をpaintImmediately(0,0,getSize().width,getSize().height)に替えて円が次々と表示されることを確認しなさい。

ファイル名 AnimeDisk1.java

3.

動く円が描き終わらないうちにgreenやblueボタンを押し、保留になっていることを確認しなさい。

ファイル名 AnimeDisk1.java


Javaプログラミング
聖愛中学高等学校
http://www.seiai.ed.jp/
Dec.2009
Nov.2011
Oct.2012