安卓 ImageView.setMatrix() 和 .invalidate() - 重绘时间过长

15
任务:我想要调整大小并在屏幕上移动一张图片。无论图片大小如何,我都希望能够平滑地完成此操作。代码应该支持API级别8。
问题:我尝试使用scaleType="matrix"的ImageView。调用ImageView.setMatrix()然后ImageView.invalidate()可以很好地处理小图像,但对于大图像则效果很差。无论ImageView有多大。
我是否可以通过某种方式加快ImageView的重绘速度,使其不必重新计算整个图像?也许有一种方法可以使用不同的组件来完成任务吗?

编辑:更多我试图实现的信息。

  • pw,ph-图片的宽度和高度(以像素为单位)
  • dw,dh-设备显示器的宽度和高度(以像素为单位)
  • fw,fh-可见帧的宽度和高度(以像素为单位)
  • x,y-框架左上角的位置(以像素为单位) 问题的可视化。

我想在屏幕上显示图像的一部分。属性xyfwfh不断变化。 我正在寻找一个部分代码(思路),或者组件,可以快速生成并显示图像的这一部分,对于这8个指定的变量。


编辑2:关于pwph的信息

我认为pwph可以保存从1到无穷大的值。如果这种方法会引起很多麻烦,我们可以假设图片不会比使用设备相机拍摄的图片更大。


3
ImageView的作用是显示来自资源的图片,通常是像JPEG或PNG这样的压缩源。它并不特别适用于快速平滑的动画。可以尝试使用带有位图的Canvas或使用纹理的OpenGL ES。请记住,图像的未压缩大小会影响性能。 - fadden
如果只是关于调整大小和移动,动画API应该就足够了。 - natario
所以你想显示一个非常大的图像的子部分?我猜fw/fh与dh/dw成比例,但不是固定的,所以你既在平移又在缩放?pw/ph有多大,即在API 8时代的设备上保持完整图像在内存中是否不合理? - fadden
2
最通用的解决方案是将图像分成一系列瓷砖。加载可见瓷砖的图像,忽略其余部分。滚动时,您只需要加载移动边界处的瓷砖。本质上与Google地图或横向滚动视频游戏相同。 - fadden
@fadden 受到您的评论启发,我想我有一个解决方案。1.使用BitmapFactory加载图像,以便结果位图的宽度不大于显示器的宽度,结果位图的高度不大于显示器的高度。2.每当x、y、fw或fh更改时,启动(相同的)异步进程,生成所选区域的位图。如果该进程已经在工作,请使用其他参数重新启动它。3.如果该进程恰好完成,则将结果显示给用户,覆盖第一个位图。3b 当x、y、fw、fh更改时,请清除生成的位图。 - Filip Hazubski
显示剩余2条评论
3个回答

2
通过您(社区)的帮助,我找到了解决方案。我相信还有其他更好的方法来解决这个问题,但我的解决方案并不复杂,并且应该适用于任何图像和任何API级别为8及以上的Android系统。
解决方案是使用两个ImageView对象,而不是一个。
第一个ImageView将像以前一样工作,但加载的图像将被缩小,使其宽度小于ImageView的宽度,高度小于ImageView的高度。
第二个ImageView在开始时为空白的。每次xyfwfh属性改变时,AsyncTask将被执行,仅加载图像的可见部分。当属性快速变化时,AsyncTask将无法及时完成。它将被取消并启动新的任务。当它完成后,结果Bitmap将加载到第二个ImageView上,因此用户可以看到它。当属性再次更改时,已加载的Bitmap将被删除,因此它不会覆盖加载到第一个ImageView的移动Bitmap注意:我将用于加载子图像的BitmapRegionDecoder自Android API级别10开始提供,因此API 8和API 9用户将只看到缩小的图像。我认为这是可以接受的。
所需代码:
- 将第一个(底部)ImageView设置为scaleType="matrix"(最好在XML中) - 将第二个(顶部)ImageView设置为scaleType="fitXY"(最好在XML中) - Android文档中的函数(在此处) - 感谢用户Vishavjeet Singh

注意:在计算inSampleSize时,使用的是||运算符,而不是&&。我们希望加载的图像比ImageView小,以确保我们有足够的RAM来加载它。(我假设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();
        }
    }
  • 使所有内容协同工作的函数。

每当xyfwfh属性更改时,都会调用此函数。
注意: 我的代码中的hiresImage是第二个(顶部)ImageViewid

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());
        }
    }

1

正如您所说,您可以裁剪大图并将其逐个放置到新的imageView中。

但是从另一方面来看,我建议您将大图分成几个小图,在这种情况下,您可能会节省内存和速度,因为您不会将整个图像加载到内存中。

您将获得类似于例如谷歌地图的东西-它们有许多小块瓷砖,按需加载。他们不会加载整张世界地图,而是其中的一部分。

在这种情况下,您将构建类似于 ListView 的东西,其中每个新项都是一个imageView,并包含作为大图的一部分的某些小图像。顺便说一下,通过这种方法,您甚至可以获得重复平铺的背景,并在运行时更改它们。


1
尝试通过BitmapFactory Options仅以解码边界加载源位图。
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));

像这样在ImageView上设置位图

然后尝试对图像进行缩放,它可以高效地处理大位图

您可以在此处进一步阅读

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


这种方法的问题在于缩小图像后放大则无法显示更多细节。但是我认为BitmapFactory可能是解决问题的方法。我很快会在问题中添加更多细节。 - Filip Hazubski
保持insamplesize=2对图像质量不会有任何影响。我已经测试过了。 - Vishavjeet Singh
当用户进行缩放操作时,会导致图像重新加载。如果图片非常大,则最终会加载过大而无法处理。如果从指定大小开始,我不允许重新加载更大的图像,那么我们将面临第一条评论中所述的情况。 - Filip Hazubski
缩放如何导致图像重新加载? - Vishavjeet Singh
请查看我编辑过的问题。我需要图像的一个子部分。您回答中的代码创建了整个图像的位图,不会超过给定的宽度和高度。这不是我的问题的解决方案。回答您的问题:缩放不会导致重新加载,这是真的。然而,如果我想让用户在缩放后看到更多细节,则需要重新加载它。 - Filip Hazubski

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接