воскресенье, 9 декабря 2012 г.

ClientSocket под Android. Клиент для взаимодействия приложений через интернет.

Доброго всем! Решил поделиться своими наработками в области создания сетевых приложений под Андроид. Данное сообщение не относится непосредственно к AndEngine, но имеет прямое отношение к геймдеву, ведь каждому игроделу когда-нибудь приходит в голову идея организовать взаимодействие игроков через интернет (настольные игры, общение, пошаговые стратегии и прочее).
Сразу оговорюсь, что приведенный здесь класс не обеспечивает "прямого" взаимодействия пользователей. Между пользователями необходимо наличие веб-сервера на котором должна быть реализована логика аутентификации, игровая логика и который будет координировать команды и события от клиентов.
В процессе портирования одной популярной настольной игры под Android, мы решили прикрутить к ней возможность игры через интернет.
Задача стояла следующая: получить инструмент, позволяющий легко реализовывать пошаговый протокол игры, имеющий события подключения, отключения, перехват ошибок,  ну и конечно отправки/приема данных. Я намеренно пишу "получить", потому что не являюсь хардкорным программером и если нахожу хорошее опенсурс-решение со свободной лицензией, то ничуть не стесняюсь его использовать. Такого же мнения придерживаюсь и для своего кода в этом блоге: если кому-то что-то глянется - берите не стесняйтесь. =)
Итак, начали мы с активных поисков готового кода. Через пару дней скитаний по интернету стало понятно, что устойчивого и законченного клиента для WEB для нас никто не выложил =). Конечно, нашлось немало статей по созданию таких клиентов, но все они были настолько печальны, что их юзабельность сводилась к нулю.
Я не могу привести здесь все источники из которых мы брали информацию, во-первых их очень много, а во-вторых ни один из них не стал для нас отправной точкой, поэтому, без лишних слов привожу здесь код который у нас получился из собранной инфы и собственных умозаключений.

package com.expedition107.mytreasures;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;

import android.os.Handler;
import android.util.Log;

public class ClientSocket {

 private Handler handler = new Handler();

 private static String mHostIpAddress;
 private static int mPort;

 private Thread mServiceThread;
 private Runnable mServiceRunnable;

 private Socket mSocket;

 private volatile boolean isConnected = false;
 private int mSocketStatus = 0;// 0-disconnected, 1- connection, 2-
         // io_pending, 3 - write out, 4 - read in, -1 - unknown state
 private volatile String mOutString = "";
 
 private volatile boolean mIsNewData;

 public ClientSocket(String pHostIp, int pPort) {
  mHostIpAddress = pHostIp;
  mPort = pPort;

 }

 public void connect(final int pTimeoutMillisecs) {
  if (mSocketStatus != 0) {
   return;
  }
  mServiceRunnable = new Runnable() {
   @Override
   public void run() {
    mSocketStatus = 1; //соединяемся
    // connect
    try {
     mSocket = new Socket();
     mSocket.connect(new InetSocketAddress(mHostIpAddress,mPort), pTimeoutMillisecs);
     mSocket.setKeepAlive(true);
     setConnected(true);
     onConnect();
     mSocketStatus = 2;//уходим в прослушку сокета
     char[] inBuff = new char[1024];
     while (isConnected()) {
      String tmpSendData = getSendData(); 
      if (!tmpSendData.equals("")) {
       mSocketStatus = 3;//пишем в сокет
       PrintWriter out = new PrintWriter(
         new BufferedWriter(new OutputStreamWriter(
           mSocket.getOutputStream())), true);
       
       out.println(tmpSendData);
       handler.post(new Runnable() {
        @Override
        public void run() {
         onSendData();
        }
       });
       
       clearSendData(); 
      }
      
      mSocketStatus = 4;//читаем из сокета
      BufferedReader in = new BufferedReader(
        new InputStreamReader(mSocket.getInputStream()), 1024);
      String line = "";
      if (in.ready()) {      
       int length = in.read(inBuff);
       while ((length) != -1) {
        for (int i = 0; i < length; i++) {
         line = line + inBuff[i];
        }
        length = -1;
        if (in.ready()){
         length = in.read(inBuff);
        }
       } 
       final String inStr = line;
       handler.post(new Runnable() {
        @Override
        public void run() {
         onReadData(inStr);
        }
       });
       
      }
     }
     mSocketStatus = 0;//отсоединяемся
     mSocket.close();
     handler.post(new Runnable() {
      @Override
      public void run() {
       onDisconnect();
      }
     });
     

    } catch (UnknownHostException e) {
     final UnknownHostException _e = e;
     handler.post(new Runnable() {
      @Override
      public void run() {
       mSocketStatus = -1;
       onSocketError(mSocketStatus,_e);
       mSocketStatus = 0;
      }
     });
     e.printStackTrace();
    } catch (SocketException e) {
     final SocketException _e = e;
     handler.post(new Runnable() {
      @Override
      public void run() {
       onSocketError(mSocketStatus, _e);
       mSocketStatus = 0;
      }
     });
     e.printStackTrace();
    } catch (IOException e) {
     final IOException _e = e;
     handler.post(new Runnable() {
      @Override
      public void run() {
       onSocketError(mSocketStatus, _e);
       mSocketStatus = 0;
      }
     });
     e.printStackTrace();
    }
   }
  };
  mServiceThread = new Thread(mServiceRunnable);
  mServiceThread.start();
 }
 
 //эти методы вызываются из UI-потока
 public void disconnect() {
  setConnected(false);
 }


 public void onConnect() {
  
 }

 public void onDisconnect() {
  
  setConnected(false);
 }

 public void onSocketError(int pSocketStatus, Exception e) {
  
 }

 public void onReadData(String pDataString) {
  
 }

 public void onSendData() {
  
 }

 //синхронизированные методы чтения/записи
 protected synchronized boolean isConnected(){
  return this.isConnected;
 }
 
 private synchronized boolean setConnected(boolean pIsConnected){
  this.isConnected = pIsConnected;
  return this.isConnected;
 }
 //этот метод вызывается из UI-потока
 protected synchronized void send(String pOutString){
  mOutString = pOutString;
  mIsNewData = true;
 }
 
 private synchronized String getSendData(){
  mIsNewData = false;
  return mOutString;
 }
 
 private synchronized void clearSendData(){
  if (!mIsNewData)
   mOutString = "";
 }
}

И, как обычно, немного пояснений.
Работа сокета по чтению и записи данных должна происходить в отдельном потоке и ни в коем случае не в потоке UI., именно поэтому, чтобы получить доступ из UI к данным (а ведь это нам и нужно) которые приняты/переданы мы создаем Handler и работаем через него.
Синхронизированные методы нужны для синхронизации доступа к volatile переменным, т.е. переменным которые могут использоваться и потоком UI и потоком сокета.
В коде фигурирует переменная mSocketStatus использование которой обоснованно только нашими способами отладки =).

Использование класса заключается в перекрытии его методов прямо в UI потоке:

Чтение данных и обработка ошибок:
final ClientSocket cs = new ClientSocket(serverIpAddress, 7777){
         @Override
         public void onConnect(){
             //Например показать Toast уведомление
           
         }
         @Override
         public void onDisconnect(){
             //обязательно вызвать метод супера
             //иначе разрыва связи не произойдет
                    super.onDisconnect();
             //тут ваш код
           
         }
         @Override
         public void onReadData(String pStr){
             //В pStr хранятся принятые данные
                    textView.setText(textView.getText()+pStr+"\n");
           
         }
         
         @Override
         public void onSocketError(int pSocketStatus, Exception e) {
          Log.v("game", e.getMessage());
         }
        };

Подключение к серверу:
...
cs.connect(5000);
...  

Отправка данных:
...
cs.send("СТРОКА ДАННЫХ НА ОТПРАВКУ");
...

Отключение от сервера:
...
cs.disconnect();
...  

Отсюда видно, что с этим классом не надо заморачиваться с работой сокетов, а сразу приступить к реализации своего протокола, а при наличии хорошего стабильного соединения - не только пошагового. Здесь имеется возможность контролировать момент подключения/отключения к серверу, ошибки сокета, отправку и прием данных.
Протокол для своей игрушки мы реализовали на базе XML. Советую использовать вот этот класс для парсинга, очень помогает не запутаться в ветках.
PS Не воспринимайте данный класс как панацею, хотя он и вполне работоспособен, небольшие сомнения все-таки вызывает отправка/очистка данных и если у кого-то появится желание по усовершенствованию данного класса мы будем рады совету или подсказке. Изменения я постараюсь сразу же внести.
Спасибо за внимание!

воскресенье, 28 октября 2012 г.

