2

Carpetgram, или использование стандартных возможностей Android SDK для работы с 2D-графикой на примере одного приложения

Таки здравствуйте, что-то давненько в нашем блоге не было постов от имени «мобильного гетто». Последний пост датирован аж 24 ноября 2011 года, в нем мы рассказывали про Примечательности Ижевска для iPhone. И в это морозное утро, мы всем отделом решили исправить эту досадную ситуацию. Вполне возможно, что родится целый цикл статей о наших внутренних и не очень внутренних разработках. Очередной вехой внутренних разработок мобильного отдела был проект Carpetgram. В этом посте я напишу об интересных вещах, с которыми мы столкнулись при разработке приложения под платформу Android.

Для начала упомяну инструменты, которые мы использовали и используем для разработки.

  1. Eclipse и Android Studio. Основной функционал был написан в Eclipse. Потом мы решили попробовать перенести проект на Android Studio и Gradle. В целом, несмотря на preview-версию, все работало стабильно, но были версии обновлений, которые ломали все, в большинстве своем это были проблемы с Gradle.

  2. Gradle в качестве системы сборки. Мы не стали прикручивать другую систему сборки, так как Gradle идет вместе со студией из коробки и позиционируется как комплексное решение для разработки под Android. Но мы столкнулись с некоторыми проблемами, например, со сборкой JNI.

  3. Genymotion для тестирования и отладки. Удобно, но без багов тоже не обошлось. При определенных условиях ADB теряет соединение с запущенной виртуальной машиной.

  4. Mercurial в качестве VCS.

  5. Testflight для публикации билдов. Сильно облегчает жизнь для тестирования билдов, особенно если интегрировать их sdk в приложение.

Это основой набор используемых инструментов. Использовались и другие специфичные инструменты из стандартной поставки Android SDK, например, hierarchyviewer, pixelperfect, lint, но о них я пока не буду упоминать.

Разработка

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

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

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

  • moveTo(float x, float y) — этот метод устанавливает начало следующего контура в точку (x,y). Данный метод вызывается, когда пользователь касается экрана.

  • quadTo(float x1, float y1, float x2, float y2) — этот метод рисует кривую Безье второго рода  P(t) = (1 — t)2P0 + 2(1 — t)tP1 + t2P2 от последней точки (предыдущего path) P0, приближаясь к точке P1 (x1,y1) и заканчивая в точке P2 (x2,y2). Собственно, именно с помощью этого метода и рисуется контур, пока пользователь ведет пальцем по экрану.

Для реализации всеми любимой функции Ctrl+Z (или Command+Z привет маководам!) мы создали список, в котором хранятся куски контура в порядке их появления на экране:

List<Path> mPathsList = new ArrayList<Path>(8);

В этом случае мы всегда можем извлечь предыдущую версию контура.

Обведение контура — это пол беды, главной проблемой все же оставалась сама функция вырезания. Для решения этой задачи был использован класс PorterDuffXferMode. Он используется для создания масок при наложении двух изображений. В сочетании с режимом PorterDuff.Mode.SRC, при использовании которого в комбинированном изображении остается только исходное, он решает поставленную задачу. Ниже скриншот примера из Android SDK, он поясняет принцип работы данного класса и его режимов.

 

 

В итоге упрощенный вариант кода получается примерно таким:

Paint mTransparentPaint = new Paint();

mTransparentPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));

mTransparentPaint.setStyle(Paint.Style.FILL);

mCanvas.drawPath(mLastPath, mTransparentPaint);

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

canvas.drawBitmap(mCarpet, mCarpetMatrix, null);

canvas.drawBitmap(mBitmap, mCurrentMatrix, null);

