Статьи

Создайте игру-головоломку для Android с помощью Dolby Audio API

В этом уроке я покажу вам, как использовать Dolby Audio API для улучшения звука в ваших приложениях Android. Чтобы показать вам, как использовать Dolby Audio API, мы создадим простую, но веселую игру-головоломку.

На сегодняшнем многолюдном рынке мобильной связи важно сделать ваши приложения максимально привлекательными. Повышение качества звука приложения может быть столь же привлекательным для пользователя, как и потрясающий пользовательский интерфейс.

Звук, создаваемый приложением, является формой взаимодействия между пользователем и приложением, которая слишком часто упускается из виду. Тем не менее, это означает, что предоставление отличного слухового опыта может помочь вашему приложению выделиться из толпы.

Dolby Digital Plus — это продвинутый аудиокодек, который можно использовать в мобильных приложениях с помощью простого в использовании аудио API Dolby. Dolby сделал свой API доступным для нескольких платформ, включая Android и Kindle Fire . В этом уроке мы рассмотрим реализацию API для Android.

Dolby Audio API для Android совместим с широким спектром устройств Android. Это означает, что ваши приложения и игры для Android могут наслаждаться высококачественным, захватывающим звуком всего за несколько минут работы, интегрируя Dolby Audio API. Давайте рассмотрим, что нужно для интеграции API, создав игру-головоломку.

В первой части этого урока я покажу вам, как создать веселую головоломку. Поскольку основное внимание в этом руководстве уделяется интеграции Dolby Audio API, я не буду вдаваться в подробности и надеюсь, что вы уже знакомы с основами разработки под Android. Во второй части этой статьи мы рассмотрим интеграцию Dolby Audio API в приложение для Android.

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

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

Не важно, какую IDE вы используете, но для этого урока я буду использовать JetBrains IntelliJ Idea . Откройте нужную среду IDE и создайте новый проект для своего приложения Android. Обязательно создайте основной класс Activity и макет XML .

Давайте сначала настроим файл манифеста приложения. В узле application файла манифеста установите для hardwareAccelerated значение true . Это повысит производительность рендеринга вашего приложения даже для 2D-игр, подобных той, которую мы собираемся создать.

1
android:hardwareAccelerated=»true»

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

1
2
3
4
5
6
<supports-screens
           android:largeScreens=»true»
           android:anyDensity=»true»
           android:normalScreens=»true»
           android:smallScreens=»false»
           android:xlargeScreens=»true»/>

В узле activity файла манифеста добавьте узел с именем configChanges и установите для его значения orientation как показано ниже. Вы можете найти больше информации об этой настройке на веб-сайте разработчика .

1
android:configChanges=»orientation»

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

1
2
<uses-permission android:name=»android.permission.VIBRATE» />
<uses-permission android:name=»android.permission.WRITE_EXTERNAL_STORAGE» />

Давайте также добавим ресурсы, которые мы будем использовать позже в этом руководстве. Начните с добавления изображения, которое вы хотите использовать для головоломки. Добавьте его в папку drawable вашего проекта. Я решил добавить изображение в папку drawable-hdpi моего проекта.

И последнее, но не менее важное: добавьте звуковые файлы, которые вы хотите использовать в своей игре. В папке res вашего проекта создайте новый каталог с именем raw и добавьте звуковые файлы в эту папку. Для целей этого урока я добавил два аудиофайла. Первый звук воспроизводится, когда игрок перемещает кусок головоломки, в то время как второй звук воспроизводится по окончании игры, то есть, когда игрок завершает головоломку. Оба звука доступны в SoundBible . Первый звук лицензирован по лицензии Creative Commons Attribution 3.0 и записан Майком Кенигом.

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

Мы начнем с создания нового класса SlidePuzzle , который станет мозгом игры. Каждый ход, сделанный в головоломке, обрабатывается и отслеживается экземпляром этого класса с использованием простой математики.

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

1
2
3
4
5
package com.dolby.DolbyPuzzle;
 
public class SlidePuzzle {
 
}

Начнем с объявления количества переменных, которые нам понадобятся чуть позже. Взгляните на следующий фрагмент кода, в котором я объявляю переменные для четырех возможных направлений, в которых кусочки головоломки могут двигаться, два массива целых чисел для горизонтального и вертикального направлений и массив для плиток головоломки. Мы также объявляем и создаем экземпляр класса Random , который мы будем использовать позже в этом руководстве.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
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;

Следующим шагом является создание метода init для класса SlidePuzzle . Метод init принимает два аргумента, которые определяют width и height объекта SlidePuzzle . Используя переменные экземпляра width и height , мы создаем массив tiles и устанавливаем handleLocation как показано ниже.

01
02
03
04
05
06
07
08
09
10
11
12
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;
}

