Работа c QR на Android - lanit-tercom-school/grouplock GitHub Wiki

TL;DR

Скачать готовый пример можно здесь (требуется версия Android 4.1 JellyBean и выше). Имейте в виду, что это лишь заготовка для дальнейшей работы, и сейчас в ней довольно много багов (например, нельзя кодировать символы кириллицы, приложение вылетает при попытке закодировать пустую строку, а также есть проблемы с камерой при повороте экрана). Сгенерированные коды сохраняются на внутреннюю память телефона в папку GroupLock.

Введение

В этой статье описываются две библиотеки для работы с QR-кодами на платформе Android – ZXing и ZBar. Перед дальнейшим чтением желательно:

  1. Знать, что такое QR-код. Хорошие ресурсы на эту тему – эта статья и сайт создателей QR-кода
  2. Понимать базовые принципы разработки приложений под Android (в этом вам поможет статья Android DevGuide). Activity, manifest, ресурсы приложения – все эти термины не должны вызывать недоумение.

Для генерации QR-кодов мы будем использовать библиотеку ZXing. Эта библиотека является, наверное, самой известной в своей категории – она используется в большинстве приложений для работы с QR-кодами в магазине Google Play. Для сканирования кодов будем использовать библиотеку ZBar. Эта библиотека сканирует коды заметно быстрее других, так как написана на C. К тому же, пользоваться ей намного удобнее, чем той же ZXing. К сожалению, генерировать коды она не умеет.

Начало работы

Первым делом необходимо добавить библиотеки в проект. ZXing мы добавим из репозитория Maven, а ZBar – вручную.

Скачиваем дистрибутив ZBar. Заходим в директорию модуля нашего приложения (предположим, он называется main). Создаём папку libs, если она не создана, и помещаем в неё файл zbar.jar. Затем в папке src/main создаём подпапку jniLibs. Копируем в неё папки armeabi, armeabi-v7a и x86. Теперь структура проекта должна выглядеть примерно так:

Структура UPDATE: на рисунке ошибка, позже исправлю

После этого в файле build.gradle внутри папки main в секции dependencies пишем зависимости:

compile 'com.google.zxing:core:3.2.1'
compile files ('libs/zbar.jar')

Теперь можно скомпилировать приложение и, открыв окно File > Project Structure > Libraries, убедиться, что библиотеки подключены.

Далее нужно прописать разрешения для приложения – на сохранение файлов в память телефона и на работу с камерой. Открываем файл AndroidManifest.xml (он находится в папке модуля main) и в начале секции manifest пишем следующее:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />

Генерация и сохранение QR-кодов

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

Для реализации генератора будем использовать класс, который наследуется от AsyncTask. Подробнее про AsyncTask можно прочитать здесь, я же вкратце опишу, что это такое. AsyncTask используется для выполнения длительных задач в фоновом режиме, таких как загрузка данных из интернета или сохранение данных на диск. Его использование не блокирует поток пользовательского интерфейса. Это значит, что приложение не будет "зависать" во время выполнения ресурсоёмкой задачи.

Итак, создадим класс генератора:

public class AsyncQRGenerator extends AsyncTask<String, Integer, ArrayList<Bitmap>> {
}

(Замечание: здесь и далее я не буду указывать все необходимые import-ы, так как IDE импортирует все классы автоматически либо по нажатию Alt + Enter)

Подробнее о параметрах в треугольных скобках:
Первый тип – это тип входного параметра для задачи. В нашем случае это String, так как мы кодируем строки.
Второй тип – тип для отображения прогресса. В нашем случае прогресс измеряется количеством QR-кодов, которые мы уже закодировали (например, 4 из 10). О прогрессе будет рассказано далее.
Третий тип – тип возвращаемого значения. Здесь это список элементов Bitmap, то есть изображений.

Замечу, что в первом параметре мы указали просто String, а не ArrayList<String>, так как входных параметров может быть сколько угодно, а возвращаемый параметр всегда один.

Далее пропишем конструктор для генератора:

