пятница, 6 июля 2012 г.

Когда зацветает папоротник

В преддверии выходных, хочется праздника. Душа жаждет чего-то буйного, необузданного и, местами, языческого. Ночь на Ивана Купала, в этих условиях, приходится как нельзя кстати, а поскольку папоротник для нее является символом не менее значимым, чем елка для Нового Года, давайте его и нарисуем :)

По привычке создав Android Project, остановимся и немного подумаем. Поскольку мы собираемся изобразить фрактал, используя при этом генератор случайных чисел, очевидно, имеет смысл рисовать его постепенно. Так оно будет и красивее и загадочнее, но нам придется не просто рисовать статичное изображение, а дорисовывать его постепенно, по таймеру. Для этих целей вполне подходит SurfaceView.
Создадим класс MainView, унаследовавшись от него и, чтобы два раза не вставать, сразу и от SurfaceHolder.Callback:

package com.WhiteRabbit.Fern;

import java.util.Random;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class MainView extends SurfaceView implements SurfaceHolder.Callback {
   
    private static final int stepCnt = 100;
   
    private int maxX = 10000;
    private int maxY = 10000;
    private int szX;
    private int szY;
   
    private Random rand = new Random();
   
    public MainView(FernActivity ctx) {
        super(ctx);
        this.getHolder().addCallback(this);
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, 

                               int width, int height) {}

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        update();
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {}
   
    public void update() {
        Canvas c = null;
        try {
            c = this.getHolder().lockCanvas();
            if (c != null) {
                onDraw(c);
            }
        } catch (Exception e) {
        } finally {
            if (c != null) {
                this.getHolder().unlockCanvasAndPost(c);
            }
        }
    }
   
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if ((MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED) ||
            (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED)) {
            setMeasuredDimension(maxX, maxY);
        } else {
            szX = MeasureSpec.getSize(widthMeasureSpec);
            szY = MeasureSpec.getSize(heightMeasureSpec);
            if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) {
                if (szX > maxX) {
                    szX = maxX;
                }
            }
            if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
                if (szY > maxY) {
                    szY = maxY;
                }
            }
            setMeasuredDimension(szX, szY);
        }
    }
   
    private float getSizeX(float x) {
        return (x * szX) / 640;
    }
   
    private float getSizeY(float y) {
        return (y * szY) / 480;
    }
   
    @Override
    protected void onDraw(Canvas c) {
        Paint paint = new Paint();
        paint.setColor(Color.GREEN);
        float x = 1.0f;
        float y = 1.0f;
        for (int i = 0; i < stepCnt; i++) {
            float p = rand.nextFloat();
            float t = x;
            if (p <= 0.85f) {
                x= (0.84f * x) + (0.04f * y);
                y =(0.04f * t) + (0.85f * y) + 1.6f;
            } else if (p < 0.93f) {
                x = (0.20f * x) - (0.26f * y);
                y = (0.26f * t) + (0.212f * y) + 0.44f;
            } else if (p < 0.99f) {
                x = (-0.15f * x) + (0.28f * y);
                y = (0.26f * t) + (0.24f * y) + 0.44f;
            } else {
                x = 0.0f;
                y = 0.16f * y;
            }
            c.drawCircle((szX / 2) + (getSizeX(61) * x), 

                          szY - (getSizeY(40) * y), 0.5f, paint);
        }
    }
}



Код, в целом, стандартный и особых вопросов не вызывающий. Сам алгоритм рисования фрактала определяется непосредственно в onDraw, onMeasure захватывает всю доступную область и инициализирует размеры холста используемого для рисования, а метод update безопасным образом получает Canvas и выполняет отрисовку.
Рисование выполняется постепенно, по 100 точек за один вызов update. Очевидно, нам необходимо создать Timer, чтобы дорисовывать изображение через заданные интервалы времени:

package com.WhiteRabbit.Fern;

import java.util.Timer;
import java.util.TimerTask;

public class Task extends TimerTask {
   
    private static long SECONDS_INTERVAL = 1000L;
    private static long FRAME_RATE = 15L;
   
    private FernActivity ctx;
    private Timer timer = new Timer();
    private int maxIteration = 500;
   
    public Task(FernActivity ctx) {
        this.ctx = ctx;
        timer.scheduleAtFixedRate(this, 0, SECONDS_INTERVAL / FRAME_RATE);
    }

    public void terminate() {
        if (timer != null) {
            timer.cancel();
            timer = null;
        }
    }

    @Override
    public void run() {
        if (maxIteration > 0) {
            ctx.viewUpdate();
            maxIteration--;
        } else {
            terminate();
        }
    }
}
Здесь все тоже довольно стандартно. В конструкторе мы инициализируем таймер так, чтобы он вызвался 15 раз в секунду, а после 1000 итераций его останавливаем. Для того чтобы завершить приложение, осталось дописать Activity:

package com.WhiteRabbit.Fern;

import android.app.Activity;
import android.content.pm.ActivityInfo;
import android.graphics.Color;
import android.os.Bundle;
import android.view.ViewGroup;
import android.widget.LinearLayout;

public class FernActivity extends Activity {
   
    private MainView view;
    private Task     task;
   
    public void viewUpdate() {
        view.update();
    }
   
    @Override
    protected void onPause() {
        task.terminate();
        super.onPause();
    }
   
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
       
        LinearLayout.LayoutParams containerParams =
                new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
                        ViewGroup.LayoutParams.WRAP_CONTENT, 0.0F);
        LinearLayout root = new LinearLayout(this);
        root.setOrientation(LinearLayout.VERTICAL);

        root.setBackgroundColor(Color.BLACK);
        root.setLayoutParams(containerParams);

        view = new MainView(this);
        root.addView(view);
        setContentView(root);
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
       
        task = new Task(this);
    }
}



Вот собственно и все. Запустив приложение на выполнение, мы видим, как из хаоса рождается наше маленькое чудо:


 
Немножко смахивает на коноплю, но задумывалось, безусловно, как папоротник. Всех с наступающим Праздником!

P.S. Если кому-то лень набирать код :)

Комментариев нет:

Отправить комментарий