Класс SlidePuzzle также нуждается в методе установки и получения свойства tiles . Их реализации не так сложны, как вы можете видеть ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
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;
}

В дополнение к методам доступа к свойству tiles , я также создал несколько удобных методов, которые пригодятся позже в этом уроке. getRowAt методы getColumnAt и getRowAt возвращают столбец и строку определенного местоположения в головоломке.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
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;
}

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

01
02
03
04
05
06
07
08
09
10
public int distance() {
  int dist = 0;
   
  for(int i = 0; i < tiles.length; i++)
  {
    dist += Math.abs(i — tiles[i]);
  }
   
  return dist;
}

Следующий метод — getPossibleMoves , который мы будем использовать для определения возможных позиций, на которые могут перемещаться части головоломки. На следующем скриншоте четыре части головоломки, которые можно переместить в пустое место на доске. Фишки, которые игрок может переместить, это 5 , 2 , 8 и 4 . Разве я не говорил вам, что цифры пригодятся?

getPossibleMoves реализация getPossibleMoves может показаться сложной, но это не более чем базовая математика.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
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);
}

В методе pickRandomMove мы используем объект Random мы создали ранее. Как видно из pickRandomMove метод pickRandomMove перемещает случайную часть головоломки. Random объект используется для генерации случайного целого числа, которое возвращается методом pickRandomMove . Метод также принимает один аргумент, целое число, которое является местоположением, которое мы игнорируем, то есть пустое место на доске пазлов.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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()));
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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;
}

Метод moveTile принимает два целых числа, которые используются для вычисления шагов, необходимых с использованием базовой математики. Метод возвращает true или false .

01
02
03
04
05
06
07
08
09
10
11
12
13
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;
    handleLocation = targetLocation;
  }
  return match;
}

Метод shuffle используется, чтобы перемешать части головоломки, когда начинается новая игра. Потратьте немного времени, чтобы осмотреть его реализацию, поскольку это важная часть игры. В shuffle мы определяем предел на основе height и width головоломки. Как видите, мы используем метод distance чтобы определить количество плиток, которые нужно переместить.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
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);
  }
}

Нам нужно реализовать еще два вспомогательных метода: getDirection и getHandleLocation . Метод getDirection возвращает направление, в которое перемещается кусок головоломки в определенном location а getHandleLocation возвращает пустой слот доски головоломки.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
public int getDirection(int location) {
  int delta = location — handleLocation;
   
  if(delta % width == 0)
  {
    return delta < 0 ?
  }
  else if(handleLocation / width == (handleLocation + delta) / width)
  {
    return delta < 0 ?
  }
  else
  {
    return -1;
  }
}
 
public int getHandleLocation() {
  return handleLocation;
}

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

В дополнение к объекту Context , конструктор SlidePuzzleView также принимает экземпляр класса SlidePuzzle как вы можете видеть ниже.

01
02
03
04
05
06
07
08
09
10
11
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);
        …
    }
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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);
}

Мы переопределяем метод класса onSizeChanged и в этом методе мы устанавливаем для puzzleWidth и puzzleHeight значение 0 .

1
2
3
4
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
  puzzleWidth = puzzleHeight = 0;
}

Метод refreshDimensions вызывается, когда размеры представления меняются, и головоломку необходимо перестраивать. Этот метод вызывается в методе класса onDraw .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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;
}

В методе SlidePuzzleView класса SlidePuzzleView происходит фактическое рисование головоломки, в том числе рисование линий на доске головоломки, но мы также устанавливаем размеры частей головоломки, чтобы они точно помещались на экране устройства. Экземпляр SlidePuzzle помогает нам SlidePuzzle представление, как вы можете видеть в реализации onDraw ниже.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
+015
016
+017
018
019
020
021
022
023
024
025
026
027
028
029
+030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
+055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
@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 ?
  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.bottom = targetRow < puzzleHeight — 1 ?
     
    sourceRect.left = sourceColumnWidth * sourceColumn;
    sourceRect.top = sourceRowHeight * sourceRow;
    sourceRect.right = sourceColumn < puzzleWidth — 1 ?
    sourceRect.bottom = sourceRow < puzzleHeight — 1 ?
 
    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);
    }
  }
}

Для обработки сенсорных событий нам необходимо переопределить метод класса onTouchEvent . Чтобы сделать onTouchEvent лаконичным и читабельным, я также объявил несколько вспомогательных методов: finishDrag , doMove , startDrag и updateDrag . Эти методы помогают реализовать поведение перетаскивания.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
+015
016
+017
018
019
020
021
022
023
024
025
026
027
028
029
+030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
+055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
@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;
}

Я также объявил методы получения для targetWidth и targetHeight и targetHeight доступа для bitmap .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
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;
}