Activity activity;

public AsyncQRGenerator(Activity a) {
    this.activity = a;
}

Здесь мы просто создаём ссылку на Activity, из которого мы запускаем генератор. Эту ссылку в дальнейшем мы будем использовать для отображения сообщения об успешном завершении генерации.

Теперь немного о цикле выполнения асинхронной задачи. При запуске задачи сначала выполняется метод onPreExecute() – в нём обычно происходит инициализация всех необходимых свойств, полей и т.п. Затем выполняется метод doInBackground() – в нём, собственно, и описывается длительная задача. При изменении прогресса выполнения вызывается метод onProgressUpdate(). Наконец, после завершения длительной задачи выполняется метод onPostExecute(). Наша цель – переопределить эти четыре метода. Для простоты мы пока не будем реализовывать логику отмены задачи.

Итак, метод onPreExecute():

private ProgressDialog dialog;

@Override
protected void onPreExecute() {
    dialog = new ProgressDialog(activity);
    dialog.setTitle("Generating...");
    dialog.setMessage("Generating QR code, please wait...");
    dialog.setCancelable(false);
    dialog.show();
    super.onPreExecute();
}

В этом методе мы создаём всплывающее окно с сообщением о выполняемой работе.
(Замечание: здесь для простоты я указываю сообщения прямо в коде. В реальном проекте рекомендуется использовать строковые ресурсы.)
Строка super.onPreExecute() вызывает реализацию метода у родителя. Это нужно делать во многих случаях, чтобы всё работало без ошибок.

Теперь переходим к методу doInBackground():

private static final int WIDTH = 400; /* ширина выходного изображения */

@Override
protected ArrayList<Bitmap> doInBackground(String... params) {
    ArrayList<Bitmap> list = new ArrayList<Bitmap>();
    for (int i = 0; i < params.length; i++) {
        try {
            /* Используется библиотека ZXing.
               Первый параметр - исходная строка.
               Второй параметр - формат кода (ZXing умеет не только QR)
               Третий и четвёртый - размер матрицы */
            BitMatrix matrix = new QRCodeWriter().encode(
                    params[i],
                    com.google.zxing.BarcodeFormat.QR_CODE,
                    WIDTH, WIDTH);
            /* Конвертируем матрицу битов в картинку */
            Bitmap bitmap = matrixToBitmap(matrix);
            /* Сохраняем файл */
            saveBitmapAsImageFile(bitmap, String.valueOf(i + 1));
            list.add(bitmap);
            /* Сообщаем об очередном сгенерированном коде */
            publishProgress(i + 1, params.length);
        } catch (WriterException e) {
            e.printStackTrace();
        }
    }
    return list;
}

На вход здесь подаётся набор строк. Многоточие означает, что количество строк неизвестно. Далее все эти строки представляются как массив params.

Метод matrixToBitmap() генерирует картинку по матрице битов.

private static final int BLACK = 0xFF000000;
private static final int WHITE = 0xFFFFFFFF;

private Bitmap matrixToBitmap(BitMatrix matrix) {
    int width = matrix.getWidth();
    int height = matrix.getHeight();
    Bitmap image = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    for (int x = 0; x < width; x++) {
        for (int y = 0; y < height; y++) {
            image.setPixel(x, y, matrix.get(x, y) ? BLACK : WHITE);
        }
    }
    return image;
}

По сути, мы здесь просто проходимся по каждому биту и закрашиваем i,j-й пиксель в соответствующий цвет. Кстати, цвета могут быть не только белым и чёрным, QR-код успешно сгенерируется и при любом другом наборе цветов.
Что такое ARGB_8888, я, к сожалению, не знаю. Наверное, название какого-то формата.

Метод saveBitmapAsImageFile() сохраняет файл на диск. Для простоты в нашем примере в качестве имени файла мы передаём индекс строки в массиве.

/* Сохраняет bitmap на SD-карту в файл с названием fileName.
   Возвращает true, если сохранение успешно, и false, если сохранить не удалось. */