Moonlight Spirits Pro. Кое-что новенькое в мире LWP.

Всем привет!
Чтобы как-то себя расшевелить на написание новых статей по программированию, решил написать сообщение не содержащее ни кусочка кода. А все потому, что наша команда, наконец-то закончила еще один проект живых обоев под Android с названием Moonlight Spirits Pro.  =)

Скриншоты (видео смотрите внизу сообщения):

Проект был реализован примерно за один месяц и над проектом работали два человека, так что, отбросив скромность могу сказать,что работа была проделана не малая и, я бы даже сказал - очень большая. :) Движок для обоев все тотже: AndeEngine GLES2.

Что же из себя представляют наши обои, оправдано ли в название слово "Pro" и стоит ли о них вообще писать? Давайте посмотрим.

Темы.
Итак, обои выполнены в жанре ужасов, мистики и прочей чертовщины. В отличии от других обоев предлагаемых на маркете, Moonlight Spirits не является просто несколькими настраиваемыми картинками, ведь они включают в себя 7 разных тематических наборов, каждый из которых может быть отдельными приложениями. Тут вам и замок дракулы, и заснеженные горы, и Стоунхендж, и еще 4-е основных темы кардинально различающиеся друг от друга.
Небесные объекты.
Особое внимание следует уделить небесным объектам: луна, мерцающие звезды, плывущие облака и туман. На луне проявляются картины подобранные для каждой темы отдельно, например картины из темы Haunted House невозможно увидеть в теме  Dust of  Time. Кроме того, луна умеет подниматься и садиться за горизонт, при этом цветовая гамма меняется соответствующим образом и все выглядит очень естественно и главное красиво. Всего сделано 4-е режима восхода и захода луны: всегда ночь, всегда день, день/ночь автоматически и день/ночь по тапу. Ну и конечно, все небесные объекты имеют опции включения/отключения.
Лунные образы.
Лунные образы это те самые картины которые нам показывает луна. Повторюсь, что наборы образов тематические и индивидуальны для каждой темы. Сменяются они случайным образом.  Из можно отключить или изменить скорость показа.
Облака.
На переднем плане плывут облака. Это даже скорее не облака, а больше похоже на туман который добавляет еще больше мистичности. Всего в обоях предусмотрено два слоя тумана которыми можно манипулировать настраивая скорость движения и включая/отключая слои.
Цветовые схемы.
К каждой теме может быть применена цветовая схема. На текущий момент 11 схем и каждая из них индивидуальна. Цветовая схема по сути представляет собой набор цветов для дневного и ночного режима для каждого элемента текущей темы. Использование цветовых схем - это один из значительных шагов в сторону реальной персонализации смартфона.
Создания.
Moonlight Spirits живут своей жизнью. Для каждой из тем предусмотрены свои наборы созданий которые периодически появляются на экране (летучие мыши, совы, вороны и даже есть орлы с химерами). Кроме того присутствует настройка по выбору созданий при взаимодействии пользователя с экраном смартфона и их количества. Опция Background Life включает/отключает случайно появляющихся созданий.
Интерактивность.
Ну какие обои на сегодняшний день не обладают возможностью взаимодействия с пользователем? Вот и Moonlight Spirits предоставляет такую опцию. Взаимодействие с пользователем проявляется в двух событиях: на тап по экрану и на сдвиг экрана. В первом случае обои порождают созданий в зависимости от выбранной настройки,а во втором случае выполняется параллакс-эффект:  основные элементы композиции сдвигаются относительно друг друга и выглядит это действительно здорово.
Звук.
При касании экрана раздаются звуки специально подобранные в жанре ужасов. Можно выбрать конкретный звук на нажатие или поставить случайный звук.
Psycho.
В попытке еще больше расширить возможность персонализации, мы придумали цветовой фильтр который собственно и назвали Psycho. При установке этого фильтра, вся текущая цветовая схема просто выворачивается наизнанку и как будут выглядеть обои после применения этого фильтра фактически неизвестно, НО цвета получаются настолько неправдоподобными и сумасшедшими, что просто не могут не понравиться  =) .
Quake at touch.
Интересная опция которая позволяет шевелиться всей основной композиции при прикосновению к экрану. Получается что-то вроде волнения местнности и мне особенно нравиться этот эффект на готических развалинах  и на на теме Стоунхендж.
Прочее.
Обои поддерживают портретный и ландшафтный режимы. Переносятся на SD, и имеют вес менее 4 Мб! Это действительно здорово, при том что уровень графики высок и очень разнообразен.

Вот такие вот обои у нас получились, а вам судить на сколько хорошо у нас это вышло. Спасибо! Маркет: https://play.google.com/store/apps/details?id=com.expedition107.wp.halloween.hd


понедельник, 30 июля 2012 г.

Пример шейдера на Android. Metablob.

Здесь я выложу код модифицированного шейдера под названием Metablob (автор Adrian Boeing). Исходник взят от сюда Shader Toy.
Как, что и почему я тут расписывать не буду. Более детальное прикручивание шейдера к Android можно посмотреть в моем сообщении "Пример шейдера на Android. Ripple Effect.". Здесь будет интересен уже результат шейдера.

Выглядит он *примерно* так, плюс оно шевелиться всячески =):

