понедельник, 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