Когда реализация классов SlidePuzzle и SlidePuzzleView завершена, настало время сосредоточиться на основном классе Activity , созданном для вас вашей IDE. Основной класс Activity в этом примере называется SlidePuzzleMain , но ваш класс может называться по-другому. Класс SlidePuzzleMain объединит все, что мы создали до сих пор.

01
02
03
04
05
06
07
08
09
10
11
package com.dolby.DolbyPuzzle;
 
import android.app.Activity;
 
public class SlidePuzzleMain extends Activity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    …
  }
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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;

В методе onCreate мы создаем экземпляр объекта bitmapOptions , устанавливая для его атрибута inScaled значение false . Мы также создаем экземпляр класса SlidePuzzle и экземпляр класса SlidePuzzleView , передавая действие в качестве контекста представления. Затем мы устанавливаем вид действия, вызывая setContentView и передавая объект view .

1
2
3
4
5
6
7
bitmapOptions = new BitmapFactory.Options();
bitmapOptions.inScaled = false;
 
slidePuzzle = new SlidePuzzle();
 
view = new SlidePuzzleView(this, slidePuzzle);
setContentView(view);

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
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;
  }
}

В loadBitmap мы также вызываем setBitmap . Реализация setBitmap показана ниже.

1
2
3
4
5
6
7
8
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);
}

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

Чтобы заставить все это работать, мы реализуем два новых метода, selectImage и takePicture , в которых мы создаем намерение извлечь takePicture изображение. Метод onActivityResult обрабатывает результат выбора пользователя. Посмотрите на фрагмент кода ниже, чтобы понять полную картину.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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;
    }
  }
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@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);
  }
}

Меню параметров должно выглядеть примерно так, как показано ниже.

Нажав Выбрать изображение или Сделать фото , вы сможете выбрать изображение из фотогалереи вашего устройства или взять его с помощью камеры, и использовать его в игре-головоломке.

Вы, возможно, заметили, что я также добавил третий пункт меню, чтобы перетасовать кусочки головоломки. Эта опция меню вызывает метод shuffle который мы реализовали в классе SlidePuzzle чуть ранее в этом уроке.

Прежде чем мы реализуем Dolby Audio API, давайте создадим два метода, которые будут запускать воспроизведение аудио файлов, которые мы добавили ранее. Вы можете пока оставить реализации этих методов пустыми. Метод onFinish вызывается, когда игра заканчивается, когда playSound вызывается всякий раз, когда часть головоломки перемещается.

1
2
3
4
5
6
7
public void onFinish() {
 
}
 
public void playSound() {
 
}

Все, что нам осталось сделать, это вызвать loadBitmap из метода onCreate и передать ему местоположение изображения, которое вы хотите использовать для головоломки.

1
2
3
Uri path = Uri.parse(«android.resource://com.dolby.DolbyPuzzle/» + R.drawable.dolby);
 
loadBitmap(path);

Посмотрите на изображение ниже для примера того, как должна выглядеть ваша игра, в зависимости от изображения, которое вы использовали для головоломки.

Как я упоминал в начале этого урока, интеграция Dolby Audio API проста и занимает всего несколько минут. Давайте посмотрим, как мы можем использовать Dolby Audio API в нашей игре.

Начните с загрузки Dolby Audio API с сайта разработчика Dolby . Для этого создайте бесплатную учетную запись разработчика или войдите в систему, если она у вас уже есть. После загрузки API добавьте библиотеку в свой проект.

Прежде чем интегрировать Dolby Audio API, рекомендуется добавить регуляторы громкости в свое приложение. Это легко сделать и занимает всего одну строку кода. Добавьте следующий фрагмент кода в метод onCreate вашей деятельности

1
setVolumeControlStream(AudioManager.STREAM_MUSIC);

Следующим шагом является объявление двух переменных в вашем классе Activity , экземпляра класса MediaPlayer и экземпляра класса DolbyAudioProcessing . Не забудьте добавить требуемый импорт вверху.

1
2
3
4
5
import android.media.MediaPlayer;
import com.dolby.dap.*;
 
MediaPlayer mPlayer;
DolbyAudioProcessing mDolbyAudioProcessing;

Теперь мы заставим класс Activity принять интерфейсы OnDolbyAudioProcessingEventListener и MediaPlayer.OnCompletionListener .

1
2
3
4
public class SlidePuzzleMain extends Activity implements MediaPlayer.OnCompletionListener,
        OnDolbyAudioProcessingEventListener {
    …
}

Чтобы принять эти интерфейсы, нам нужно реализовать несколько методов, как показано в фрагменте кода ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
// 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) {}

Мы DolbyAudioProcessing объект DolbyAudioProcessing при onDolbyAudioProcessingClientConnected и снова отключаем его при onDolbyAudioProcessingClientDisconnected .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@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) {}