Изменению подверглись количество и радиусы сгустков (свой! уникальный! аррргххх....=( ).
А вот и код активити:

package com.expedition107.shader.test;
import java.io.IOException;
import java.io.InputStream;

import org.andengine.engine.Engine;
import org.andengine.engine.LimitedFPSEngine;
import org.andengine.engine.camera.Camera;
import org.andengine.engine.handler.IUpdateHandler;
import org.andengine.engine.options.EngineOptions;
import org.andengine.engine.options.ScreenOrientation;
import org.andengine.engine.options.resolutionpolicy.RatioResolutionPolicy;
import org.andengine.entity.scene.IOnAreaTouchListener;
import org.andengine.entity.scene.IOnSceneTouchListener;
import org.andengine.entity.scene.ITouchArea;
import org.andengine.entity.scene.Scene;
import org.andengine.entity.scene.background.Background;
import org.andengine.entity.sprite.Sprite;
import org.andengine.entity.sprite.UncoloredSprite;
import org.andengine.entity.util.FPSLogger;
import org.andengine.input.touch.TouchEvent;
import org.andengine.input.touch.detector.ClickDetector;
import org.andengine.input.touch.detector.ClickDetector.IClickDetectorListener;
import org.andengine.opengl.shader.PositionTextureCoordinatesShaderProgram;
import org.andengine.opengl.shader.ShaderProgram;
import org.andengine.opengl.shader.constants.ShaderProgramConstants;
import org.andengine.opengl.shader.exception.ShaderProgramException;
import org.andengine.opengl.shader.exception.ShaderProgramLinkException;
import org.andengine.opengl.texture.ITexture;
import org.andengine.opengl.texture.PixelFormat;
import org.andengine.opengl.texture.bitmap.BitmapTexture;
import org.andengine.opengl.texture.region.ITextureRegion;
import org.andengine.opengl.texture.region.TextureRegionFactory;
import org.andengine.opengl.texture.render.RenderTexture;
import org.andengine.opengl.util.GLState;
import org.andengine.opengl.vbo.attribute.VertexBufferObjectAttributes;
import org.andengine.ui.activity.BaseGameActivity;
import org.andengine.util.adt.io.in.IInputStreamOpener;
import org.andengine.util.color.Color;

import com.expedition107.shader.test.ShockwaveTest.ShockwaveShaderProgram;

import android.opengl.GLES20;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.WindowManager;

public class MetablobTest extends BaseGameActivity implements IOnSceneTouchListener {
 
 public static int WIDTH = 800;
 public static int HEIGHT = 480;
 
 protected static MetablobTest Instance;
 private Camera mCamera;
 private Sprite mSprite;
        private ITexture mTexture;
        private ITextureRegion mTextureRegion;
    
 private boolean mRenderTextureInitialized = false;
 private RenderTexture mRenderTexture;
 private Sprite mRenderTextureSprite;
 
 private float mShockwaveTime = 0f;
 private float mCenterX = 0.5f;
 private float mCenterY= 0.5f;
 
 @Override
 public EngineOptions onCreateEngineOptions() {
  
  DisplayMetrics displayMetrics = new DisplayMetrics();
  WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
  wm.getDefaultDisplay().getMetrics(displayMetrics);
  wm.getDefaultDisplay().getRotation();
  WIDTH = displayMetrics.widthPixels;
  HEIGHT = displayMetrics.heightPixels;
  
  mCamera = new Camera(0, 0, WIDTH, HEIGHT);
  
  final EngineOptions engineOptions = new EngineOptions(true, ScreenOrientation.LANDSCAPE_FIXED, new RatioResolutionPolicy(WIDTH, HEIGHT), this.mCamera);

  return engineOptions;
 }
 
 @Override
    public Engine onCreateEngine(final EngineOptions pEngineOptions) {
        return new LimitedFPSEngine(pEngineOptions, 120) {
         
         
         @Override
         public void onDrawFrame(GLState pGLState)
           throws InterruptedException {
    
     if (!mRenderTextureInitialized) {
      initRenderTexture(pGLState);
      mRenderTextureInitialized = true;
     }
     
     mRenderTexture.begin(pGLState, false, true, Color.TRANSPARENT);
     {  
      super.onDrawFrame(pGLState);
     }
     mRenderTexture.end(pGLState);
         
     pGLState.pushProjectionGLMatrix();
     pGLState.orthoProjectionGLMatrixf(0, mCamera.getSurfaceWidth(), 0, mCamera.getSurfaceHeight(), -1, 1);
     {
      mRenderTextureSprite.onDraw(pGLState, mCamera);
     }
     pGLState.popProjectionGLMatrix(); 

         }
         
   private void initRenderTexture(GLState pGLState) {
    mRenderTexture = new RenderTexture(this.getTextureManager(),mCamera.getSurfaceWidth(), mCamera.getSurfaceHeight(), PixelFormat.RGBA_8888);
    mRenderTexture.init(pGLState);
    mRenderTextureSprite = new UncoloredSprite(0f, 0f, TextureRegionFactory.extractFromTexture(mRenderTexture), getVertexBufferObjectManager());
    
    mRenderTextureSprite.setShaderProgram(ShockwaveShaderProgram.getInstance());    
   }
        };
 }

 @Override
 public void onCreateResources(OnCreateResourcesCallback pOnCreateResourcesCallback) throws Exception {

 Instance = this;

        try {
            this.mTexture = new BitmapTexture(this.getTextureManager(), new IInputStreamOpener() {
    
    @Override
    public InputStream open() throws IOException {
     // TODO Auto-generated method stub
     return getAssets().open("gfx/sw_12_23.jpg");
    }
   });
            
            
            this.mTextureRegion = TextureRegionFactory.extractFromTexture(mTexture);
            this.mTexture.load();
        } catch (IOException e) {
        }
        
        this.getShaderProgramManager().loadShaderProgram(ShockwaveShaderProgram.getInstance());

  pOnCreateResourcesCallback.onCreateResourcesFinished();
 }

 @Override
 public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) throws Exception {        
   this.mEngine.registerUpdateHandler(new FPSLogger());

         final Scene scene = new Scene();
         
         scene.setBackground(new Background(0.09804f, 0.6274f, 0.8784f));
         
         this.mSprite = new Sprite(0f, 0f,40,40, this.mTextureRegion, getVertexBufferObjectManager());
   scene.attachChild(mSprite);
   
   getEngine().registerUpdateHandler(new IUpdateHandler() {
    @Override
    public void reset() {
    }   
    @Override
    public void onUpdate(float pSecondsElapsed) {
     mShockwaveTime += pSecondsElapsed*0.5;
    }
   });
         scene.setOnSceneTouchListener(this);         
         pOnCreateSceneCallback.onCreateSceneFinished(scene);
 }

 @Override
 public void onPopulateScene(Scene pScene,
   OnPopulateSceneCallback pOnPopulateSceneCallback) throws Exception {
  pOnPopulateSceneCallback.onPopulateSceneFinished();
 }
 
 public static class ShockwaveShaderProgram extends ShaderProgram {
  
  private static ShockwaveShaderProgram instance;
  
  public static ShockwaveShaderProgram getInstance() {
   if (instance == null) instance = new ShockwaveShaderProgram();
   return instance;
  }
    
  public static final String FRAGMENTSHADER = 
  "precision highp float;\n" +
        
        "uniform float time;\n" +
        "uniform vec2 resolution;\n" +

  "void main() \n" +
  "{\n" +
  //центры сгустков
  " vec2 move1;\n" +
  " move1.x = cos(time)*0.4;\n" +
  " move1.y = sin(time*1.5)*0.4;\n" +
  " vec2 move2;\n" +  
  " move2.x = cos(time*2.0)*0.6;\n" +
  " move2.y = sin(time*3.0)*0.6;\n" +
  " vec2 move3;\n" +  
  " move3.x = sin(time*2.0)*0.8;\n" +
  " move3.y = cos(time*3.0)*0.8;\n" +
  " vec2 move4;\n" +  
  " move4.x = cos(time*1.8)*0.2;\n" +
  " move4.y = sin(time*3.0)*0.2;\n" +
  " vec2 move5;\n" +  
  " move5.x = cos(time*4.8)*1.2;\n" +
  " move5.y = sin(time*1.0)*0.5;\n" +
  " vec2 move6;\n" +  
  " move6.x = sin(time*1.8)*0.2;\n" +
  " move6.y = cos(time*1.1)*0.2;\n" +
  " vec2 move7;\n" +  
  " move7.x = cos(time*1.8)*0.2;\n" +
  " move7.y = sin(time*3.0)*0.2;\n" +
  " vec2 move8;\n" +  
  " move8.x = sin(time*0.8)*0.2;\n" +
  " move8.y = cos(time*0.3)*0.5;\n" +
  " vec2 move9;\n" +  
  " move9.x = cos(time*1.8)*0.9;\n" +
  " move9.y = sin(time)*5.4;\n" +
  //координаты
  " vec2 p = -1.0 + 2.0 * gl_FragCoord.xy / resolution.xy;\n" +
  //радиусы
  " float r1 =(dot(p-move1,p-move1))*16.0;\n" +
  " float r2 =(dot(p+move2,p+move2))*24.0;\n" +
  " float r3 =(dot(p+move3,p+move3))*32.0;\n" +
  " float r4 =(dot(p+move4,p+move4))*40.0;\n" +
  " float r5 =(dot(p+move5,p+move5))*42.0;\n" +
  " float r6 =(dot(p+move6,p+move6))*50.0;\n" +
  " float r7 =(dot(p+move7,p+move7))*56.0;\n" +
  " float r8 =(dot(p+move8,p+move8))*65.0;\n" +
  " float r9 =(dot(p+move9,p+move9))*70.0;\n" +
  //сумма сгустков
  " float metaball =(1.0/r1-1.0/r2+1.0/r3-1.0/r4+1.0/r5-1.0/r6+1.0/r7-1.0/r8+1.0/r9);\n" +
  //места разрыва сгустков
  " float col = pow(metaball,8.0);\n" +
  //вывод конечного цвета фрагмента
  " gl_FragColor = vec4(col+move2.x,col+move1.x,col+move4.y,0.8);\n" +
  "}\n";

  
  private ShockwaveShaderProgram() {
   super(PositionTextureCoordinatesShaderProgram.VERTEXSHADER, FRAGMENTSHADER);
  }
  
        public static int sUniformModelViewPositionMatrixLocation = ShaderProgramConstants.LOCATION_INVALID;
        public static int sUniformTexture0Location = ShaderProgramConstants.LOCATION_INVALID;
        public static int sUniformTimeLocation = ShaderProgramConstants.LOCATION_INVALID;
        public static int sUniformResolutionLocation = ShaderProgramConstants.LOCATION_INVALID;
        
        @Override
        protected void link(final GLState pGLState) throws ShaderProgramLinkException {
            GLES20.glBindAttribLocation(this.mProgramID, ShaderProgramConstants.ATTRIBUTE_POSITION_LOCATION, ShaderProgramConstants.ATTRIBUTE_POSITION);
            GLES20.glBindAttribLocation(this.mProgramID, ShaderProgramConstants.ATTRIBUTE_TEXTURECOORDINATES_LOCATION, ShaderProgramConstants.ATTRIBUTE_TEXTURECOORDINATES);

            super.link(pGLState);

            ShockwaveShaderProgram.sUniformModelViewPositionMatrixLocation = this.getUniformLocation(ShaderProgramConstants.UNIFORM_MODELVIEWPROJECTIONMATRIX);     
            ShockwaveShaderProgram.sUniformResolutionLocation = this.getUniformLocation("resolution");
            ShockwaveShaderProgram.sUniformTimeLocation = this.getUniformLocation("time");
        }
        
        @Override
        public void bind(final GLState pGLState, final VertexBufferObjectAttributes pVertexBufferObjectAttributes) {
            GLES20.glDisableVertexAttribArray(ShaderProgramConstants.ATTRIBUTE_COLOR_LOCATION);

            super.bind(pGLState, pVertexBufferObjectAttributes);
            
            GLES20.glUniformMatrix4fv(ShockwaveShaderProgram.sUniformModelViewPositionMatrixLocation, 1, false, pGLState.getModelViewProjectionGLMatrix(), 0);
            GLES20.glUniform2f(ShockwaveShaderProgram.sUniformResolutionLocation, WIDTH, HEIGHT);
            GLES20.glUniform1f(ShockwaveShaderProgram.sUniformTimeLocation, MetablobTest.Instance.mShockwaveTime);
        }

      
        @Override
        public void unbind(final GLState pGLState) throws ShaderProgramException {
            GLES20.glEnableVertexAttribArray(ShaderProgramConstants.ATTRIBUTE_COLOR_LOCATION);

            super.unbind(pGLState);
        }
 }


 @Override
 public boolean onSceneTouchEvent(Scene pScene, TouchEvent pSceneTouchEvent) {
  if (pSceneTouchEvent.getAction() == TouchEvent.ACTION_DOWN){
  mCenterX = pSceneTouchEvent.getMotionEvent().getX() / this.mCamera.getSurfaceWidth();
  mCenterY = 1-pSceneTouchEvent.getMotionEvent().getY() / this.mCamera.getSurfaceHeight();
  return true;
  }
  return false;
 } 
}

