Crea un juego de rompecabezas para Android con la API Dolby Audio
This sponsored post features a product relevant to our readers while meeting our editorial guidelines for being objective and educational.
Spanish (Español) translation by CYC (you can also view the original English article)
En este tutorial, te mostraré cómo usar la API de Dolby Audio para mejorar el sonido en tus aplicaciones de Android. Para mostrarte cómo usar la API de Dolby Audio, crearemos un juego de rompecabezas simple pero divertido.
Introducción
En el concurrido mercado de dispositivos móviles de hoy en día, es importante que tus aplicaciones sean lo más atractivas posible. Mejorar la experiencia auditiva de una aplicación puede ser tan atractivo para el usuario como tener una interfaz de usuario deslumbrante.
El sonido creado por una aplicación es una forma de interacción entre el usuario y la aplicación que con demasiada frecuencia se pasa por alto. Sin embargo, esto significa que ofrecer una gran experiencia auditiva puede ayudar a que tu aplicación se destaque entre la multitud.
Dolby Digital Plus es un códec de audio avanzado que se puede usar en aplicaciones móviles utilizando -la fácil de usar- API de Dolby Audio. Dolby ha hecho que su API esté disponible para varias plataformas, incluyendo Android y Kindle Fire. En este tutorial, veremos la implementación de la API en Android.
La API Dolby Audio para Android es compatible con una amplia gama de dispositivos Android. Esto significa que tus aplicaciones y juegos Android pueden disfrutar de audio inmersivo de alta fidelidad con solo unos pocos minutos de trabajo integrando Dolby's Audio API. Exploremos qué se necesita para integrar la API creando un juego de acertijos.
1. Resumen
En la primera parte de este tutorial, te mostraré cómo crear un divertido juego de rompecabezas. Como el objetivo de este tutorial es integrar Dolby Audio API, no entraré en demasiados detalles y espero que ya estés familiarizado con los aspectos básicos del desarrollo en Android. En la segunda parte de este artículo, haremos un acercamiento a la integración de la API de Dolby Audio en una aplicación de Android.
Vamos a hacer un juego de rompecabezas tradicional para Android. El objetivo del juego es deslizar una pieza de rompecabezas en la ranura vacía del tablero de rompecabezas para mover las piezas del rompecabezas. El jugador debe repetir este proceso hasta que cada pieza del rompecabezas esté en el orden correcto. Como puedes ver en la captura de pantalla a continuación, he agregado un número a cada pieza del rompecabezas. Esto facilitará el seguimiento de las piezas del rompecabezas y el orden en que se encuentran.



