Android ImageView.setMatrix() и .invalidate() - перекраска занимает слишком много времени

Задача: Я хочу изменить размер и перемещать изображение по экрану. Я хочу сделать это плавно независимо от того, насколько большой размер изображения. Код должен поддерживаться уровнем API API.

Проблема: Я попытался использовать ImageView с scaleType="matrix". Вызов ImageView.setMatrix(), а затем ImageView.invalidate() отлично работает с маленькими изображениями, но ужасен с большими. Независимо от того, насколько велика ImageView.

Можно ли как-то ускорить перерисовку ImageView, чтобы он не пересчитал целое изображение? Может быть, есть способ выполнить задачу с использованием другого компонента?


EDIT: дополнительная информация о том, чего я пытаюсь достичь.

  • pw, ph - ширина и высота изображения (в пикселях)
  • dw, dh - ширина и высота отображения устройства (в пикселях)
  • fw, fh - ширина и высота видимого кадра (в пикселях)
  • x, y - положение верхнего левого угла кадра (в пикселях) Визуализация проблемы.

Я хочу отобразить часть изображения на экране. Свойства x, y, fw и fh постоянно меняются. Я ищу часть кода (идеи) или компоненты, которые для этих 8 заданных переменных будут быстро генерироваться и отображать часть изображения.


EDIT 2: информация в pw и ph

Я предполагаю, что pw и ph могут хранить значения от 1 до бесконечности. Если этот подход вызывает много проблем, мы можем предположить, что изображение не больше изображения, сделанного камерой устройства.

Ответы

Ответ 1

С вашей помощью (сообщества) я выяснил решение. Я уверен, что есть и другие способы сделать это, но мое решение не очень сложно и должно работать с любым изображением, любым Android с уровня API 8.

Решение состоит в использовании двух ImageView объектов вместо одного.

Первый ImageView будет работать как раньше, но загруженное изображение будет уменьшено, так что ширина будет меньше ширины ImageView, а высота будет меньше высоты ImageView.

Второй ImageView будет пустым в начале. Каждый раз, когда изменяются свойства x, y, fw и fh, AsyncTask будет выполняться только для загрузки видимая часть изображения. Когда свойства быстро меняются, AsyncTask не сможет закончить вовремя. Он должен быть отменен, и новый будет запущен. Когда он завершит результат, Bitmap будет загружен на второй ImageView, чтобы он был видимым для пользователя. Когда свойства снова будут изменены, Bitmap будет удален, поэтому он не будет перемещать перенос Bitmap в первый ImageView. Примечание. BitmapRegionDecoder, который я буду использовать для загрузки суб-изображения, доступен с уровня API Android 10, поэтому пользователи API 8 и API 9 будут видеть только уменьшенное изображение. Я решил, что все в порядке.


Необходимый код:

  • Установка первого (нижнего) ImageView scaleType="matrix" (лучше всего в XML)
  • Установка второго (верхнего) ImageView scaleType="fitXY" (лучше всего в XML)
  • Функции из документации для Android (здесь) - благодаря пользователю Vishavjeet Singh,

ПРИМЕЧАНИЕ. Обратите внимание на оператор || вместо && при вычислении inSampleSize. Мы хотим, чтобы загруженное изображение было меньше, чем ImageView, так что мы уверены, что для его загрузки достаточно ОЗУ. (Я предполагаю, что размер ImageView не больше размера дисплея устройства. Я также предполагаю, что на устройстве достаточно памяти для загрузки не менее 2 Bitmaps размера дисплея устройства. Скажите, пожалуйста, ошибка здесь.)
ПРИМЕЧАНИЕ 2: Загружаю изображения с помощью InputStream. Чтобы загрузить файл по-другому, вам нужно будет изменить код в try{...} catch(...){...} блоках.

public static int calculateInSampleSize(
        BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) > reqHeight
                || (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}