Копируйте, пользуйтесь, не забывайте про Adrian-а Boeing-а! :-)


Продвинутый пример смотрите в статье "Немного о Render-to-Texture..."
Скачать исходник с апк внутри.

понедельник, 23 июля 2012 г.

Пример шейдера на Android. Ripple Effect.

С недавнего времени заинтересовался шейдерами под Android используя в качестве движка AndEngine.
Копаясь в данной теме выяснил кое-что, но явно недостаточное для написания собственного шейдера. Тогда я решил пойти проторенной дорожкой - найти готовые шейдера и попробовать прикрутить их к своему аппу. Вот здесь находиться библиотека шейдеров часть которых мне удалось портировать под Android. Кроме того, для понимания процесса, я рекомендую статью на форуме AndEngine, кое-какие материалы я взял оттуда.

Вот, на мой взгляд, интересный шейдер под названием Pulse ('Pulse' by Danguafer/Silexars (2010)). По сути он представляет собой Ripple Effect (все любят эффект воды ёкарный бабаааай) с постоянно движущимся центром источника волн. 
Вот как *примерно* он будет выглядеть:

Попробуем воссоздать его для Android.

Коротко о том, что нам предстоит сделать:
1. Создать специальную текстуру, которая будет содержать проекцию обычной текстуры.
2. Создать специальный спрайт, который будет отрисовывать измененное шейдером изображение.
3. Создать текстуру с изображением из файла.
4. Создать на ее основе спрайт.
5. Написать программу шейдера.
6. Все это дело связать между собой и задать значения uniform-переменных.

Сразу оговорюсь, что шейдер в этом примере будет действовать на всю поверхность сцены т.е., вы можете добавить на сцену кучу спрайтов, но шейдер будет работать для всей сцены а не для каждого спрайта по отдельности. Как прицепить шейдер к конкретному спрайту я возможно расскажу в следующих сообщениях.

Приступим:

package com.expedition107.shadershock;

import java.io.IOException;
import java.io.InputStream;

import org.andengine.engine.Engine;
import org.andengine.engine.LimitedFPSEngine;
import org.andengine.engine.camera.Camera;
import org.andengine.engine.handler.IUpdateHandler;
import org.andengine.engine.options.EngineOptions;
import org.andengine.engine.options.ScreenOrientation;
import org.andengine.engine.options.resolutionpolicy.RatioResolutionPolicy;
import org.andengine.entity.modifier.LoopEntityModifier;
import org.andengine.entity.modifier.RotationByModifier;
import org.andengine.entity.scene.IOnSceneTouchListener;
import org.andengine.entity.scene.Scene;
import org.andengine.entity.scene.background.Background;
import org.andengine.entity.sprite.Sprite;
import org.andengine.entity.sprite.UncoloredSprite;
import org.andengine.entity.util.FPSLogger;
import org.andengine.input.touch.TouchEvent;
import org.andengine.opengl.shader.PositionTextureCoordinatesShaderProgram;
import org.andengine.opengl.shader.ShaderProgram;
import org.andengine.opengl.shader.constants.ShaderProgramConstants;
import org.andengine.opengl.shader.exception.ShaderProgramException;
import org.andengine.opengl.shader.exception.ShaderProgramLinkException;
import org.andengine.opengl.texture.ITexture;
import org.andengine.opengl.texture.PixelFormat;
import org.andengine.opengl.texture.bitmap.BitmapTexture;
import org.andengine.opengl.texture.region.ITextureRegion;
import org.andengine.opengl.texture.region.TextureRegionFactory;
import org.andengine.opengl.texture.render.RenderTexture;
import org.andengine.opengl.util.GLState;
import org.andengine.opengl.vbo.attribute.VertexBufferObjectAttributes;
import org.andengine.ui.activity.BaseGameActivity;
import org.andengine.util.adt.io.in.IInputStreamOpener;
import org.andengine.util.color.Color;

import android.opengl.GLES20;
import android.util.Log;