Para que el juego sea más atractivo, te mostraré cómo usar imágenes personalizadas y cómo tomar una foto para crear tus propios rompecabezas únicos. También agregaremos un botón aleatorio para reorganizar las piezas del rompecabezas para comenzar un nuevo juego.
2. Empezando
Paso 1
No importa qué IDE uses, pero para este tutorial usaré JetBrains IntelliJ Idea. Abre tu IDE de elección y crea un nuevo proyecto para tu aplicación Android. Asegúrate de crear una clase principal Activity
y un diseño XML
.
Paso 2
Primero configuremos el archivo de manifiesto de la aplicación. En el nodo de application
del archivo de manifiesto, establece hardwareAccelerated
en true
. Esto aumentará el rendimiento de representación de tu aplicación incluso para juegos 2D como el que estamos a punto de crear.
android:hardwareAccelerated="true"
En el siguiente paso, especificamos los tamaños de pantalla que admitirá nuestra aplicación. Para los juegos, por lo general me concentro en dispositivos con pantallas más grandes, pero esta elección depende completamente de ti.
<supports-screens android:largeScreens="true" android:anyDensity="true" android:normalScreens="true" android:smallScreens="false" android:xlargeScreens="true"/>
En el nodo activity
del archivo de manifiesto, agrega un nodo llamado configChanges
y establece su valor orientation
como se muestra a continuación. Puedes encontrar más información sobre esta configuración en el sitio web del desarrollador.
android:configChanges="orientation"
Antes de continuar, agrega dos nodos uses-permission
para habilitar la vibración y el acceso de escritura para nuestro juego. Inserta el siguiente fragmento antes del nodo application
en el archivo de manifiesto.
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
Paso 3
Agreguemos también los recursos que usaremos más adelante en este tutorial. Comienza agregando la imagen que deseas usar para el rompecabezas. Agrégalo a la carpeta drawable
de tu proyecto. Elegí agregar la imagen a la carpeta drawable-hdpi
de mi proyecto.
Por último, pero no menos importante, agrega los archivos de sonido que quieras usar en tu juego. En la carpeta res
de tu proyecto, crea un nuevo directorio llamado raw
y agrega los archivos de sonido a esta carpeta. Para el propósito de este tutorial, he agregado dos archivos de audio. El primer sonido se reproduce cuando el jugador mueve una pieza del rompecabezas, mientras que el segundo se reproduce cuando el juego está terminado, es decir, cuando el jugador completa el rompecabezas. Ambos sonidos están disponibles en SoundBible. El primer sonido está licenciado bajo la licencia Creative Commons Attribution 3.0 y fue grabado por Mike Koenig.
3. Creando el cerebro del juego
Como mencioné anteriormente, no explicaré el proceso de creación del juego en detalle ya que el objetivo de este tutorial es integrar la API Dolby Audio. En los siguientes pasos, te guiaré por los pasos que debes seguir para crear el juego de rompecabezas.
Comenzamos creando una nueva clase, SlidePuzzle
, que será el cerebro del juego. Cada movimiento realizado en el rompecabezas es procesado y rastreado por una instancia de esta clase usando matemática simple.
Es una parte importante del juego, ya que determinará qué fichas se pueden mover y en qué dirección. La clase también nos notificará cuando se complete el rompecabezas.
package com.dolby.DolbyPuzzle; public class SlidePuzzle { }
Comenzaremos por declarar una serie de variables que necesitaremos un poco más adelante. Eche un vistazo al siguiente fragmento de código en el que declaro variables para las cuatro direcciones posibles en las que se pueden mover las piezas del rompecabezas, dos matrices de números enteros para las direcciones horizontal y vertical, y una matriz para las fichas del rompecabezas. También declaramos y creamos una instancia de la clase Random
, que usaremos más adelante en este tutorial.
public static final int DIRECTION_LEFT = 0; public static final int DIRECTION_UP = 1; public static final int DIRECTION_RIGHT = 2; public static final int DIRECTION_DOWN = 3; public static final int[] DIRECTION_X = {-1, 0, +1, 0}; public static final int[] DIRECTION_Y = {0, -1, 0, +1}; private int[] tiles; private int handleLocation; private Random random = new Random(); private int width; private int height;
El siguiente paso es crear un método init
para la clase SlidePuzzle
. El método init
acepta dos argumentos que determinan el width
(ancho) y el height
(la altura) del objeto SlidePuzzle
. Usando las variables de instancia de ancho y alto, instanciamos la matriz de tiles
y establecemos handleLocation
como se muestra a continuación.
public void init(int width, int height) { this.width = width; this.height = height; tiles = new int[width * height]; for(int i = 0; i < tiles.length; i++) { tiles[i] = i; } handleLocation = tiles.length - 1; }
La clase SlidePuzzle
también necesita un método setter y getter para la propiedad tiles
. Sus implementaciones no son tan complicadas como se puedes ver a continuación.
public void setTiles(int[] tiles) { this.tiles = tiles; for(int i = 0; i < tiles.length; i++) { if(tiles[i] == tiles.length - 1) { handleLocation = i; break; } } } public int[] getTiles() { return tiles; }
Además de los accesorios para la propiedad tiles
(fichas), también he creado algunos métodos de conveniencia que serán útiles más adelante en este tutorial. Los métodos getColumnAt
y getRowAt
, por ejemplo, devuelven la columna y la fila de una ubicación particular en el rompecabezas.
public int getColumnAt(int location) { return location % width; } public int getRowAt(int location) { return location / width; } public int getWidth() { return width; } public int getHeight() { return height; }
El método distance
, otro método de ayuda que usaremos en unos momentos, calcula la distancia entre las fichas usando matemáticas simples y la matriz tiles
.
public int distance() { int dist = 0; for(int i = 0; i < tiles.length; i++) { dist += Math.abs(i - tiles[i]); } return dist; }
El siguiente método es getPossibleMoves
, que usaremos para determinar las posibles posiciones a las que se pueden mover las piezas del rompecabezas. En la siguiente captura de pantalla, hay cuatro piezas de rompecabezas que se pueden mover a la ranura vacía del tablero de rompecabezas. Las piezas que el jugador puede mover son 5
, 2
, 8
y 4
. ¿No te dije que los números te serían útiles?
La implementación de getPossibleMoves
puede parecer desalentadora al principio, pero no es más que matemática básica.
public int getPossibleMoves() { int x = getColumnAt(handleLocation); int y = getRowAt(handleLocation); boolean left = x > 0; boolean right = x < width - 1; boolean up = y > 0; boolean down = y < height - 1; return (left ? 1 << DIRECTION_LEFT : 0) | (right ? 1 << DIRECTION_RIGHT : 0) | (up ? 1 << DIRECTION_UP : 0) | (down ? 1 << DIRECTION_DOWN : 0); }
En el método pickRandomMove
, usamos el objeto Random
que creamos anteriormente. Como su nombre indica, el método pickRandomMove
mueve una pieza aleatoria del rompecabezas. El objeto Random
se usa para generar un entero aleatorio, que es devuelto por el método pickRandomMove
. El método también acepta un argumento, un número entero, que es la ubicación que ignoramos, es decir, la ranura vacía del tablero de rompecabezas.
private int pickRandomMove(int exclude) { List<Integer> moves = new ArrayList<Integer>(4); int possibleMoves = getPossibleMoves() & ~exclude; if((possibleMoves & (1 << DIRECTION_LEFT)) > 0) { moves.add(DIRECTION_LEFT); } if((possibleMoves & (1 << DIRECTION_UP)) > 0) { moves.add(DIRECTION_UP); } if((possibleMoves & (1 << DIRECTION_RIGHT)) > 0) { moves.add(DIRECTION_RIGHT); } if((possibleMoves & (1 << DIRECTION_DOWN)) > 0) { moves.add(DIRECTION_DOWN); } return moves.get(random.nextInt(moves.size())); }
El método invertMove
, que se usa un poco más adelante en el método shuffle
, invierte el entero utilizado para una dirección elegida.
private int invertMove(int move) { if(move == 0) { return 0; } if(move == 1 << DIRECTION_LEFT) { return 1 << DIRECTION_RIGHT; } if(move == 1 << DIRECTION_UP) { return 1 << DIRECTION_DOWN; } if(move == 1 << DIRECTION_RIGHT) { return 1 << DIRECTION_LEFT; } if(move == 1 << DIRECTION_DOWN) { return 1 << DIRECTION_UP; } return 0; }
El método moveTile
acepta dos enteros, que se usan para calcular los movimientos necesarios usando matemática básica. El método devuelve true
o false
.
public boolean moveTile(int direction, int count) { boolean match = false; for(int i = 0; i < count; i++) { int targetLocation = handleLocation + DIRECTION_X[direction] + DIRECTION_Y[direction] * width; tiles[handleLocation] = tiles[targetLocation]; match |= tiles[handleLocation] == handleLocation; tiles[targetLocation] = tiles.length - 1; // handle tile handleLocation = targetLocation; } return match; }
El método shuffle
se usa para mezclar las piezas del rompecabezas cuando comienzas un nuevo juego. Tómate un momento para inspeccionar tu implementación, ya que es una parte importante del juego. En orden aleatorio, determinamos el límite en función de la altura y el ancho del rompecabezas. Como puedes ver, usamos el método de distancia para determinar el número de fichas que se deben mover.
public void shuffle() { if(width < 2 || height < 2) { return; } int limit = width * height * Math.max(width, height); int move = 0; while(distance() < limit) { move = pickRandomMove(invertMove(move)); moveTile(move, 1); } }
Hay dos métodos más de ayuda que debemos implementar, getDirection
y getHandleLocation
. El método getDirection
devuelve la dirección en la que se mueve la pieza del rompecabezas en la ubicación y getHandleLocation
devuelve la ranura vacía del tablero de rompecabezas.
public int getDirection(int location) { int delta = location - handleLocation; if(delta % width == 0) { return delta < 0 ? DIRECTION_UP : DIRECTION_DOWN; } else if(handleLocation / width == (handleLocation + delta) / width) { return delta < 0 ? DIRECTION_LEFT : DIRECTION_RIGHT; } else { return -1; } } public int getHandleLocation() { return handleLocation; }
4. Creando el tablero del rompecabezas
Crea una nueva clase y llámala SlidePuzzleView
. Esta clase es la vista del tablero de rompecabezas, extiende la clase View
y ocupará toda la pantalla del dispositivo. La clase es responsable de dibujar las piezas del rompecabezas y de tocar los eventos táctiles.
Además de un objeto Context
, el constructor de SlidePuzzleView
también acepta una instancia de la clase SlidePuzzle
como puedes ver a continuación.
package com.dolby.DolbyPuzzle; import android.content.Context; import android.view.View; public class SlidePuzzleView extends View { public SlidePuzzleView(Context context, SlidePuzzle slidePuzzle) { super(context); ... } }
public static enum ShowNumbers { NONE, SOME, ALL }; private static final int FRAME_SHRINK = 1; private static final long VIBRATE_DRAG = 5; private static final long VIBRATE_MATCH = 50; private static final long VIBRATE_SOLVED = 250; private static final int COLOR_SOLVED = 0xff000000; private static final int COLOR_ACTIVE = 0xff303030; private Bitmap bitmap; private Rect sourceRect; private RectF targetRect; private SlidePuzzle slidePuzzle; private int targetWidth; private int targetHeight; private int targetOffsetX; private int targetOffsetY; private int puzzleWidth; private int puzzleHeight; private int targetColumnWidth; private int targetRowHeight; private int sourceColumnWidth; private int sourceRowHeight; private int sourceWidth; private int sourceHeight; private Set<Integer> dragging = null; private int dragStartX; private int dragStartY; private int dragOffsetX; private int dragOffsetY; private int dragDirection; private ShowNumbers showNumbers = ShowNumbers.SOME; private Paint textPaint; private int canvasWidth; private int canvasHeight; private Paint framePaint; private boolean dragInTarget = false; private int[] tiles; private Paint tilePaint; public SlidePuzzleView(Context context, SlidePuzzle slidePuzzle) { super(context); sourceRect = new Rect(); targetRect = new RectF(); this.slidePuzzle = slidePuzzle; tilePaint = new Paint(); tilePaint.setAntiAlias(true); tilePaint.setDither(true); tilePaint.setFilterBitmap(true); textPaint = new Paint(); textPaint.setARGB(0xff, 0xff, 0xff, 0xff); textPaint.setAntiAlias(true); textPaint.setTextAlign(Paint.Align.CENTER); textPaint.setTextSize(20); textPaint.setTypeface(Typeface.DEFAULT_BOLD); textPaint.setShadowLayer(1, 2, 2, 0xff000000); framePaint = new Paint(); framePaint.setARGB(0xff, 0x80, 0x80, 0x80); framePaint.setStyle(Style.STROKE); }
Anulamos el método onSizeChanged
de la clase y en este método establecemos PuzzleWidth
y PuzzleHeight
en 0
.
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { puzzleWidth = puzzleHeight = 0; }
El método refreshDimensions
se invoca cuando las dimensiones de la vista cambian y el rompecabezas necesita ser reconstruido. Este método se invoca en el método onDraw
de la clase.
private void refreshDimensions() { targetWidth = canvasWidth; targetHeight = canvasHeight; sourceWidth = bitmap.getWidth(); sourceHeight = bitmap.getHeight(); double targetRatio = (double) targetWidth / (double) targetHeight; double sourceRatio = (double) sourceWidth / (double) sourceHeight; targetOffsetX = 0; targetOffsetY = 0; if(sourceRatio > targetRatio) { int newTargetHeight = (int) (targetWidth / sourceRatio); int delta = targetHeight - newTargetHeight; targetOffsetY = delta / 2; targetHeight = newTargetHeight; } else if(sourceRatio < targetRatio) { int newTargetWidth = (int) (targetHeight * sourceRatio); int delta = targetWidth - newTargetWidth; targetOffsetX = delta / 2; targetWidth = newTargetWidth; } puzzleWidth = slidePuzzle.getWidth(); puzzleHeight = slidePuzzle.getHeight(); targetColumnWidth = targetWidth / puzzleWidth; targetRowHeight = targetHeight / puzzleHeight; sourceColumnWidth = sourceWidth / puzzleWidth; sourceRowHeight = sourceHeight / puzzleHeight; }
En el método onDraw
de la clase SlidePuzzleView
, se realiza el dibujo real del rompecabezas, que incluye dibujar las líneas del tablero de rompecabezas, pero también establecemos las dimensiones de las piezas del rompecabezas para asegurarnos de que encajen perfectamente en la pantalla del dispositivo. La instancia SlidePuzzle
de la vista nos ayuda a diseñar la vista como puedes ver en la implementación de onDraw
a continuación.
@Override protected void onDraw(Canvas canvas) { if(slidePuzzle == null || bitmap == null) { return; } if(puzzleWidth != slidePuzzle.getWidth() || puzzleHeight != slidePuzzle.getHeight()) { refreshDimensions(); } boolean solved = slidePuzzle.isSolved(); canvas.drawColor(solved ? COLOR_SOLVED : COLOR_ACTIVE); int[] originalTiles = slidePuzzle.getTiles(); if(tiles == null || tiles.length != originalTiles.length) { tiles = new int[originalTiles.length]; } for(int i = 0; i < tiles.length; i++) { if(originalTiles[i] == originalTiles.length - 1) { continue; } if(dragInTarget && dragging.contains(i)) { tiles[i - SlidePuzzle.DIRECTION_X[dragDirection] - puzzleWidth * SlidePuzzle.DIRECTION_Y[dragDirection]] = originalTiles[i]; } else { tiles[i] = originalTiles[i]; } } int delta = !dragInTarget ? 0 : (SlidePuzzle.DIRECTION_X[dragDirection] + puzzleWidth * SlidePuzzle.DIRECTION_Y[dragDirection]) * dragging.size(); int shownHandleLocation = slidePuzzle.getHandleLocation() + delta; tiles[shownHandleLocation] = tiles.length - 1; int emptyTile = tiles.length - 1; for(int i = 0; i < tiles.length; i++) { if(!solved && originalTiles[i] == emptyTile) { continue; } int targetColumn = slidePuzzle.getColumnAt(i); int targetRow = slidePuzzle.getRowAt(i); int sourceColumn = slidePuzzle.getColumnAt(originalTiles[i]); int sourceRow = slidePuzzle.getRowAt(originalTiles[i]); targetRect.left = targetOffsetX + targetColumnWidth * targetColumn; targetRect.top = targetOffsetY + targetRowHeight * targetRow; targetRect.right = targetColumn < puzzleWidth - 1 ? targetRect.left + targetColumnWidth : targetOffsetX + targetWidth; targetRect.bottom = targetRow < puzzleHeight - 1 ? targetRect.top + targetRowHeight : targetOffsetY + targetHeight; sourceRect.left = sourceColumnWidth * sourceColumn; sourceRect.top = sourceRowHeight * sourceRow; sourceRect.right = sourceColumn < puzzleWidth - 1 ? sourceRect.left + sourceColumnWidth : sourceWidth; sourceRect.bottom = sourceRow < puzzleHeight - 1 ? sourceRect.top + sourceRowHeight : sourceHeight; boolean isDragTile = dragging != null && dragging.contains(i); boolean matchLeft; boolean matchRight; boolean matchTop; boolean matchBottom; int di = i; if(dragInTarget && dragging.contains(i)) { di = di - SlidePuzzle.DIRECTION_X[dragDirection] - puzzleWidth * SlidePuzzle.DIRECTION_Y[dragDirection]; } if(di == tiles[di]) { matchLeft = matchRight = matchTop = matchBottom = true; } else { matchLeft = (di - 1) >= 0 && di % puzzleWidth > 0 && tiles[di] % puzzleWidth > 0 && tiles[di - 1] == tiles[di] - 1; matchRight = tiles[di] + 1 < tiles.length - 1 && (di + 1) % puzzleWidth > 0 && (tiles[di] + 1) % puzzleWidth > 0 && (di + 1) < tiles.length && (di + 1) % puzzleWidth > 0 && tiles[di + 1] == tiles[di] + 1; matchTop = (di - puzzleWidth) >= 0 && tiles[di - puzzleWidth] == tiles[di] - puzzleWidth; matchBottom = tiles[di] + puzzleWidth < tiles.length - 1 && (di + puzzleWidth) < tiles.length && tiles[di + puzzleWidth] == tiles[di] + puzzleWidth; } if(!matchLeft) { sourceRect.left += FRAME_SHRINK; targetRect.left += FRAME_SHRINK; } if(!matchRight) { sourceRect.right -= FRAME_SHRINK; targetRect.right -= FRAME_SHRINK; } if(!matchTop) { sourceRect.top += FRAME_SHRINK; targetRect.top += FRAME_SHRINK; } if(!matchBottom) { sourceRect.bottom -= FRAME_SHRINK; targetRect.bottom -= FRAME_SHRINK; } if(isDragTile) { targetRect.left += dragOffsetX; targetRect.right += dragOffsetX; targetRect.top += dragOffsetY; targetRect.bottom += dragOffsetY; } canvas.drawBitmap(bitmap, sourceRect, targetRect, tilePaint); if(!matchLeft) { canvas.drawLine(targetRect.left, targetRect.top, targetRect.left, targetRect.bottom, framePaint); } if(!matchRight) { canvas.drawLine(targetRect.right - 1, targetRect.top, targetRect.right - 1, targetRect.bottom, framePaint); } if(!matchTop) { canvas.drawLine(targetRect.left, targetRect.top, targetRect.right, targetRect.top, framePaint); } if(!matchBottom) { canvas.drawLine(targetRect.left, targetRect.bottom - 1, targetRect.right, targetRect.bottom - 1, framePaint); } if(!solved && (showNumbers == ShowNumbers.ALL || (showNumbers == ShowNumbers.SOME && di != tiles[di]))) { canvas.drawText(String.valueOf(originalTiles[i] + 1), (targetRect.left + targetRect.right) / 2, (targetRect.top + targetRect.bottom) / 2 - (textPaint.descent() + textPaint.ascent()) / 2, textPaint); } } }
Para manejar eventos táctiles, necesitamos anular el método onTouchEvent
de la clase. Para mantener el TouchTest
conciso y legible, también he declarado algunos métodos de ayuda, finishDrag
, doMove
, startDrag
y updateDrag
. Estos métodos ayudan a implementar el comportamiento de arrastre.
@Override public boolean onTouchEvent(MotionEvent event) { if(slidePuzzle == null || bitmap == null) { return false; } if(slidePuzzle.isSolved()) { return false; } if(event.getAction() == MotionEvent.ACTION_DOWN) { return startDrag(event); } else if(event.getAction() == MotionEvent.ACTION_MOVE) { return updateDrag(event); } else if(event.getAction() == MotionEvent.ACTION_UP) { return finishDrag(event); } else { return false; } } private boolean finishDrag(MotionEvent event) { if(dragging == null) { return false; } updateDrag(event); if(dragInTarget) { doMove(dragDirection, dragging.size()); } else { vibrate(VIBRATE_DRAG); } dragInTarget = false; dragging = null; invalidate(); return true; } private void doMove(int dragDirection, int count) { playSlide(); if(slidePuzzle.moveTile(dragDirection, count)) { vibrate(slidePuzzle.isSolved() ? VIBRATE_SOLVED : VIBRATE_MATCH); } else { vibrate(VIBRATE_DRAG); } invalidate(); if(slidePuzzle.isSolved()) { onFinish(); } } private boolean startDrag(MotionEvent event) { if(dragging != null) { return false; } int x = ((int) event.getX() - targetOffsetX) / targetColumnWidth; int y = ((int) event.getY() - targetOffsetY) / targetRowHeight; if(x < 0 || x >= puzzleWidth || y < 0 || y >= puzzleHeight) { return false; } int direction = slidePuzzle.getDirection(x + puzzleWidth * y); if(direction >= 0) { dragging = new HashSet<Integer>(); while(x + puzzleWidth * y != slidePuzzle.getHandleLocation()) { dragging.add(x + puzzleWidth * y); dragStartX = (int) event.getX(); dragStartY = (int) event.getY(); dragOffsetX = 0; dragOffsetY = 0; dragDirection = direction; x -= SlidePuzzle.DIRECTION_X[direction]; y -= SlidePuzzle.DIRECTION_Y[direction]; } } dragInTarget = false; vibrate(VIBRATE_DRAG); return true; } private boolean updateDrag(MotionEvent event) { if(dragging == null) { return false; } int directionX = SlidePuzzle.DIRECTION_X[dragDirection] * -1; int directionY = SlidePuzzle.DIRECTION_Y[dragDirection] * -1; if(directionX != 0) { dragOffsetX = (int) event.getX() - dragStartX; if(Math.signum(dragOffsetX) != directionX) { dragOffsetX = 0; } else if(Math.abs(dragOffsetX) > targetColumnWidth) { dragOffsetX = directionX * targetColumnWidth; } } if(directionY != 0) { dragOffsetY = (int) event.getY() - dragStartY; if(Math.signum(dragOffsetY) != directionY) { dragOffsetY = 0; } else if(Math.abs(dragOffsetY) > targetRowHeight) { dragOffsetY = directionY * targetRowHeight; } } dragInTarget = Math.abs(dragOffsetX) > targetColumnWidth / 2 || Math.abs(dragOffsetY) > targetRowHeight / 2; invalidate(); return true; }
También he declarado métodos getter para targetWidth
y targetHeight
y accesorios para bitmap
.
public int getTargetWidth() { return targetWidth; } public int getTargetHeight() { return targetHeight; } public void setBitmap(Bitmap bitmap) { this.bitmap = bitmap; puzzleWidth = 0; puzzleHeight = 0; } public Bitmap getBitmap() { return bitmap; }
5. Creando la Clase Activity
Con la implementación de las clases SlidePuzzle
y SlidePuzzleView
finalizadas, es hora de enfocarte en la clase de actividad principal que tu IDE creó para ti. La clase principal Activity
en este ejemplo se llama SlidePuzzleMain
, pero la tuya puede tener un nombre diferente. La clase SlidePuzzleMain
reunirá todo lo que hemos creado hasta ahora.
package com.dolby.DolbyPuzzle; import android.app.Activity; public class SlidePuzzleMain extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ... } }
protected static final int MENU_SCRAMBLE = 0; protected static final int MENU_SELECT_IMAGE = 1; protected static final int MENU_TAKE_PHOTO = 2; protected static final int RESULT_SELECT_IMAGE = 0; protected static final int RESULT_TAKE_PHOTO = 1; protected static final String KEY_SHOW_NUMBERS = "showNumbers"; protected static final String KEY_IMAGE_URI = "imageUri"; protected static final String KEY_PUZZLE = "slidePuzzle"; protected static final String KEY_PUZZLE_SIZE = "puzzleSize"; protected static final String FILENAME_DIR = "dolby.digital.plus"; protected static final String FILENAME_PHOTO_DIR = FILENAME_DIR + "/photo"; protected static final String FILENAME_PHOTO = "photo.jpg"; protected static final int DEFAULT_SIZE = 3; private SlidePuzzleView view; private SlidePuzzle slidePuzzle; private Options bitmapOptions; private int puzzleWidth = 1; private int puzzleHeight = 1; private Uri imageUri; private boolean portrait; private boolean expert;
En el método onCreate
de la actividad, instanciamos el objeto bitmapOptions
, estableciendo su atributo inScaled
en false
. También creamos una instancia de la clase SlidePuzzle
y una instancia de la clase SlidePuzzleView
, pasando la actividad como el contexto de la vista. Luego configuramos la vista de la actividad invocando setContentView
y pasando el objeto view
.
bitmapOptions = new BitmapFactory.Options(); bitmapOptions.inScaled = false; slidePuzzle = new SlidePuzzle(); view = new SlidePuzzleView(this, slidePuzzle); setContentView(view);
En loadBitmap
, cargamos la imagen que agregaste al proyecto al comienzo de este tutorial y que usaremos para el rompecabezas. El método acepta la ubicación de la imagen como su único argumento, que utiliza para recuperar la imagen.
protected void loadBitmap(Uri uri) { try { Options o = new Options(); o.inJustDecodeBounds = true; InputStream imageStream = getContentResolver().openInputStream(uri); BitmapFactory.decodeStream(imageStream, null, o); int targetWidth = view.getTargetWidth(); int targetHeight = view.getTargetHeight(); if(o.outWidth > o.outHeight && targetWidth < targetHeight) { int i = targetWidth; targetWidth = targetHeight; targetHeight = i; } if(targetWidth < o.outWidth || targetHeight < o.outHeight) { double widthRatio = (double) targetWidth / (double) o.outWidth; double heightRatio = (double) targetHeight / (double) o.outHeight; double ratio = Math.max(widthRatio, heightRatio); o.inSampleSize = (int) Math.pow(2, (int) Math.round(Math.log(ratio) / Math.log(0.5))); } else { o.inSampleSize = 1; } o.inScaled = false; o.inJustDecodeBounds = false; imageStream = getContentResolver().openInputStream(uri); Bitmap bitmap = BitmapFactory.decodeStream(imageStream, null, o); if(bitmap == null) { Toast.makeText(this, getString(R.string.error_could_not_load_image), Toast.LENGTH_LONG).show(); return; } int rotate = 0; Cursor cursor = getContentResolver().query(uri, new String[] {MediaStore.Images.ImageColumns.ORIENTATION}, null, null, null); if(cursor != null) { try { if(cursor.moveToFirst()) { rotate = cursor.getInt(0); if(rotate == -1) { rotate = 0; } } } finally { cursor.close(); } } if(rotate != 0) { Matrix matrix = new Matrix(); matrix.postRotate(rotate); bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); } setBitmap(bitmap); imageUri = uri; } catch(FileNotFoundException ex) { Toast.makeText(this, MessageFormat.format(getString(R.string.error_could_not_load_image_error), ex.getMessage()), Toast.LENGTH_LONG).show(); return; } }
En loadBitmap
, también invocamos setBitmap
. La implementación de setBitmap
se muestra a continuación.
private void setBitmap(Bitmap bitmap) { portrait = bitmap.getWidth() < bitmap.getHeight(); view.setBitmap(bitmap); setPuzzleSize(Math.min(puzzleWidth, puzzleHeight), true); setRequestedOrientation(portrait ? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT : ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); }
Para que el juego de rompecabezas sea más atractivo para el jugador, agregaremos una opción para personalizar el juego al permitir que el jugador seleccione una imagen para el rompecabezas de la galería de fotos del usuario o tome una con la cámara del dispositivo. También crearemos una opción de menú para cada método.
Para que todo esto funcione, implementamos dos métodos nuevos, selectImage
y takePicture
, en los que creamos la intención de recuperar la imagen que necesitamos. El método onActivityResult
maneja el resultado de la selección del usuario. Echa un vistazo al fragmento de código a continuación para comprender la imagen completa.
private void selectImage() { Intent photoPickerIntent = new Intent(Intent.ACTION_PICK); photoPickerIntent.setType("image/*"); startActivityForResult(photoPickerIntent, RESULT_SELECT_IMAGE); } private void takePicture() { File dir = getSaveDirectory(); if(dir == null) { Toast.makeText(this, getString(R.string.error_could_not_create_directory_to_store_photo), Toast.LENGTH_SHORT).show(); return; } File file = new File(dir, FILENAME_PHOTO); Intent photoPickerIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); photoPickerIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file)); startActivityForResult(photoPickerIntent, RESULT_TAKE_PHOTO); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent imageReturnedIntent) { super.onActivityResult(requestCode, resultCode, imageReturnedIntent); switch(requestCode) { case RESULT_SELECT_IMAGE: { if(resultCode == RESULT_OK) { Uri selectedImage = imageReturnedIntent.getData(); loadBitmap(selectedImage); } break; } case RESULT_TAKE_PHOTO: { if(resultCode == RESULT_OK) { File file = new File(getSaveDirectory(), FILENAME_PHOTO); if(file.exists()) { Uri uri = Uri.fromFile(file); if(uri != null) { loadBitmap(uri); } } } break; } } }
Todo lo que nos queda por hacer es crear una opción de menú para cada método. La implementación a continuación ilustra cómo puedes crear un menú con opciones, que se muestra al usuario cuando tocas el botón de opciones del dispositivo.
@Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); onCreateOptionsMenu(menu); } @Override public boolean onCreateOptionsMenu(Menu menu) { boolean hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA); menu.add(0, MENU_SELECT_IMAGE, 0, R.string.menu_select_image); if(hasCamera) { menu.add(0, MENU_TAKE_PHOTO, 0, R.string.menu_take_photo); } menu.add(0, MENU_SCRAMBLE, 0, R.string.menu_scramble); return true; } @Override public boolean onContextItemSelected(MenuItem item) { return onOptionsItemSelected(item); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch(item.getItemId()) { case MENU_SCRAMBLE: shuffle(); return true; case MENU_SELECT_IMAGE: selectImage(); return true; case MENU_TAKE_PHOTO: takePicture(); return true; default: return super.onOptionsItemSelected(item); } }
El menú de opciones debe ser similar al que se muestra a continuación.