Как вы можете видеть в предыдущем фрагменте кода, мы освобождаем объект MediaPlayer когда он заканчивает воспроизведение аудиофайла.

Чтобы воспроизводить звук, когда игрок перемещает кусок головоломки, нам нужно реализовать метод playSound . Прежде чем сосредоточиться на playSound , мы сначала создаем экземпляр SlidePuzzleMain в классе SlidePuzzleView а в методе представления playSlide мы вызываем playSound SlidePuzzleMain экземпляра SlidePuzzleMain .

1
2
3
4
private void playSlide() {
  SlidePuzzleMain activity = (SlidePuzzleMain) getContext();
  activity.playSound();
}

В методе playSound мы создаем экземпляр класса MediaPlayer и используем API-интерфейс Dolby Audio, чтобы инициировать обработку аудио. Если Dolby Audio API не поддерживается устройством пользователя, метод getDolbyAudioProcessing вернет значение null .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
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;
  }
}

Как вы можете видеть ниже, реализация onFinishметода очень похожа на реализацию playSound. Основным отличием является то, что мы показываем сообщение пользователю, если Dolby Audio API не доступен. Как вы, возможно, помните, onFinishметод используется, когда игра закончена, и игрок завершил головоломку.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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();
  }
}

Мы также призываем shuffleв конце onFinishначать новую игру, когда игрок закончил головоломку.

Очень важно , что мы выпускаем DolbyAudioProcessingи MediaPlayerобъекты , когда они больше не нужны. Освобождение этих объектов гарантирует, что мы не снизим время автономной работы устройства и не окажем негативного влияния на производительность устройства.

Мы начнем с объявления трех методов. Первый метод releaseDolbyAudioProcessing, освобождает DolbyAudioProcessingобъект и устанавливает его mDolbyAudioProcessingв null. Второй метод, restartSessionперезапускает сеанс, управляемый DolbyAudioProcessingобъектом, а в третьем методе suspendSessionаудиосеанс приостанавливается, и текущая конфигурация сохраняется для дальнейшего использования.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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));
}

Как вы можете видеть в приведенном выше фрагменте кода, я также создал несколько методов для обработки исключений , которые могут быть отброшены в releaseDolbyAudioProcessing, restartSessionи suspendSession.

Три метода, которые мы только что создали, должны вызываться в несколько ключевых моментов жизненного цикла приложения. Мы добиваемся этого путем переопределения onStop, onStart, onDestroy, onResumeи onPauseметод в нашем SlidePuzzleMainклассе.

В onStop, мы говорим MediaPlayerобъект , чтобы сделать паузу и onStartна MediaPlayerобъекте продолжает воспроизведение , если это не null. onDestroyМетод вызывается , когда приложение закрыто. В этом методе мы освобождаем MediaPlayerобъект, устанавливаем mPlayerв nullи вызываем releaseDolbyAudioProcessing, что мы реализовали ранее.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@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();
}

Наконец, в onPauseи onResumeмы приостанавливаем и перезапускаем аудио сеанс, вызывая suspendSessionи restartSessionсоответственно.

Если вы выполнили шаги, описанные в этом руководстве, то ваша игра теперь должна быть полностью функциональной с интегрированным Dolby Audio API. Постройте проект, чтобы поиграть с конечным результатом.

Я уверен, что вы согласны с тем, что интеграция Dolby Audio API проста и занимает не более пяти минут. Давайте кратко суммируем шаги, которые мы предприняли для интеграции API.

  1. импортировать библиотеку Dolby Audio API
  2. создать экземпляр DolbyAudioProcessingкласса
  3. реализовать OnDolbyAudioProcessingEventListenerинтерфейс.
  4. включить DolbyAudioProcessingэкземпляр вonDolbyAudioProcessingClientConnected
  5. отключить DolbyAudioProcessingэкземпляр вonDolbyAudioProcessingClientDisconnected
  6. после запуска медиаплеера инициализируйте DolbyAudioProcessingэкземпляр с помощью GAMEпрофиля
  7. проверить, должен ли DolbyAudioProcessingобъект nullпроверять, поддерживается ли Dolby Audio API устройством
  8. Чтобы продлить срок службы батареи и оптимизировать производительность, приостановите и отпустите DolbyAudioProcessingэкземпляр, когда приложение уничтожено или перемещено в фоновый режим.

Несмотря на то, что игра, которую мы создали, довольно проста, важно помнить об этом уроке — Dolby Audio API. Мобильный рынок — многолюдное место, и отличаться от других игр непросто. Добавление превосходного звука в вашу игру не останется незамеченным для ваших пользователей, и это сделает вашу игру заметной. Зайдем на сайт разработчиков Dolby в дать ему попробовать.