public class ShockwaveTest extends BaseGameActivity implements
  IOnSceneTouchListener {

 public static final int WIDTH = 800;

 public static final int HEIGHT = 480;

 protected static ShockwaveTest Instance;

 private Camera mCamera;

 private Sprite mSprite;

 private ITexture mTexture;

 private ITextureRegion mTextureRegion;

 private boolean mRenderTextureInitialized = false;

 private RenderTexture mRenderTexture;

 private Sprite mRenderTextureSprite;

 private float mShockwaveTime = 0f;

 @Override
 public EngineOptions onCreateEngineOptions() {

  mCamera = new Camera(0, 0, WIDTH, HEIGHT);

  final EngineOptions engineOptions = new EngineOptions(true,
    ScreenOrientation.LANDSCAPE_FIXED, new RatioResolutionPolicy(
      WIDTH, HEIGHT), this.mCamera);

  return engineOptions;

 }

 @Override
 public Engine onCreateEngine(final EngineOptions pEngineOptions) {

  return new LimitedFPSEngine(pEngineOptions, 120) {

   @Override
   public void onDrawFrame(GLState pGLState)

   throws InterruptedException {

    // при первой попытке нарисовать фрэйм инициализируем текстуру

    if (!mRenderTextureInitialized) {

     initRenderTexture(pGLState);

     mRenderTextureInitialized = true;

    }

    // рисуем нашу подставную текстуру

    mRenderTexture.begin(pGLState, false, true, Color.TRANSPARENT);

    {

     super.onDrawFrame(pGLState);

    }

    mRenderTexture.end(pGLState);

    pGLState.pushProjectionGLMatrix();

    pGLState.orthoProjectionGLMatrixf(0, mCamera.getSurfaceWidth(),
      0, mCamera.getSurfaceHeight(), -1, 1);

    {

     mRenderTextureSprite.onDraw(pGLState, mCamera);

    }

    pGLState.popProjectionGLMatrix();

   }

   // метод инициализации RenderTexture

   private void initRenderTexture(GLState pGLState) {

    mRenderTexture = new RenderTexture(this.getTextureManager(),
      mCamera.getSurfaceWidth(), mCamera.getSurfaceHeight(),
      PixelFormat.RGBA_8888);

    mRenderTexture.init(pGLState);

    mRenderTextureSprite = new UncoloredSprite(
      0f,
      0f,
      TextureRegionFactory.extractFromTexture(mRenderTexture),
      getVertexBufferObjectManager());

    mRenderTextureSprite.setShaderProgram(ShockwaveShaderProgram
      .getInstance());

   }
  };
 }

 @Override
 public void onCreateResources(
   OnCreateResourcesCallback pOnCreateResourcesCallback)
   throws Exception {

  Instance = this;

  try {
   this.mTexture = new BitmapTexture(this.getTextureManager(),
     new IInputStreamOpener() {
      public InputStream open() throws IOException {
       return getAssets().open("gfx/tex2.jpg");
      }
     });

   mTexture.load();

   this.mTextureRegion = TextureRegionFactory
     .extractFromTexture(mTexture);

  } catch (IOException e) {

  }

  this.getShaderProgramManager().loadShaderProgram(
    ShockwaveShaderProgram.getInstance());

  pOnCreateResourcesCallback.onCreateResourcesFinished();

 }

 @Override
 public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback)
   throws Exception {
  // Создаем главную сцену
  final Scene scene = new Scene();
  // Задаем цвет бэкграунда
  scene.setBackground(new Background(0.09804f, 0.6274f, 0.8784f));
  // Регистрируем FPSLogger чтобы оценить fps во время исполнения
  getEngine().registerUpdateHandler(new FPSLogger());
  // Назначением слушателя прикосновения пальца к сцене
  scene.setOnSceneTouchListener(this);

  pOnCreateSceneCallback.onCreateSceneFinished(scene);

 }

 @Override
 public void onPopulateScene(Scene pScene,
 OnPopulateSceneCallback pOnPopulateSceneCallback) throws Exception {

  // Создаем спрайт и растягиваем его на всю ширину и высоту сцены (можно
  // и не растягивать)
  this.mSprite = new Sprite(0f, 0f, 800, 480, this.mTextureRegion,
    getVertexBufferObjectManager());
  // Добавляем спрайт на сцену (иначе не увидим его)
  pScene.attachChild(mSprite);
  // Регистрируем хендлер обновления чтобы менять нашу переменную времени
  getEngine().registerUpdateHandler(new IUpdateHandler() {
   public void reset() {
    // TODO Auto-generated method stub
   }

   public void onUpdate(float pSecondsElapsed) {
    mShockwaveTime += pSecondsElapsed * 0.5;
   }
  });

  pOnPopulateSceneCallback.onPopulateSceneFinished();

 }

 // Класс программы шейдера

 public static class ShockwaveShaderProgram extends ShaderProgram {

  // Указатель на созданный экземпляр программы
  private static ShockwaveShaderProgram instance;
  // Метод возвращающий указаетль на созданный экземпляр
  public static ShockwaveShaderProgram getInstance() {
   if (instance == null)
    instance = new ShockwaveShaderProgram();
   return instance;
  }

  // Текст вершинного шейдера здесь отсутствует т.к. используется шейдер
  // из библиотеки AndEngine

  // Текст фрагментного шейдера

  public static final String FRAGMENTSHADER =
  "precision highp float;\n" +
  "uniform float time;\n" +
  "uniform vec2 resolution;\n" +
  "uniform sampler2D " + ShaderProgramConstants.UNIFORM_TEXTURE_0 + ";\n" +
  "varying mediump vec2 " + ShaderProgramConstants.VARYING_TEXTURECOORDINATES + ";\n" +
  "void main(void)\n" +
  "{\n" +
  "vec2 halfres = resolution.xy/2.0;\n" +
  "vec2 cPos = gl_FragCoord.xy;\n" +
  "cPos.x -= 0.5*halfres.x*sin(time/2.0)+0.3*halfres.x*cos(time)+halfres.x;\n" +
  "cPos.y -= 0.4*halfres.y*sin(time/5.0)+0.3*halfres.y*cos(time)+halfres.y;\n" +
  "float cLength = length(cPos);\n" +
  "vec2 uv = gl_FragCoord.xy/resolution.xy+(cPos/cLength)*sin(cLength/30.0-time*10.0)/25.0;\n" +
     "vec3 col = texture2D(" + ShaderProgramConstants.UNIFORM_TEXTURE_0 + ",uv).xyz;\n" +
  "gl_FragColor = vec4(col,1.0);\n" +
  "}\n";

  // Конструктор программы шейдера

  private ShockwaveShaderProgram() {
   super(PositionTextureCoordinatesShaderProgram.VERTEXSHADER,
     FRAGMENTSHADER);
  }

  // Константы для связывания внешних переменных с uniform-переменными
  // шейдеров

  public static int sUniformModelViewPositionMatrixLocation = ShaderProgramConstants.LOCATION_INVALID;
  public static int sUniformTexture0Location = ShaderProgramConstants.LOCATION_INVALID;
  public static int sUniformTimeLocation = ShaderProgramConstants.LOCATION_INVALID;
  public static int sUniformResolution = ShaderProgramConstants.LOCATION_INVALID;

  // Собственно метод линковки
  @Override
  protected void link(final GLState pGLState)
    throws ShaderProgramLinkException {

   GLES20.glBindAttribLocation(this.mProgramID,
     ShaderProgramConstants.ATTRIBUTE_POSITION_LOCATION,
     ShaderProgramConstants.ATTRIBUTE_POSITION);

   GLES20.glBindAttribLocation(
     this.mProgramID,
     ShaderProgramConstants.ATTRIBUTE_TEXTURECOORDINATES_LOCATION,
     ShaderProgramConstants.ATTRIBUTE_TEXTURECOORDINATES);

   super.link(pGLState);

   // Линковка нашей текстуры
   ShockwaveShaderProgram.sUniformModelViewPositionMatrixLocation = this
     .getUniformLocation(ShaderProgramConstants.UNIFORM_MODELVIEWPROJECTIONMATRIX);
   ShockwaveShaderProgram.sUniformTexture0Location = this
     .getUniformLocation(ShaderProgramConstants.UNIFORM_TEXTURE_0);
   // Линковка переменной resolution
   ShockwaveShaderProgram.sUniformResolution = this
     .getUniformLocation("resolution");
   // Линковка переменной time
   ShockwaveShaderProgram.sUniformTimeLocation = this
     .getUniformLocation("time");

  }

  // Метод для связки переменных с переменными шейдера

  @Override
  public void bind(final GLState pGLState,
    final VertexBufferObjectAttributes pVertexBufferObjectAttributes) {

   GLES20.glDisableVertexAttribArray(ShaderProgramConstants.ATTRIBUTE_COLOR_LOCATION);

   super.bind(pGLState, pVertexBufferObjectAttributes);

   GLES20.glUniformMatrix4fv(
     ShockwaveShaderProgram.sUniformModelViewPositionMatrixLocation,
     1, false, pGLState.getModelViewProjectionGLMatrix(), 0);
   GLES20.glUniform1i(ShockwaveShaderProgram.sUniformTexture0Location,0);

   GLES20.glUniform1f(ShockwaveShaderProgram.sUniformTimeLocation,
     ShockwaveTest.Instance.mShockwaveTime);

   GLES20.glUniform2f(ShockwaveShaderProgram.sUniformResolution,
     ShockwaveTest.Instance.mCamera.getSurfaceWidth(),
     ShockwaveTest.Instance.mCamera.getSurfaceHeight());

  }

  @Override
  public void unbind(final GLState pGLState)
    throws ShaderProgramException {
   GLES20.glEnableVertexAttribArray(ShaderProgramConstants.ATTRIBUTE_COLOR_LOCATION);
   super.unbind(pGLState);
  }
 }

 // здесь при касании переменная mShockwaveTime сбрасывается в 0, что
 // незамедлительно передается
 // переменной time внутри шейдера и мы это увидим прикоснувшись к сцене

 @Override
 public boolean onSceneTouchEvent(Scene pScene, TouchEvent pSceneTouchEvent) {
  if (pSceneTouchEvent.getAction() == TouchEvent.ACTION_DOWN) {
   mShockwaveTime = 0;
  }
  return false;
 }
}

Несколько замечаний по коду:
1. Текстура, содержащая картинку, в этом примере грузиться немного непривычным для меня способом. Мне привычнее делать так:

final BitmapTextureAtlas texture = new BitmapTextureAtlas(
    getTextureManager(), 800, 400, TextureOptions.NEAREST);
mRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset(
    textureMain, this, "gfx/kartinko.png", 0, 0);

Но в данном конкретном случае подход оправдан, так как для быстрой проверки, не надо будет указывать размеры текстуры в зависимости от размеров картинки в файле.
2. Получившийся у нас эффект от шейдера не будет выглядеть так как на картинке выше, она сделана скриншотом с WebGL и надо понимать, что видео-карта компьютера немножко получше чем карта смартфона.
3. Не забудьте бросить в assets/gfx какой-нибудь файл с картинкой (у меня tex1.jpg).
4. Обратите внимание, что программа шейдера в Java-коде по сути представляет собой строку, т.е. вы можете наделать разных файлов с текстами шейдеров и подгружать их стандартными методами Java по мере необходимости.
5. В AndEngine для описания шейдеров используется язык GLSL.
6. Поиграйте с кодом шейдера. Попробуйте добавить свои uniform-переменные (например, при касании сцены задавать начальную точку движения пульса) и связать их с переменными в своей программе.

Ну что же, вот такой вот пример получился.
Кому надо, можете спокойно копировать код и использовать как вам потребуется, но помните: шейдер хоть и изменен, но все же у него есть автор (приведен выше).  Полностью проект выкладывать не буду, потому как предполагается, что у вас уже стоит Eclipse, есть свой workspace и подключена библиотека AndEngine GLES2. В следующем сообщении распишу еще один немного модифицированный шейдер из той же библиотеки, который называется Metablob. Очень симпатишный. :)  Всем удачи!

