Screensaver на J2ME или Назад в прошлое. Часть первая

    Доброго времени суток!

    Введение


    На дворе третье января, а душа то и дело требует написать какую-нибудь программку. Недолго думая, я вспомнил, что когда-то предложил поправочку автору топика Splash Screen на J2ME, после чего сомнения по поводу того, что писать, окончательно развеялись. Сегодня я предлагаю вам окунуться в мир «обычных звонилок» и написать для них приложение — это будет обычный скринсейвер.


    Итак, приступим


    Для начала вам следует установить себе необходимое для разработки ПО. Думаю, не надо расписывать, что необходимо ставить себе на ПК для компиляции и запуска нашего приложения, т.к. в интернете такую инструкцию можно найти за пять секунд. Вот, например. Лично я компилирую все исходники приложений для J2ME на «рядовой звонилке». Да, да, есть умельцы на земле русской, которые портировали компилятор с преверификатором на платформу J2ME.
    После того, как всё установлено, неплохо было бы вспомнить саму теорию, начиная устройством J2ME приложения и заканчивая директивами манифеста нашего приложения. Впрочем, если вы не знаете, что к чему, то не расстраивайтесь — немного теории по разработке приложений для J2ME будет разобрано в данном топике.

    Анатомия J2ME приложения


    Вообще, приложение в J2ME называется MIDlet-ом. Почему так, вы узнаете далее.

    javax.microedition.midlet.MIDlet

    Приложение для J2ME является ничем иным, как классом, наследованным от класса MIDlet из пакета javax.microedition.midlet. Данный класс является «сердцем» MIDlet'a. В нашем классе мы обязательно должны объявить три метода, необходимые для функционирования нашего приложения:
    
    public void startApp() {
         // данный метод вызывается при запуске нашего приложения
    }
    
    public void pauseApp() {
         // данный метод вызывается, когда на наше устройство поступает звонок, SMS и т.д.
    }
    
    public void destroyApp(boolean unconditional) {
         // данный метод вызывается тогда, когда он вызывается.
         // сейчас объясню...
    }
    

    Дело в том, что данный метод указан, как необходимый к реализации в то время, как практической пользы он не несёт. Методы startApp() и pauseApp() используются самой AMS, а destroyApp() — нет. По сложившейся традиции, обычно блок данной функции выглядит следующим образом:
    
    public void destroyApp(boolean unconditional) {
         notifyDestroyed(); // данный метод закрывает наше приложение
    }
    

    Как вы можете видеть, прекращением цикла жизни приложений руководит функция notifyDestroyed(), а не destroyApp(). Ну да ладно, предназначение последней так и останется тайной. Не будем отвлекаться, а перейдём дальше.

    javax.microedition.lcdui.Display

    Следующим шагом в реализации нашего приложения является получение ссылки на объект Display пакета javax.microedition.lcdui. Как вы могли догадаться, делается это для получения возможности работы с экраном, формами (javax.microedition.lcdui.Form) или же холстом (javax.microedition.lcdui.Canvas) и переключения между ними. Получить ссылку мы можем следующим образом:
    
    Display dsp = Display.getDisplay(this); // вот так...
    

    Дело сделано, переходим к следующему шагу.

    javax.microedition.lcdui.game.GameCanvas

    Сразу же хочется отметить, что с появлением класса GameCanvas — он начал вытеснять обычный Canvas из-за невозможности двойной буфферизации «из коробки», как таковой последнего. Поэтому, в качестве холста, я предлагаю использовать именно GameCanvas. Сказано — сделано.
    GameCanvas и Canvas отличаются всего-лишь по двум признакам: первый уже был указан (это двойная буфферизация), а вторым является необходимость объявления метода paint в последнем. Для наглядности приведу следующий код реализации холста с помощью Canvas:
    
    import javax.microedition.lcdui.Canvas;
    import javax.microedition.lcdui.Graphics;
    
    public class OurCanvas extends Canvas { // наследуемся от Canvas
    
         public void paint(Graphics g) {
    	     
    		 int w = getWidth(); // получаем высоту экрана
    		 int h = getHeight(); // и про ширину не забываем
    		 
    		 g.setColor(0xffffff); // устанавливаем цвет кисти
    		 g.fillRect(0,0,w,h); // заполняем выбранную область заданным цветом
    	 }
    }
    

    В методе paint() мы выполняем всю отрисовку графических примитивов. Данный подход не особо эффективен по многим причинам, одной из которой является желание разделить логику.
    GameCanvas позволяет вынести логику в другие методы, классы и т.д. Вот так:
    
    import javax.microedition.lcdui.Canvas;
    import javax.microedition.lcdui.Graphics;
    
    public class OurCanvas extends GameCanvas implements Runnable { // наследуемся от GameCanvas и реализуем интерфейс Runnable, который используется классом Thread
    
         public void run() {
    	     
    		 Graphics g = getGraphics(); // получаем ссылку на объект Graphics
    		                           // с помощью которого мы и отрисовываем эти самые графические примитивы
    								   
    		 int w = getWidth(); // получаем высоту экрана
    		 int h = getHeight(); // и про ширину не забываем
    		 
    		 g.setColor(0xffffff); // устанавливаем цвет кисти
    		 g.fillRect(0,0,w,h); // заполняем выбранную область заданным цветом
    	 }
    }
    

    Тут я совершил ошибку: не вынес объявления объекта Graphics куда-нибудь внутрь объявления самого класса в место объявления переменных, т.е. код должен выглядеть следующим образом:
    
    import javax.microedition.lcdui.Canvas;
    import javax.microedition.lcdui.Graphics;
    
    public class OurCanvas extends GameCanvas implements Runnable {
    	 Graphics g = getGraphics();				   
    	 int w = getWidth();
    	 int h = getHeight();
    		 
         public void run() {
    		 g.setColor(0xffffff);
    		 g.fillRect(0,0,w,h);
    		 isSomethingDone();
    	 }
    	 
    	 public void isSomethingDone() {
    	     g.setColor(0xababab);
    		 g.drawLine(25,25,125,125);
    	 }
    }
    

    Как-то так. Вот и всё — необходимая база знаний для реализации задуманного у нас есть. Поехали!

    Переходим к разработке


    Наше приложение будет состоять из нескольких классов:
    Встречайте! midlet.java:
    
    import javax.microedition.midlet.MIDlet;
    import javax.microedition.lcdui.*;
    import java.util.Timer;
    
    public class midlet extends MIDlet {
         Timer timer;
         midletTimerTask task;
         midletCanvas canvas;
    
         public midlet () {
             canvas  = new midletCanvas(this);    // создаем канву
             timer = new Timer ();
             task = new midletTimerTask (canvas); // и задачу, которая будет выполняться
             timer.scheduleAtFixedRate(task,10,10); // каждые 10 миллисекунд
     // 10 мс - очень мало. Поэтому вызываемый метод repaint будет проверять, 
     // закончена ли перерисовка предыдущего экрана, с помощью флага in
        }
     
         protected void startApp() { // сразу передаем управление канве
             Display.getDisplay(this).setCurrent (canvas);
         }
     
         protected void pauseApp() {}
    
         protected void destroyApp(boolean unconditional) {}
     
         public void exitMIDlet()  {
             notifyDestroyed();
         }
    }
    

    Далее сам холст — midletCanvas.java:
    
    import javax.microedition.lcdui.*;
    
    class midletCanvas extends Canvas  {
     midlet midlet;
     random Random;
     static int [] PlasmaTab = { //таблица цветов плазмы - 256 элементов
      32,32,33,34,35,35,36,37,38,39,39,40,41,42,42,43,
      44,45,45,46,47,47,48,49,49,50,51,51,52,52,53,54,
      54,55,55,56,56,57,57,58,58,59,59,59,60,60,60,61,
      61,61,62,62,62,62,63,63,63,63,63,63,63,63,63,63,
      63,63,63,63,63,63,63,63,63,63,62,62,62,62,62,61,
      61,61,60,60,60,59,59,58,58,58,57,57,56,56,55,54,
      54,53,53,52,52,51,50,50,49,48,48,47,46,46,45,44,
      43,43,42,41,40,40,39,38,37,37,36,35,34,33,33,32,
      31,30,30,29,28,27,26,26,25,24,23,23,22,21,20,20,
      19,18,17,17,16,15,15,14,13,13,12,11,11,10,10, 9,
       9, 8, 7, 7, 6, 6, 5, 5, 5, 4, 4, 3, 3, 3, 2, 2,
       2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2,
       2, 3, 3, 3, 4, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9,
       9,10,11,11,12,12,13,14,14,15,16,16,17,18,18,19,
      20,21,21,22,23,24,24,25,26,27,28,28,29,30,31,31
     }; 
     //Естественно, я не набивал вручную эту таблицу, а получил 
     //по закону 32* (1+ sin (x*(2*pi/255)), приведя все к целым числам
    
     static int Delta = 6; //размер ячейки плазмы в пикселах
    
     static int Yold_pos=0, Yold_asp=0, Xold_pos=0, Xold_asp=0;
     int Ynew_pos, Ynew_asp, Xnew_pos, Xnew_asp, x, y, Color;
     static int Width, Height; //Ширина и высота экрана
     static boolean in = false; //состояние перерисовки
     Graphics gbuffer; //а это для
     Image screen;     //двойной буферизации
      
     public midletCanvas (midlet midlet)  {
      this.midlet = midlet;
      Random = new random ();
      setFullScreenMode (true); //Единственный оператор профиля MIDP 2.0
      Width = getWidth ();
      Height = getHeight ();
      screen=Image.createImage(Width,Height);
      gbuffer=screen.getGraphics(); //рисовать будем на gbuffer,
      draw (gbuffer); //а потом одномоментно выводить его на канву!
     }
    
     void draw (Graphics g) { //рисуем в буфере очередное состояние плазмы
      in = true;
      Ynew_pos = Yold_pos;         
      Ynew_asp = Yold_asp;
      for (y=0; y<Height; y+=Delta) {
       Xnew_pos = Xold_pos;       
       Xnew_asp = Xold_asp;
       for (x=0; x<Width; x+=Delta) {
        Color = PlasmaTab[Ynew_pos]+ PlasmaTab[Ynew_asp]+ 
                PlasmaTab[Xnew_pos]+ PlasmaTab[Xnew_asp];
        g.setColor(((255-Color)<<16 | Color<<8 | 128+(Color>>1)));
        g.fillRect (x,y,Delta,Delta);
        Xnew_pos += 1;
        if (Xnew_pos > 255) Xnew_pos=0;
        Xnew_asp += 7;
        if (Xnew_asp > 255) Xnew_asp=0;
       }
       Ynew_pos += 2;
       if (Ynew_pos > 255) Ynew_pos=0;
       Ynew_asp += 1;
       if (Ynew_asp > 255) Ynew_asp=0;
      }
      Xold_pos -= 2;
      if (Xold_pos<0) Xold_pos=255;
      Xold_asp += Random.get(8);
      if (Xold_asp > 255) Xold_asp=0;
      Yold_pos += 4;
      if (Yold_pos > 255) Yold_pos=0;
      Yold_asp -= Random.get(6);
      if (Yold_asp<0) Yold_asp=255;
      in = false;
     }
    
     protected void paint (Graphics g) {
      if (in == true) return; //Если мы еще в состоянии перерисовки
      g.drawImage(screen,0,0,0);
      draw (gbuffer);
     }
        
     public void keyPressed(int keyCode) {
      switch (keyCode) { //Выход по клавише #
       case Canvas.KEY_POUND:    
        midlet.exitMIDlet();
       break;
      }
     }
    }
    

    Следующим по счёту идёт таймер — midletTimerTask.java:
    
    import java.util.TimerTask;
    import javax.microedition.midlet.MIDlet;
    
    class midletTimerTask extends TimerTask {
     midletCanvas canvas;  
        
     public midletTimerTask (midletCanvas canvas) {
      this.canvas = canvas;
     }
    
     public final void run() {  
      canvas.repaint();
     }
    }
    

    И, наконец, реализация выбора случайных чисел — random.java
    
    import java.util.Date;
    import java.util.Random;
    
    class random { //Класс-оболочка генератора случайных чисел
     private Random r;
     private Date d;
    
     public random () {
      d = new Date ();
      r = new Random (d.getTime());
     }
    
     public int get (int max) { //случайное число из диапазона [0,max-1]
      int n=r.nextInt()%max; 
      return (n<0 ? -n : n);
     }
    }
    


    Скандалы, интриги, расследования


    Мои исходники не хотят компилироваться. Вернее, они компилируются, но выполняется не то, что хочется мне. Поздняя ночь, поэтому вашему покорному слуге пришлось взять не свои исходники. Извиняюсь за форматирование кода — завтра поправлю.

    Дальше дело техники


    Компиляция и преверификация. Этот шаг я оставляю на вас.

    MANIFEST.MF


    Я предлагаю вам повозиться с этим файлом, а находится он в каталоге META-INF. Ведь, если мы делаем скринсейвер, то и вести он должен себя соответствующе. К сожалению, такое поведение доступно только телефонам от Sony Ericsson. Давайте добавим в него следующую строку:
    SEMC-StandbyApplication: Y


    Что мы сделали


    Мы сделали возможной загрузку нашего скринсейвера, как фоновый рисунок. Для этого вам необходимо кинуть MIDlet в телефон. Далее заходим в «Приложения», находим наше. Следующим действием будет нажатие правой софт-клавиши и выбор пункта «Как фон. рисунок». Выбираем — кушать подано. Вы увидите следующее

    Вот, что получилось






    All you need


    Исходники вы можете взять на Pastebin. вот они:
    midlet.java
    midletCanvas.java
    midletTimerTask.java
    random.java

    Заключение


    Вот и всё. Всего-лишь за несколько шагов мы написали MIDlet. В наши дни J2ME постепенно уходит в никуда и, возможно, у вас уже не осталось подобной трубки, но пока Oracle занимается её поддержкой — кто знает, то ли ещё будет. Надеюсь вам было интересно.

    До скорых встреч!
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 36

      0
      Эм… первый вопрос: с чего вы взяли, что destroyApp не вызывается AMS?
      прекращением цикла жизни приложений руководит функция notifyDestroyed(), а не destroyApp()
      Вы прямо с самого начала статьи всё напутали совершенно. Эти функции не имеют ничего общего и совершенно разные значения имеют. Первая всего лишь велит переводить в destroyed, а вторая как раз, грубо говоря, вызывается в мидлете для указания этого факта (причём флаг в параметре указывает можно ли отбрить, почитайте про это). Совершенно точно так же, как есть notifyPaused, например, и pauseApp.
       По сложившейся традиции, обычно блок данной функции выглядит следующим образом:
      public void destroyApp(boolean unconditional) {
           notifyDestroyed(); // данный метод закрывает наше приложение
      }</java>И блок этот выглядит так по традиции (у вас, например) именно только затем, чтобы можно было вызывать метод destroyApp (помимо вызова его AMS) из самой программы, чтобы завершить мидлет насильно, например по кнопке QUIT, ибо при этом в destroyApp обычно всякие ресурсы освобождают итд итп, чтобы не городить отдельный метод для этого. И при вызове notifyDestroyed из destroyApp обратного вызова не происходит, это всё даже в явадоках, вроде описано.
        +2
        Ну и дальше код какой-то странный, честно говоря, не по-джавовски как-то, классы со строчных букв, переменные с заглавных и т.п. И писано про GameCanvas, а пишется на Canvas почему-то. Выясняется в итоге, что писали про свой класс, а взяли чужой? Оригинально-с :) За острый приступ ностальгии +1 всё равно можно поставить.
          0
          Да, начал про гейм канвас, но код так планировал на обычной канве, чтобы показать, как в ней протекает «двойная буферизация».
            0
            И я, если заметили, там указал, что код не свой использовал. Поэтому «не по-джавовски». :)
            0
            Да, напутал немного. destroyApp() вызывается самой AMS, когда, например, java-машине не хватает памяти в heap'е. Это да — в этом признаю ошибку.
              0
              Справедливости ради, не QUIT, а EXIT. Константа в javax.microedition.lcdui.Command.
              +1
              Дайте готовый jar, пощупаю на телефоне :))
                0
                Держите — upwww.ru/?id=2962 :)
                  +1
                  upwww вместо Screensaver.jar мне телефону подсунул загрузчик платного контента за смс, сцуко.
                  скачал по прямой ссылке. няшка! :)
                  Найду какой-нибудь второй телефон, сфотаю
                    +1
                    У вас какой телефон, если не секрет? ;)
                      +1
                      Philips X550 :)
                        +1
                        Неплохой аппарат. Преимущественно, фили держат зарядку прекрасно, как и Sony Ericsson. Плюс ко всему, их очень тяжело разбить. :)
                          +1
                          Трещину на экране я ему посадил, глюки с блютус гарнитурой наличенствуют, с жавой отдельная песня (например этот FREE_XXX_VIDEOS я удалить не могу — телефон уходит в перезагрузку сразу), но в остальном… как звонилка он работает на ура, батарею держит очень долго, MidpSSH и Opera Mini работают.
                            +1
                            Всё-таки согласитесь, что фили — самые ударопрочные телефоны. Сам пользовался — знаю, что за зверь.
                              0
                              Ммм… Alcatel OT715 в алюминевом корпусе очень хорошо выдерживал вертикальное падение с 2х метров на бетон :)
                                0
                                Да, про алков забыл. Достаточно прочные, но слишком простые звонилки.
                              +1
                              Тоже MidpSSH используете? :)
                                +1
                                А что, есть альтернативы для J2ME под такие экраны?
                                  0
                                  К сожалению, альтернативы для J2ME нет вообще. Ни под какой экран. :)
                                    0
                                    Выше ответил. :)
                                      0
                                      Что-то не то:
                            0
                            Дико извиняюсь. Просто в тот момент уже с телефона сидел, поэтому скинул на что первое в закладках попалось. :)
                        –2
                        Прощай, зарядка?
                          0
                          Вообще не особо тратится зарядка. Я кстати засекал ради интереса: у меня LG KP500 и до полной разрядки с включенным скринсейвером она работала ~17 часов.
                            0
                            Это при том, что зарядки оставалось ~40%
                              0
                              Поставил скринсейвер на свой Sony Ericsson K550i. Для чистоты эксперимента зарядил его полностью. Как сядет батарейка — отпишусь.
                              0
                              Вам будет интересна статья по геймдеву для J2ME. За пример думаю взять текущую поделку, которая скоро будет в Ovi Store
                                0
                                Это был вопрос!
                                  +1
                                  Не плохо было бы. Искал литературу по j2me — много старых материалов. Хотелось бы увидеть что-то более современное.

                                  Так же хотел спроить про интерфейс приложения. Его тоже лучше через canvas рисовать? Слишком неразнообразен набор стандартных элементов.
                                    0
                                    В пакете javax.microedition.lcdui реализован весь необходимый UI. К сожалению, на рынке J2ME телефонов ситуация такая же, как когда-то была между Netscape и IE. Каждый производитель отрисовывает стандартные элементы UI по-своему. Красивые же интерфейсы, которые выглядят одинаково на всех устройствах (Jimm, за примером далеко ходить не надо) рисуются на Canvas, да. Есть готовые библиотеки для отрисовки красивого UI на Canvas.
                                      0
                                      Я лично постигал искусство программирования под J2ME по этим самым старым материалам и, собственно, небезызвестной книге Буткевича, которая новой тоже не является.
                                        0
                                        Значит решено — статье быть. Хотя, если честно, в них не будет никакой новизны. Код почище, логика по логичнее (извиняюсь за каламбур), ну и описывать процесс буду более детально. :)
                                          0
                                          Я не сильно упорствовал в освоении. Просто заметил GameCanvas, о котором я не знал. Полагаю, могу еще что-нибудь открыть для себя.
                                            0
                                            Если возникнут вопросы — пишите, спрашивайте. Ждём-с. ;)
                                    0
                                    Мне интересно, кто-нибудь сам решился скомпилировать приведённые мною исходники? :)
                                      0
                                      Вторая часть моих размышлений и растлоковании теории создания J2ME MIDlet'ов — читать здесь.

                                      Only users with full accounts can post comments. Log in, please.