private boolean saveBitmapAsImageFile(Bitmap bitmap, String fileName) {
    /* Получаем путь до папки сохранения */
    String storagePath = Environment.getExternalStorageDirectory() + "/GroupLock/";
    File sdDir = new File(storagePath);
    /* Создаём директорию */
    sdDir.mkdirs();

    try {
        /* Создаём необходимые потоки */
        String filePath = sdDir.getPath() + "/" + fileName + ".png";
        FileOutputStream fileOutputStream = new FileOutputStream(filePath);

        /* Сохраняем файл в формате .png */
        BufferedOutputStream bos = new BufferedOutputStream(fileOutputStream);
        bitmap.compress(Bitmap.CompressFormat.PNG, 100, bos);

        /* Закрываем потоки */
        bos.flush();
        bos.close();

    } catch (FileNotFoundException e) {
        e.printStackTrace();
        return false;
    } catch (IOException e) {
        e.printStackTrace();
        return false;
    }

    return true;
}

Метод onProgressUpdate перезаписывает сообщение во всплывающем окне.

@Override
protected void onProgressUpdate(Integer... values) {
    super.onProgressUpdate(values);
    dialog.setMessage(values[0] + " of " + values[1] + " done...");
}

Первое значение, которое подаётся на вход – это количество готовых кодов. Второе – общее количество.

Наконец, метод onPostExecute:

@Override
protected void onPostExecute(ArrayList<Bitmap> bitmap) {
    try {
        /* Закрываем диалоговое окно */
        dialog.dismiss();
    } catch (Exception e) {
        e.printStackTrace();
    }

    /* Оповещаем пользователя об успешном завершении */
    String message = "QR codes generated successfully!";
    Toast toast = Toast.makeText(activity, message, Toast.LENGTH_SHORT);
    toast.show();

    super.onPostExecute(bitmap);
}

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

Всё, генератор готов! Теперь его нужно вызвать из нашего Activity:

String source = "test";
new AsyncQRGenerator(this).execute(source);

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

Пример считывания QR-кода

Сначала необходимо загрузить библиотеку iconv в память:

public class MainActivity extends Activity { 
    static {
        System.loadLibrary("iconv");
    }

    ...

}

После этого можно использовать библиотеку ZBar.

Инициализируем сканер:

ImageScanner scanner = new ImageScanner();
scanner.setConfig(0, Config.X_DENSITY, 3);
scanner.setConfig(0, Config.Y_DENSITY, 3);

Почему указываются именно эти параметры, нигде не указано.

Здесь я не буду описывать работу с камерой, это тема отдельной статьи, да и я сам не особо хорошо разобрался, как там всё устроено. Примерная схема работы такая: после подключения к камере устройства на экран выводится изображение. Затем каждый кадр передаётся в метод onPreviewFrame(). Этот метод мы и будем использовать для анализа изображения:

/* data - данные с камеры (текущий кадр) */
public void onPreviewFrame(byte[] data, Camera camera) {
    Camera.Parameters parameters = camera.getParameters();
    Camera.Size size = parameters.getPreviewSize();

    /* Запоминаем кадр */
    Image barcode = new Image(size.width, size.height, "Y800");
    barcode.setData(data);

    /* Анализируем кадр */
    int result = scanner.scanImage(barcode);

    /* Если что-то нашли */
    if (result != 0) {
        /* ... останавливаем предпросмотр ... */

        /* Получаем и выводим данные */
        SymbolSet symbols = scanner.getResults();
        for (Symbol sym : symbols) {
            String textResult = sym.getData();
            Toast toast = Toast.makeText(getContext(), textResult, Toast.LENGTH_LONG);
            toast.show();
        }
    }
}

Не совсем понятно название класса Symbol, ведь это не отдельный символ, а целая строка с результатом. Но создателям библиотеки виднее.
Результат сканирования сохраняется в переменную textResult. Дальше его можно передавать куда угодно, например, в метод, который будет расшифровывать файлы.

⚠️ **GitHub.com Fallback** ⚠️