Продвинутый пример с шейдером смотрите в статье "Немного о Render-to-Texture..."
Проект на GitHub

понедельник, 9 июля 2012 г.

Отображение графиков на Android. AChartEngine. AsyncTask.

Пару недель назад возникла необходимость в отображении данных в виде графика. Покопавшись в интернетах и перебрав несколько вариантов, обратил внимание на библиотеку AChartEngine. В библиотеке реализован огромный потенциал по построению диаграмм и трендов, и самое главное - ее использование оказалось интуитивно понятным даже для такого новичка в Java как я.
Вместе с библиотекой идет хороший пример с демонстрацией возможностей библиотеки и, судя по отзывам на stackoverflow, она является очень популярной среди разработчиков.

Цель данного сообщения - показать обрубленную версию официального примера для более легкого понимания процесса вывода тренда, пример использования AsyncTask (хорошие статьи по использованию AsyncTask и загрузку данных из интернет можно прочитать на habrahabr.ru.),  для получения данных с WEB, а также прикрепления AChartEngine-а к реальной задаче.

Процесс подключения библиотеки achartengine-1.0.0.jar стандартный, рассказывать тут нечего. Единственное на что стоит обратить внимание - после подключения библиотеки убедитесь чтобы в Project Properties->Java Build Path-> Order&Export эта библиотека стояла самой первой с установленной галочкой, иначе получите ClassNotFoundException.

Итак, что мне было нужно от либы: построить график изменяющегося во времени физ.параметра (дискретность данных непостоянна, т.е. временные интервалы между значениями почти всегда разные), данные физ.параметра я получаю с WEB-сервера посредством http-запроса к php-скрипту.

Общий механизм получения данных:
I. В асинхронной задаче выполняем запрос к серверу посредством передачи параметров запроса php-скрипту.
II. php-скрипт соединяется с СУБД и выполняет хранимую процедуру.
III.Получаем данные для графика
IV. Парсим полученную строку и заполняем серию для нашего чарта.

Так, шаг за шагом и пойдем:
php-скрипт, в моем случае, принимает три параметра:
1 - идентификатор параметра (tag_id);
2 - дата/время начала временного промежутка
3 - дата/время окончания временного промежутка

Запрос к серверу может выглядеть так:
http://www.my_super_mega_web_server.ru/android/?start=2011-11-06%201:15:00&end=2011-11-07%200:00:00&tag=2116 (это нерабочая ссылка ;-) )

Все три параметра будут меняться в зависимости от того, что хочет посмотреть пользователь, поэтому нам необходимо создать функцию которая динамически собирает такой URL.

php-скрипт возвращает нам данные вот в таком виде (кому интересно - это температура воздуха на улице):

06.11.11 01:19     -11.46
06.11.11 01:31     -11.31
06.11.11 01:33     -11.46
06.11.11 01:47     -11.61
06.11.11 01:49     -11.76
06.11.11 02:05     -11.91
06.11.11 02:18     -12.06
06.11.11 02:21     -12.21
Код скрипта здесь рассматривать не будем, это за рамками сообщения.

По сути - это обычные строки, разделенные на дату/время и значение знаком табуляции.
Предвижу комментарии типа "почему строки, почему не бинарные данные в виде datetime и float?" Отвечаю: потому, что скрипт писал не я, + это пилотный, так сказать, проект.

Запрос данных и построение графика будет происходить сразу при создании активити, поэтому необходимо учесть, что при смене ориентации экрана - активити пересоздастся и снова начнется загрузка данных. В моем случае нет нужды отображать график в портретном режиме, поэтому я фиксирую разворот в положении landscape.

Код класса наследованного от AsyncTask:

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.StringTokenizer;

import android.os.AsyncTask;
import android.util.Log;
import android.widget.Toast;

public class DownloadTagDataAsyncTask extends
  AsyncTask<String, Void, String> {

 private HttpURLConnection mHttpConn;
 
 @Override
 protected String doInBackground(String... params) {
  String strURL = "";
  if (params.length != 0) {
   strURL = params[0];
  }
  return DownloadData(strURL);
 }

 @Override
 protected void onProgressUpdate(Void... values) {
  super.onProgressUpdate(values);
 }

 @Override
 protected void onPostExecute(String result) {
  super.onPostExecute(result);
  if (hasError){
   hasError = false;
   Toast.makeText(GraphActivity.Instance, "Ошибка соединения. Повторите попытку.", Toast.LENGTH_SHORT);
   GraphActivity.Instance.finish();
   return;
  }
  StringTokenizer allData = new StringTokenizer(result, "\r\n", false);
  int l = 0;
  while (allData.hasMoreTokens()) {

   String s = allData.nextToken();
   l++;
   if (s.equals(" ")) {
    break;
   }
  }
  
  float[] values = new float[l];
  Date[] times = new Date[l];
  
  allData = new StringTokenizer(result, "\r\n", false);
  int i = 0;
  while (allData.hasMoreTokens() && (i < l-1)) {
   String s = allData.nextToken();
   if (s.equals(" ")) {
    break;
   }
   StringTokenizer record = new StringTokenizer(s, "\u0009", false);
   if (record.hasMoreTokens()) {
    String timePoint = record.nextToken();
    String valData = record.nextToken();
    values[i] = Float.valueOf(valData);
    SimpleDateFormat sdf = new SimpleDateFormat("yy.dd.MM HH:mm");
    Date ddd = null;
    try {
    ddd = sdf.parse(timePoint);
    times[i] = new Date();
    times[i] = ddd;
    }catch(Exception e) {

    }
    
    
   }
   i++;
  }
  GraphActivity.Instance.getDateDemoDataset(values, times);
  GraphActivity.Instance.getDemoRenderer();
  GraphActivity.Instance.buildChart();
  GraphActivity.Instance.endProgress();
 }

 private InputStream OpenHttpConnection(String urlString) throws IOException {
  InputStream in = null;
  int response = -1;

  URL url = new URL(urlString);
  URLConnection conn = url.openConnection();
  mHttpConn = null;
  if (!(conn instanceof HttpURLConnection))
   throw new IOException("Error. HTTP connection failed.");
  try {
   mHttpConn = (HttpURLConnection) conn;
   mHttpConn.setAllowUserInteraction(false);
   mHttpConn.setInstanceFollowRedirects(true);
   mHttpConn.setRequestMethod("GET");
   mHttpConn.connect();
   response = mHttpConn.getResponseCode();
   if (response == HttpURLConnection.HTTP_OK) {
    in = mHttpConn.getInputStream();
   }
  } catch (Exception ex) {
   hasError = true;
   
   throw new IOException("Connection error.");
  }
  return in;
 }

 private String DownloadData(String URL) {
  int BUFFER_SIZE = 20000;
  InputStream in = null;
  String str = "";
  try {
   in = OpenHttpConnection(URL);
  } catch (IOException e1) {

   e1.printStackTrace();
   return str;
  }
  if (in == null) {
   return str;
  }
  InputStreamReader isr = null;
  try {
   isr = new InputStreamReader(in, "cp-1251");
  } catch (UnsupportedEncodingException e1) {
   e1.printStackTrace();
  }
  int charRead;

  char[] inputBuffer = new char[BUFFER_SIZE];
  try {
   while ((charRead = isr.read(inputBuffer)) > 0) {
    // ---convert the chars to a String---
    String readString = String
      .copyValueOf(inputBuffer, 0, charRead);
    str += readString;
    inputBuffer = new char[BUFFER_SIZE];
   }
   in.close();
  } catch (IOException e) {
   e.printStackTrace();
   return "";
  }

  if (mHttpConn != null) {
   mHttpConn.disconnect();
  }
  return str;
 }

}
GraphActivity.Instance - это ссылка на саму себя, чтобы из AsyncTask.onPostExecute(...) можно было обратиться к методам нашей активити.
Теперь код GraphActivity:

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

import org.achartengine.ChartFactory;
import org.achartengine.GraphicalView;
import org.achartengine.model.TimeSeries;
import org.achartengine.model.XYMultipleSeriesDataset;
import org.achartengine.renderer.XYMultipleSeriesRenderer;
import org.achartengine.renderer.XYSeriesRenderer;

import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.view.Window;
import android.view.WindowManager;
import android.widget.LinearLayout;
import android.widget.ProgressBar;

public class GraphActivity extends Activity {

 private View mMainView;
 protected static GraphActivity Instance;
 private int mTag;
 private String mSeriesCaption = "Chart";