Al tocar Seleccionar imagen o Tomar foto, deberías poder seleccionar una imagen de la galería de fotos de tu dispositivo o tomar una con la cámara y usarla en el juego de rompecabezas.






Puedes haber notado que también he agregado una tercera opción de menú para mezclar las piezas del tablero de rompecabezas. Esta opción de menú invoca el método shuffle
que implementamos en la clase SlidePuzzle
un poco antes en este tutorial.
Antes de implementar la API de Dolby Audio, creemos los dos métodos que activarán la reproducción de los archivos de audio que agregamos anteriormente. Puedes dejar las implementaciones de estos métodos en blanco por ahora. El método onFinish
se invoca cuando el juego finaliza mientras se invoca playSound
siempre que se mueve una pieza del rompecabezas.
public void onFinish() { } public void playSound() { }
Todo lo que nos queda por hacer es invocar loadBitmap
desde el método onCreate
de la actividad y pasarle la ubicación de la imagen que queremos usar para el rompecabezas.
Uri path = Uri.parse("android.resource://com.dolby.DolbyPuzzle/" + R.drawable.dolby); loadBitmap(path);
Echa un vistazo a la siguiente imagen para ver un ejemplo de cómo debería verse tu juego, dependiendo de la imagen que hayas utilizado para el rompecabezas.