Все оказалось достаточно просто, но как известно дьявол кроется в деталях. А именно в особенностях работы с Bitmap и памятью, выделяемой для них в системе Android. О них я расскажу чуть-чуть позже. А чтобы эти особенности не влияли на работоспособность приложения, пришлось выполнить некоторые оптимизации. В частности изменить размеры исходного Bitmap, после того как пользователь вырезал необходимый контур. Для того, чтобы минимизировать занимаемую этим Bitmap память.Реализовано это было следующим образом:

  1. Вычисляем максимальные и минимальные значения координат х и у

    // mMaxX, mMaxY,mMinX, mMinY — максимальные и минимальные значения

    // координат контура x и y относительно экрана, в realMin и realMax ..

    // записываются минимальные и максимальные значения относительно

    // изображения

    final PointF realMax = calculateRealCoordinate(mMaxX, mMaxY);

    final PointF realMin = calculateRealCoordinate(mMinX, mMinY);

    /*

    ..*/

    // вычисление значений координат относительно текущего изображения

    // это необходимо, потому что можно сперва увеличить кусок изображения,

    // а после рисовать контур

    private PointF calculateRealCoordinate(float x, float y) {

     

    mCurrentMatrix.getValues(mMatrixVal);

    final float origX = (x — (mMatrixVal[Matrix.MTRANS_X])) / mMatrixVal[Matrix.MSCALE_X];

    final float origY = (y — (mMatrixVal[Matrix.MTRANS_Y])) / mMatrixVal[Matrix.MSCALE_Y];

     

    return new PointF(origX, origY);

    }

  2. Вырезаем из картинки прямоугольник

    mCanvas.clipRect(realMin.x, realMin.y, realMax.x, realMax.y);

Таким образом, в памяти хранится не вся картинка, а только прямоугольная область, ограничивающая вырезанный контур.

Как уже было сказано выше, в системе Android работа с Bitmap это довольно нетривиальная задача. Память для Bitmap резервируется из общей кучи процесса, поэтому главный принцип при работе с большим количеством изображений — стараться использовать как можно меньше памяти и вовремя её освобождать. Иначе можно получить такую ошибку AndroidRuntime: java.lang.OutOfMemoryError: bitmap size exceeds VM budget.

Особенно актуален этот принцип, если приложение активно взаимодействует с камерой. Стратегия эффективного использования Bitmap зависит от версии Android, так как на разных версиях отличается способ их хранения. В Android 2.3 и ниже данные о пикселях Bitmap хранятся в нативной памяти, отдельно от самой Bitmap, которая хранится в куче. Невозможно прогнозировать, когда именно освободится память, и это может привести к превышению лимита памяти для приложения. Начиная с Android 3.0 все хранится в куче.

В Android 2.3 и ниже необходимо уничтожать неиспользуемые Bitmap, вызывая recycle(). Стоит обратить внимание на то что, если после вызова recycle() приложение вновь обратится к этой же Bitmap, произойдет ошибка «Canvas: trying to use a recycled bitmap». Начиная с Android 3.0, появилась возможность установить опцию BitmapFactory.Options.inBitmap. Это позволяет при загрузке контента повторно использовать память выделенную для Bitmap, что повышает производительность.

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

  • Правильный Holo дизайн

  • Интеграция с Carpetstream

 

А также:

  • Добавим фильтры для изображений

  • Вернемся к изначальной концепции по вырезанию контура

И еще много чего интересно. Всем ковров добра и добра ковров.

 

 

Метки:

2 комментария к записи «Carpetgram, или использование стандартных возможностей Android SDK для работы с 2D-графикой на примере одного приложения»

  1. Andrew Podkin,

    По выделению памяти есть еще такая штука: по умолчанию приложению предоставляется довольно маленькая куча, но можно в манифесте запросить большую. Я правда не пробовал это именно с битмапами, но с другими данными работает хорошо (даже на слабом устройстве у меня получилось захватить > 100 Мб в отличие от 50 со стандартной кучей).

    • Андрей Иосифович Плиткенштейн Андрей Иосифович Плиткенштейн,

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

Оставить комментарий