 private XYMultipleSeriesDataset mDataset = new XYMultipleSeriesDataset();
 private XYMultipleSeriesRenderer mRenderer = new XYMultipleSeriesRenderer();
 private GraphicalView mChartView;
 
 private static ProgressDialog pd;

 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  
  //Remove title bar
  this.requestWindowFeature(Window.FEATURE_NO_TITLE);

  //Remove notification bar
  this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
  
  Instance = this;

  Intent intent = this.getIntent();
  
  if (intent.getExtras() != null) {
   mTag = intent.getExtras().getInt("tagid", 0);
   mSeriesCaption = intent.getExtras().getString("descr");
  }

  
  pd = new ProgressDialog(this);
  pd.setMessage("Загрузка данных. Ждите...");
  startProgress();
  
  mMainView = LayoutInflater.from(this).inflate(R.layout.graph, null);
  setContentView(mMainView);
  
  
  DownloadTagDataAsyncTask dat = new DownloadTagDataAsyncTask();
  dat.execute(makeURL(mTag));
 }
 
 public void getDateDemoDataset(float[] pValues, Date[] pTimes) {
  XYMultipleSeriesDataset dataset = new XYMultipleSeriesDataset();
     TimeSeries series = new TimeSeries(mSeriesCaption);
     for (int k = 0; k < pValues.length-1; k++) {
      series.add(pTimes[k], pValues[k]);
     }
     
     dataset.addSeries(series);
     mDataset = dataset;
   }
 
 public void getDemoRenderer() {
     XYMultipleSeriesRenderer renderer = new XYMultipleSeriesRenderer();
     renderer.setAxisTitleTextSize(16);
     renderer.setChartTitleTextSize(20);
     renderer.setLabelsTextSize(15);
     renderer.setLegendTextSize(20);
     renderer.setShowGrid(true);
     renderer.setGridColor(Color.GRAY);
     XYSeriesRenderer r = new XYSeriesRenderer();
     r.setColor(Color.GREEN);
     r.setFillPoints(true);
     renderer.addSeriesRenderer(r);
     mRenderer = renderer;
   }
 
 public void buildChart(){
  if (mChartView == null) {
        LinearLayout layout = (LinearLayout) findViewById(R.id.chart);
        mChartView = ChartFactory.getTimeChartView(this, mDataset, mRenderer,"HH:mm");
        //mRenderer.setClickEnabled(true);
        //mRenderer.setSelectableBuffer(100);
        layout.addView(mChartView, new LayoutParams(LayoutParams.FILL_PARENT,
            LayoutParams.FILL_PARENT));
      } else {
        mChartView.repaint();
      } 
 }
 
 public String makeURL(int pTagId) {
  Calendar cal = new GregorianCalendar();
  
  Date currentDate = new Date(); 
  Long time = currentDate.getTime(); 
  long anotherDate = -1; 
  time = time + (60*60*1000*anotherDate); 
  currentDate = new Date(time); 
  
  String res = "http://www.my_super_mega_web_server.ru/android/?start="
   + new SimpleDateFormat("yyyy-MM-dd'%20'HH:mm:ss").format(currentDate)
   + "&end="
   + new SimpleDateFormat("yyyy-MM-dd'%20'HH:mm:ss").format(cal.getTime()) + "&tag=" + pTagId;

  return res;
 }
 
 public void startProgress(){
  pd.show();
 }

 public void endProgress(){
  pd.hide();
  pd.dismiss();
 }
}

Поясню. При создании GraphActivity ей в Intent передается посылка содержащая идентификатор параметра(tagid) и его текстовое описание(descr), чтобы нам было чем подписать сам график. Если Вы собираетесь адаптировать пример под свои нужды, можете напрямую задать эти параметры и быстро посмотреть результат просто сделав эту активити главной. Итак, первый параметр для php-скрипта мы передали через Intent, параметр конца периода у нас будет текущее время, а параметр начала периода =  конец - 2 часа.
За формирование URL у нас отвечает функция makeURL(int pTag).

У себя в программе эту активити я вызываю нажатием на соответствующую View (SimpleIndicator - это собственный компонент, не ищите его в палитре):

@Override
 public boolean onTouch(View v, MotionEvent event) {
  if (event.getAction() == MotionEvent.ACTION_DOWN){
   if (v instanceof SimpleIndicator){
    Intent intent = new Intent(this, GraphActivity.class);
    intent.putExtra("tagid", ((SimpleIndicator)v).getmTag());
    intent.putExtra("descr", ((SimpleIndicator)v).getDescr());
    this.startActivity(intent);
   } 
  }
  
  return false;
 }

Вот собственно и все.

суббота, 7 июля 2012 г.

Делаем облака. ParallaxSprite.

Все что описано в этом сообщении, касается движка AndEngine версии GLES2, под версию GLES1 не проверялось.

Мне встречалась попытка реализации такого класса на stackoverflow.com, но приведенный там код у меня не заработал и сейчас, к сожалению,  я не могу дать на него прямую ссылку.

Итак, к делу.
В AndEngine есть великолепный класс - AutoParallaxBackground который позволяет сделать непрерывно движущуюся картинку бэкграунда. Конечно, картинка должна быть согласована, т.е. начало картинки должно совпадать с концом (чисто визуально), иначе, все получится, но будет виден разрыв. Благодаря такому поведению спрайта, создается впечатление глубины сцены, а если его сделать многослойным - получается очень красиво и профессионально.Ну про AutoParalaxBackground мы можем подробно узнать из AndEngine examples.
А, что делать, если нам нужно такое же "поведение" спрайта, но не на бэкграунде, а поверх сцены? Например, сделать непрерывно движущиеся облака поверх солнца или там луны, и при этом к объекту, который перекрывается облаками нам нужен доступ как к обычному спрайту (это максимально развязывает руки дизайнеру и разработчику), К сожалению, AndEngine не предоставляет такого класса... Хотя почему к сожалению? Есть повод пораскинуть мозгами! (а у меня есть тема для блога) Короче, начнем раскидывать:-)

public class ParallaxSprite extends Sprite {

    private float mParallaxSpeed;
    private float mOffsetX = 0;

    public ParallaxSprite(float pX, float pY, float mParallaxSpeed, ITextureRegion pTextureRegion, VertexBufferObjectManager pVertexBufferObjectManager) {
        super(pX, pY, pTextureRegion, pVertexBufferObjectManager);

        this.mParallaxSpeed = mParallaxSpeed;
        this.mOffsetX = pX - (pX * mParallaxSpeed);
    }

    @Override
    public void onManagedUpdate(float pSecondsElapsed){
     super.onManagedUpdate(pSecondsElapsed);
     this.mOffsetX += this.mParallaxSpeed * pSecondsElapsed; 
    }
    
    @Override
    protected void onManagedDraw(GLState pGLState, Camera pCamera) {
     pGLState.pushModelViewGLMatrix();
        {
         final float shapeWidthScaled = this.getWidthScaled();
         final float cameraWidth = pCamera.getWidth();
            float baseOffsetX = (this.mOffsetX * this.mParallaxSpeed)% shapeWidthScaled;

            while(baseOffsetX > 0) {
             baseOffsetX -= shapeWidthScaled;
   }
            
            pGLState.translateModelViewGLMatrixf(baseOffsetX, 0, 0);
            
            float currentMaxX = baseOffsetX;
   
   do {
    
    this.preDraw(pGLState, pCamera);
             this.draw(pGLState, pCamera);
             this.postDraw(pGLState, pCamera);
    pGLState.translateModelViewGLMatrixf(shapeWidthScaled, 0, 0);
    currentMaxX += shapeWidthScaled;
   } while(currentMaxX < cameraWidth);
        }
        pGLState.popModelViewGLMatrix();
    }
    
    public void setSpeed(float pSpeed){
     this.mParallaxSpeed = pSpeed;
    }
    
    public float getSpeed(float pSpeed){
     return this.mParallaxSpeed;
    }
}
Использование:
ParallaxSprite spriteClouds1 = new ParallaxSprite(0, 0, 5.0f, mRegionClouds,
    this.getVertexBufferObjectManager())

0,0 - это координаты левого верхнего угла спрайта относительно сцены.
mRegionClouds - это регион текстуры нашего спрайта.
this.getVertexBufferManager() - это так надо ;-)
Вот и все.
Что мы натворили: перекрыли метод отрисовки стандартного спрайта немного измененным кодом AutoParallaxBackground. Если интересуют какие именно отличия - велком в исходный код AutoParallaxBackground.
Еще тройка интересных, на мой взгляд, замечаний:
1.Здесь реализовано горизонтальное движение, а если хотим вертикальное? Посмотрите на этот незамысловатый код внимательней и увидите, что его ничего не стоит переделать и двигать по оси Y, причем делать это в зависимости от ситуации. Зачем двигать спрайт вертикально? Чтобы сделать джампер круче Doodle например ;-)
2.Наши облака представляют собой два спрайта имеющих одну и ту же текстуру. Они занимают всю сцену и имеют разные скорости, таким образом, создается впечатление, что облака постоянно меняются, хотя на самом деле две одинаковых картинки движутся относительно друг друга с разной скоростью.
3. Вы можете изменить направление движения поставив третий параметр < 0.