public Bitmap decodeSampledBitmapFromResource(Uri fileUri,
                                              int reqWidth, int reqHeight) {

    // First decode with inJustDecodeBounds=true to check dimensions
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;

    try {
        InputStream is = this.getContentResolver().openInputStream(fileUri);
        BitmapFactory.decodeStream(is, null, options);
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }

    // Calculate inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // Decode bitmap with inSampleSize set
    options.inJustDecodeBounds = false;

    try {
        InputStream is = this.getContentResolver().openInputStream(fileUri);
        return BitmapFactory.decodeStream(is, null, options);
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
}
  • Функция, возвращающая суб-изображение изображения.

ПРИМЕЧАНИЕ. Размер прямоугольника, который будет вырезан из исходного изображения, относится к изображению. Значения, определяющие его, составляют от 0 до 1, поскольку размер ImageView и загруженного Bitmap отличается от размера исходного изображения.

public Bitmap getCroppedBitmap (Uri fileUri, int outWidth, int outHeight,
                                    double rl, double rt, double rr, double rb) {
        // rl, rt, rr, rb are relative (values from 0 to 1) to the size of the image.
        // That is because image moving will be smaller than the original.
        if (Build.VERSION.SDK_INT >= 10) {
            // Ensure that device supports at least API level 10
            // so we can use BitmapRegionDecoder
            BitmapRegionDecoder brd;
            try {
                // Again loading from URI. Change the code so it suits yours.
                InputStream is = this.getContentResolver().openInputStream(fileUri);
                brd = BitmapRegionDecoder.newInstance(is, true);

                BitmapFactory.Options options = new BitmapFactory.Options();
                options.outWidth = (int)((rr - rl) * brd.getWidth());
                options.outHeight = (int)((rb - rt) * brd.getHeight());
                options.inSampleSize = calculateInSampleSize(options,
                        outWidth, outHeight);

                return brd.decodeRegion(new Rect(
                        (int) (rl * brd.getWidth()),
                        (int) (rt * brd.getHeight()),
                        (int) (rr * brd.getWidth()),
                        (int) (rb * brd.getHeight())
                ), options);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
        else
            return null;
    }
  • AsyncTask Загрузка суб-изображения Bitmap.

ПРИМЕЧАНИЕ: уведомление, объявляющее переменную типа этого класса. Он будет использоваться позже.

private LoadHiResImageTask loadHiResImageTask = new LoadHiResImageTask();

private class LoadHiResImageTask extends AsyncTask<Double, Void, Bitmap> {
        /** The system calls this to perform work in a worker thread and
         * delivers it the parameters given to AsyncTask.execute() */
        protected Bitmap doInBackground(Double... numbers) {
            return getCroppedBitmap(
                    // You will have to change first parameter here!
                    Uri.parse(imagesToCrop[0]),
                    numbers[0].intValue(), numbers[1].intValue(),
                    numbers[2], numbers[3], numbers[4], numbers[5]);
        }

        /** The system calls this to perform work in the UI thread and delivers
         * the result from doInBackground() */
        protected void onPostExecute(Bitmap result) {
            ImageView hiresImage = (ImageView) findViewById(R.id.hiresImage);
            hiresImage.setImageBitmap(result);
            hiresImage.postInvalidate();
        }
    }
  • Функция, которая заставит все работать вместе.

Эта функция будет вызываться каждый раз при изменении свойств x, y, fw или fh. < ш > ПРИМЕЧАНИЕ. hiresImage в моем коде - это id второго (верхнего) ImageView

private void updateImageView () {
        //  ... your code to update ImageView matrix ...
        // 
        // imageToCrop.setImageMatrix(m);
        // imageToCrop.postInvalidateDelayed(10);

        if (Build.VERSION.SDK_INT >= 10) {
            ImageView hiresImage = (ImageView) findViewById(R.id.hiresImage);
            hiresImage.setImageDrawable(null);
            hiresImage.invalidate();
            if (loadHiResImageTask.getStatus() != AsyncTask.Status.FINISHED) {
                loadHiResImageTask.cancel(true);
            }
            loadHiResImageTask = null;
            loadHiResImageTask = new LoadHiResImageTask();
            loadHiResImageTask.execute(
                    (double) hiresImage.getWidth(),
                    (double) hiresImage.getHeight(),
                    // x, y, fw, fh are properties from the question
                    (double) x / d.getIntrinsicWidth(),
                    (double) y / d.getIntrinsicHeight(),
                    (double) x / d.getIntrinsicWidth()
                            + fw / d.getIntrinsicWidth(),
                    (double) y / d.getIntrinsicHeight()
                            + fh / d.getIntrinsicHeight());
        }
    }

Ответ 2

Попробуйте загрузить исходное битовое изображение с помощью параметров BitmapFactory только в границах декодирования

public static int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) > reqHeight
                && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
        int reqWidth, int reqHeight) {

    // First decode with inJustDecodeBounds=true to check dimensions
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    // Calculate inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // Decode bitmap with inSampleSize set
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

После написания этих двух методов

mImageView.setImageBitmap(
    decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

установить растровое изображение на изображении таким образом

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

Вы можете прочитать здесь далее

http://developer.android.com/training/displaying-bitmaps/load-bitmap.html

Ответ 3

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

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

Вы получите то же, что, например, карты google, у них много мелких плит, которые загружаются по требованию. Они не загружают всю карту мира, а небольшие ее части.

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