6. Implementación de la API Dolby Audio
Paso 1
Como mencioné al principio de este tutorial, la integración de Dolby Audio API es fácil y solo toma unos minutos. Veamos cómo podemos aprovechar la API de Dolby Audio en nuestro juego.
Comienza por descargar la API de Dolby Audio desde el sitio web para desarrolladores de Dolby. Para hacerlo, crea una cuenta gratuita de desarrollador o inicia sesión si ya tienes una. Una vez que hayas descargado la API, agrega la biblioteca a tu proyecto.
Paso 2
Antes de integrar la API de Dolby Audio, es una buena idea agregar controles de volumen a tu aplicación. Esto es fácil de hacer y solo toma una sola línea de código. Agrega el siguiente fragmento de código al método onCreate
de tu actividad
setVolumeControlStream(AudioManager.STREAM_MUSIC);
Paso 3
El siguiente paso es declarar dos variables en tu clase Activity
, una instancia de la clase MediaPlayer
y una instancia de la clase DolbyAudioProcessing
. No olvides agregar las importaciones requeridas en la parte superior.
import android.media.MediaPlayer; import com.dolby.dap.*; MediaPlayer mPlayer; DolbyAudioProcessing mDolbyAudioProcessing;
Paso 4
Ahora haremos que la clase Activity
adopte las interfaces OnDolbyAudioProcessingEventListener
y MediaPlayer.OnCompletionListener
.
public class SlidePuzzleMain extends Activity implements MediaPlayer.OnCompletionListener, OnDolbyAudioProcessingEventListener { ... }
Para adoptar estas interfaces, debemos implementar algunos métodos como se muestra en el siguiente fragmento de código.
// MediaPlayer.OnCompletionListener @Override public void onCompletion(MediaPlayer mp) {} // OnDolbyAudioProcessingEventListener @Override public void onDolbyAudioProcessingClientConnected() {} @Override public void onDolbyAudioProcessingClientDisconnected() {} @Override public void onDolbyAudioProcessingEnabled(boolean b) {} @Override public void onDolbyAudioProcessingProfileSelected(DolbyAudioProcessing.PROFILE profile) {}
Activamos el objeto DolbyAudioProcessing
cuando se invoca onDolbyAudioProcessingClientConnected
y lo deshabilitamos nuevamente cuando se invoca onDolbyAudioProcessingClientDisconnected
.
@Override public void onCompletion(MediaPlayer mp) { if(mPlayer != null) { mPlayer.release(); mPlayer = null; } } @Override public void onDolbyAudioProcessingClientConnected() { } @Override public void onDolbyAudioProcessingClientDisconnected() { } @Override public void onDolbyAudioProcessingEnabled(boolean b) {} @Override public void onDolbyAudioProcessingProfileSelected(DolbyAudioProcessing.PROFILE profile) {}
Como puedes ver en el fragmento de código anterior, lanzamos el objeto MediaPlayer
cuando finaliza la reproducción del archivo de audio.
Para reproducir un sonido cuando el jugador mueve una pieza del rompecabezas, debemos implementar el método playSound
. Antes de centrarnos en PlaySound
, primero creamos una instancia de SlidePuzzleMain
en la clase SlidePuzzleView
y en el método playSlide
de la vista, llamamos a playSound
en la instancia de SlidePuzzleMain
.
private void playSlide() { SlidePuzzleMain activity = (SlidePuzzleMain) getContext(); activity.playSound(); }
En el método playSound
, creamos una instancia de la clase MediaPlayer
y hacemos uso de la API de Dolby Audio para iniciar el procesamiento del audio. Si la API de Dolby Audio no es compatible con el dispositivo del usuario, el método getDolbyAudioProcessing
devolverá un valor null
.
public void playSound() { if(mPlayer == null) { mPlayer = MediaPlayer.create( SlidePuzzleMain.this, R.raw.slide); mPlayer.start(); } else { mPlayer.release(); mPlayer = null; mPlayer = MediaPlayer.create( SlidePuzzleMain.this, R.raw.slide); mPlayer.start(); } mDolbyAudioProcessing = DolbyAudioProcessing.getDolbyAudioProcessing(this, DolbyAudioProcessing.PROFILE.GAME, this); if (mDolbyAudioProcessing == null) { return; } }
Como puedes ver a continuación, la implementación del método onFinish
es muy similar a la de playSound
. La principal diferencia es que mostramos un mensaje al usuario si Dolby Audio API no está disponible. Como puedes recordar, el método onFinish
se ejecuta cuando el juego está terminado y el jugador ha completado el rompecabezas.
public void onFinish() { if(mPlayer == null) { mPlayer = MediaPlayer.create( SlidePuzzleMain.this, R.raw.fireworks); mPlayer.start(); } else { mPlayer.release(); mPlayer = null; mPlayer = MediaPlayer.create( SlidePuzzleMain.this, R.raw.fireworks); mPlayer.start(); } mDolbyAudioProcessing = DolbyAudioProcessing.getDolbyAudioProcessing(this, DolbyAudioProcessing.PROFILE.GAME, this); if (mDolbyAudioProcessing == null) { Toast.makeText(this, "Dolby Audio Processing not available on this device.", Toast.LENGTH_SHORT).show(); shuffle(); } }
También llamamos al método shuffle
al final de onFinish
para comenzar un nuevo juego cuando el jugador ha terminado el rompecabezas.
Paso 5
Es importante que liberemos los objetos DolbyAudioProcessing
y MediaPlayer
cuando ya no los necesiten. La liberación de estos objetos garantiza que no comprometamos la duración de la batería del dispositivo y afecte negativamente el rendimiento del dispositivo.
Comenzamos declarando tres métodos. El primer método, releaseDolbyAudioProcessing
, libera el objeto DolbyAudioProcessing
y establece mDolbyAudioProcessing
en null
. El segundo método, restartSession
, reinicia la sesión administrada por el objeto DolbyAudioProcessing
y en el tercer método, suspendSession
, la sesión de audio se suspende y la configuración actual se guarda para su uso posterior.
public void releaseDolbyAudioProcessing() { if (mDolbyAudioProcessing != null) { try { mDolbyAudioProcessing.release(); mDolbyAudioProcessing = null; } catch (IllegalStateException ex) { handleIllegalStateException(ex); } catch (RuntimeException ex) { handleRuntimeException(ex); } } } // Backup the system-wide audio effect configuration and restore the application configuration public void restartSession() { if (mDolbyAudioProcessing != null) { try{ mDolbyAudioProcessing.restartSession(); } catch (IllegalStateException ex) { handleIllegalStateException(ex); } catch (RuntimeException ex) { handleRuntimeException(ex); } } } // Backup the application Dolby Audio Processing configuration and restore the system-wide configuration public void suspendSession() { if (mDolbyAudioProcessing != null) { try{ mDolbyAudioProcessing.suspendSession(); } catch (IllegalStateException ex) { handleIllegalStateException(ex); } catch (RuntimeException ex) { handleRuntimeException(ex); } } } /** Generic handler for IllegalStateException */ private void handleIllegalStateException(Exception ex) { Log.e("Dolby processing", "Dolby Audio Processing has a wrong state"); handleGenericException(ex); } /** Generic handler for IllegalArgumentException */ private void handleIllegalArgumentException(Exception ex) { Log.e("Dolby processing","One of the passed arguments is invalid"); handleGenericException(ex); } /** Generic handler for RuntimeException */ private void handleRuntimeException(Exception ex) { Log.e("Dolby processing", "Internal error occurred in Dolby Audio Processing"); handleGenericException(ex); } private void handleGenericException(Exception ex) { Log.e("Dolby processing", Log.getStackTraceString(ex)); }
Como puedes ver en el fragmento de código anterior, también he creado algunos métodos para manejar cualquier excepción que pueda lanzarse en releaseDolbyAudioProcessing
, restartSession
y suspendSession
.
Los tres métodos que acabamos de crear deben invocarse en varios momentos clave del ciclo de vida de la aplicación. Esto lo logramos anulando los métodos onStop
, onStart
, onDestroy
, onResume
y onPause
en nuestra clase SlidePuzzleMain
.
En onStop
, le decimos al objeto MediaPlayer
que pause y en onStart
el objeto MediaPlayer
continúa la reproducción si no es null
. El método onDestroy
se invoca cuando la aplicación está cerrada. En este método, lanzamos el objeto MediaPlayer
, configuramos mPlayer
como null
e invocamos releaseDolbyAudioProcessing
, el cual implementamos anteriormente.
@Override protected void onStop() { super.onStop(); if (mPlayer != null) { mPlayer.pause(); } } @Override protected void onStart() { super.onStart(); if (mPlayer != null) { mPlayer.start(); } } @Override protected void onDestroy() { super.onDestroy(); Log.d("Dolby processing", "onDestroy()"); // Release Media Player instance if (mPlayer != null) { mPlayer.release(); mPlayer = null; } this.releaseDolbyAudioProcessing(); } @Override protected void onResume() { super.onResume(); restartSession(); } @Override protected void onPause() { super.onPause(); Log.d("Dolby processing", "The application is in background, supsendSession"); // // If audio playback is not required while your application is in the background, restore the Dolby audio processing system // configuration to its original state by suspendSession(). // This ensures that the use of the system-wide audio processing is sandboxed to your application. suspendSession(); }
Finalmente, en onPause
y onResume
suspendemos y reiniciamos la sesión de audio invocando suspendSession
y restartSession
, respectivamente.
Si has seguido los pasos descritos en este tutorial, entonces tu juego debería ser completamente funcional con la API Dolby Audio integrada. Construye el proyecto para jugar con el resultado final.
7. Resumen
Estoy seguro de que aceptas que la integración de Dolby Audio API es fácil y no toma más de cinco minutos. Resumamos brevemente los pasos que hemos dado para integrar la API.
- Importar la biblioteca de la API Dolby Audio
- Crear una instancia de la clase
DolbyAudioProcessing
- Implementar la interfaz
OnDolbyAudioProcessingEventListener
.
- Habilitar la instancia de
DolbyAudioProcessing
enonDolbyAudioProcessingClientConnected
- Deshabilitar la instancia de
DolbyAudioProcessing
enonDolbyAudioProcessingClientDisconnected
- Después de iniciar el reproductor multimedia, iniciar la instancia de
DolbyAudioProcessing
utilizando el perfilGAME
- Comprobar si el objeto
DolbyAudioProcessing
esnull
para verificar si el dispositivo admite la API de Dolby Audio - Para conservar la duración de la batería y optimizar el rendimiento, suspender y liberar la instancia de
DolbyAudioProcessing
cuando la aplicación se destruye o se mueve al fondo
Conclusión
Aunque el juego que creamos es bastante simple, es importante recordar el enfoque de este tutorial, la API de Dolby Audio. El mercado de dispositivos móviles es un lugar abarrotado y destacar de otros juegos no es fácil. Agregar un sonido superior a tu juego no pasará desapercibido para tus usuarios y hará que tu juego se note. Dirígete al sitio web para desarrolladores de Dolby para probarlo.