Кроме того, эта штука реализована у нас в обоях. Добро пожаловать и прокомментировать!

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

Создание собственного Preference для живых обоев

Всем привет! Вот случилось создать свой преференс для расширения настроек живых обоев поскольку поиски в интернете к подобному не привели (иногда так не хочется отвлекаться на всякую мелочь, а хочется копипаст). Хочу поделиться с заинтересованными, берите, пользуйтесь на здоровье:-)  Статья рассчитана на новичков, поскольку сам являюсь таковым.

Все, что я тут напишу базируется в первую очередь на вот этом примере.

Итак, разговор пойдет о создании собственного Preference. 

Немного предыстории. В очередной попытке популяризации наших продуктов на play.google, мы решили сделать ход конем - бесплатные живые обои. 
Целей преследуется несколько - проверить на сколько близка тема хорора андроид-пользователю, изучение расширения AndEngineLiveWallpaperExtension движка AndEngine, изучение "спроса" на живые обои и, наконец, популяризация остальных продуктов компании посредством установок ссылок из активити настроек живых обоев. Рекламу, было решено не впихивать, так как мониторинг показал, что присутствие рекламы в обоях очень раздражает пользователя и не столько из-за наличия таковой, сколько от массы различных permission.Кроме того, самые "продвинутые" путают мошенническую рекламу с  вирусами и, конечно, обвиняют, в собственной тупости глупости, авторов и приложение.
В общем задача создать свой преференс при клике на который пользователь переходил бы куда нам надо.
Выглядеть это должно примерно так:


Начинаем ваять. 
1. Собственно скелет нашего преференса. Создаем свой xml-файл (например text_link_pref.xml) для лэйаута нашего преференса и помещаем его в папку res/layout:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:paddingBottom="0dp"
    android:paddingLeft="20dp"
    android:paddingRight="20dp"
    android:paddingTop="10dp" >

    <TextView
        android:id="@+id/textMain"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        android:layout_marginLeft="57dp"
        android:gravity="top"
        android:text="TextView"
        android:textColor="#ffff"
        android:textSize="25dip" />


    <ImageView
        android:id="@+id/imageView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:src="@drawable/ic_playlogo" />

    <TextView
        android:id="@+id/textSummary"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/imageView1"
        android:layout_below="@+id/imageView1"
        android:text="TextView"
        android:textColor="#00C618"
        android:textStyle="normal" />

</RelativeLayout>
Скелет готов. Теперь, кроме того, что у нас есть спозиционированные относительно друг друга элементы интерфейса, у нас должна быть возможность задавать основной текст (title), картинку (Image) и короткое описание (Summary) внизу, ну и конечно самое главное - LINK(!) на наши веб-ресурсы. Создадим еще один xml-файл (attrs.xml) в котором будет описание собственных атрибутов для нашего преференса и поместим его в папку res/values:
<?xml version="1.0" encoding="utf-8"?>
<resources>
 
 <declare-styleable name="com.expedition107.wp.moon.nightmare.TextLinkPreference">
  <attr name="link" format="string" />
  <attr name="text" format="string" />
  <attr name="summary" format="string" />
  <attr name="picsrc" format="reference" />
 </declare-styleable>
 
</resources>
Атрибуты готовы вроде бы. Теперь, собственно, переходим к созданию собственного класса. Спросите - почему назвал TextLink... потому что думал обойтись текстом, но друг настоял на картинке. В общем есть картинка, а название так и прижилось. Наследуемся от Preference:
package com.expedition107.wp.moon.nightmare;

import com.expedition107.wp.moon.nightmare.R;

import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.preference.Preference;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;

public class TextLinkPreference extends Preference implements OnClickListener {
 private static final String PREFERENCE_NS = "http://schemas.android.com/apk/res/com.expedition107.wp.moon.nightmare";
 private static final String ANDROID_NS = "http://schemas.android.com/apk/res/android";
 
 private final String mLink;  
 private final String mText;
 private final String mSummary;
 
 private TextView mTextMain;
 private TextView mTextSummary;
 private ImageView mImageView;
 private Drawable mImageRes;

 public TextLinkPreference(Context context, AttributeSet attrs) {
  super(context, attrs);
  
  mLink = attrs.getAttributeValue(PREFERENCE_NS, "link");
  mText = attrs.getAttributeValue(PREFERENCE_NS, "text");
  mSummary = attrs.getAttributeValue(PREFERENCE_NS, "summary");
  mImageRes = context.getResources().getDrawable(attrs.getAttributeResourceValue(PREFERENCE_NS, "picsrc",R.drawable.ic_launcher));
  
 }
 
 @Override
    protected View onCreateView(ViewGroup parent){
        
        RelativeLayout layout =  null;
        
        try {
            LayoutInflater mInflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);

            layout = (RelativeLayout)mInflater.inflate(R.layout.text_link_pref, parent, false);
            
      mTextMain = (TextView)layout.findViewById(R.id.textMain);
      mTextSummary = (TextView)layout.findViewById(R.id.textSummary);
      mImageView = (ImageView)layout.findViewById(R.id.imageView1);
      mTextMain.setText(mText);
      mTextSummary.setText(mSummary);
      
      
      mImageView.setImageDrawable(mImageRes);
      
      layout.setOnClickListener(this);
        }
        catch(Exception e)
        {
            //Log.e("wp_steel", "Error creating seek bar preference", e);
        }
        
        return layout;
        
    }
    
    @Override
    public void onBindView(View view) {
        super.onBindView(view);

    }
    

 @Override
 public void onClick(View v) {
  Intent intent = new Intent(Intent.ACTION_VIEW);
  intent.setData(Uri
    .parse(mLink));
  v.getContext().startActivity(intent);
  
 }
}
Еще одна мелочь: одноименные файлы-картинки используемые для наших линков будут лежать в папках res/drawable-hdpi (...ldpi,mdpi, xhdpi) в соответствии с resolution. У меня эти файлы называются (для facebook и play.google соответственно) ic_fblogo.png и ic_playlogo.png Вот вроде и все. Файл со структурой preferences может выглядеть так:
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:customattrs="http://schemas.android.com/apk/res/com.expedition107.wp.moon.nightmare"
    android:key="settings"
    android:title="Moon Settings" >

    <PreferenceScreen
        android:enabled="true"
        android:key="supportscreen"
        android:orderingFromXml="true"
        android:summary="Your voice is very important!"
        android:title="Support us" >
        
        <com.expedition107.wp.moon.nightmare.TextLinkPreference
            android:key="fblink"
            android:persistent="true"
            customattrs:link="http://www.facebook.com/pages/Expedition107/199617420087112"
            customattrs:summary="Like us on Facebook!"
            customattrs:text="Facebook!" 
            customattrs:picsrc = "@drawable/ic_fblogo"/>
        
       <!--  <com.expedition107.wp.moon.nightmare.TextLinkPreference
            android:key="twitterlink"
            android:persistent="true"
            customattrs:link="http://twitter.com"
            customattrs:summary="Follow us on Twitter!"
            customattrs:text="Twitter!" 
            customattrs:picsrc = "@drawable/ic_twitlogo"/> -->
        
        <com.expedition107.wp.moon.nightmare.TextLinkPreference
            android:key="market"
            android:persistent="true"
            customattrs:link="https://play.google.com/store/apps/developer?id=Expedition107"
            customattrs:summary="Look more goodies on play.google!"
            customattrs:text="Expedition107" 
            customattrs:picsrc = "@drawable/ic_playlogo"/>
    
    </PreferenceScreen>

</PreferenceScreen>
Вроде ничего не забыл. Если встретили ошибки, неточности - давайте исправим ;) 

PS В процессе создания этого чуда природы у меня возник вопрос - есть ли возможность как-то вытащить значения атрибутов элементов по умолчанию? Например цвет summary всегда синий в стандартных преференсах, я тоже хочу синий но не хочу указывать его конкретное значение по понятным причинам, поэтому в примере сделал зеленым , тоже касается и размеров текста и прочего. Кто знает - расскажите, плиз.
Ну а вот, собственно, и наши обои на play.google