熟悉的陌生人ImageView

前言

项目中产品经理提了一个对话框全屏背景旋转的效果图:

背景是一张从中心位置向四周发射光柱的图片,并且有一个不停旋转的动画。

看到这个需求的时候,马上想到的一个方案是用一个填充满全屏的ImageView来承载这个图片,然后用一个旋转的属性动画应用在这个 ImageView 上

但是这里有一个问题

如果 ImageView 的大小仅仅是屏幕的大小,旋转过后它的形状必然被屏幕裁剪,导致4个边角有留白区域,达不到我们想要的全屏背景图旋转的效果。

问题点是 ImageView 旋转时被屏幕裁剪后留有空白。那么让 ImageView 的区域扩展到屏幕以外保证它的长和宽都大于屏幕的对角线,这样无论怎么旋转 ImageView 它都不会被屏幕裁剪后有边角空白区域了,思路是这样,但是好像没有什么实际可操作性,Android 中这个 ImageView 能否超出屏幕还是个问题,超出父控件倒有个属性为 clipChildren ,设置为 false 就可以不裁剪子 View 了,虽然这么说但是还是要尝试一下,各种调试属性加 Google 一番发现没什么卵用,这就尴尬了。

我们需要换一个方向再来考虑这个问题

既然要达到图形旋转的效果,除了旋转这个图形容器载体 ImageView 外,我们可以通过改变 ImageView 内部的 bitmap 来达到视觉上旋转的效果,而操作 bitmap 变换是通过矩阵变换来实现的,具体代码如下:

//计算需要放大的倍数
private void caclulateScaleFactor(){
    mScreenWidth = getDialog().getWindow().getWindowManager().getDefaultDisplay().getWidth();
    mScreenHeight = getDialog().getWindow().getWindowManager().getDefaultDisplay().getHeight();
    double r = Math.sqrt(mScreenWidth * mScreenWidth + mScreenHeight * mScreenHeight);
    int min = Math.min(mBgBitmap.getWidth(), mBgBitmap.getHeight());
    scaleFactor=(float) Math.ceil(r / min);
}

 //利用handler.sendEmptyMessageDelayed来实时刷新bitmap来达到旋转的效果

private Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        if (MSG_ROTATE_ANIMATOR == msg.what) {
            if (mBgImageView == null) {
                return;
            }
            Matrix matrix=new Matrix();
            rotateDegree=rotateDegree+1;
            //旋转
            matrix.postRotate(rotateDegree);
            //放大
            matrix.postScale(scaleFactor,scaleFactor);
            //创建经过处理后的bitmap
            Bitmap nextBitmap=Bitmap.createBitmap(mBgBitmap,0,0,mBgBitmap.getWidth(),mBgBitmap.getHeight(),matrix,true);
            //重新设回到ImageView上
            mBgImageView.setImageBitmap(nextBitmap);
            mHandler.sendEmptyMessageDelayed(MSG_ROTATE_ANIMATOR, 50);
        }
    }
};

这里主要是利用Bitmap.createBitmap(source,px,py,width,height,matrix,filter)这个函数来对bitmap应用matrix并生成新的bitmap,形状变换主要是通过matrix来操作。matrix矩阵变换通俗来说就是将bitmap所有元素点的坐标经过矩阵运算得到新的坐标点来达到变换的作用,具体可以看这边文章
安卓自定义View进阶-Matrix原理

但是

运行后发现虽然达到了满屏旋转的效果,但是页面比较卡顿,内存监控图上是一个频率很高的锯齿线图,表明内存抖动的厉害,频繁 gc 导致阻塞了 UI。

其实看看代码也可以知道,我们在一个50ms的间隔循环内创建新的 bitmap ,bitmap 的内存占用还是很大的。

那么有没有不新建 bitmap 对原始 bitmap 进行转换并应用在 ImageView 上面的方法呢?

Canvas 类中有一个方法是:

canvas.drawBitmap(bitmap,matrix,paint);  

会通过 matrix 对 bitmap 进行处理后绘制在 canvas 上,不需要创建一个新的 bitmap,但是这个 canvas 是 View 内部绘制执行时的 canvas,我们通过在 View 外部直接 new 一个 Canvas 并没有什么用 倒是可以通过

 Canvas canvas=new Canvas();

 canvas.setBitmap(bitmap);  

 canvas.drawBitmap(sourceBitmap,matrix,paint);

将画布的绘制又转化到 bitmap 上,但是这样又会有创建一个新的 bitmap 的问题

所以只能通过重写继承ImageView的onDraw方法来操作

但是单为这去重写一个ImageView不是一个简洁的方案,同时侵入性较高。

还有这种操作?

阅读了一番 ImageView 的源码后发现了这么一个方法

ImageView.setImageMatrix(matrix)

/**
    * Adds a transformation {@link Matrix} that is applied
    * to the view's drawable when it is drawn.  Allows custom scaling,
    * translation, and perspective distortion.
    *
    * @param matrix the transformation parameters in matrix form
    */
    public void setImageMatrix(Matrix matrix) {
        // collapse null and identity to just null
        if (matrix != null && matrix.isIdentity()) {
            matrix = null;
        }

        // don't invalidate unless we're actually changing our matrix
        if (matrix == null && !mMatrix.isIdentity() ||
                matrix != null && !mMatrix.equals(matrix)) {
            mMatrix.set(matrix);
            configureBounds();
            invalidate();
        }
    }  

这个方法会将一个矩阵 matrix 设置在 ImageView 内部,然后在 draw 的时候会应用上这个 matrix ,这不正是我们想要的吗?我们继续看 ImageView 的 onDraw() 方法

protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        if (mDrawable == null) {
            return; // couldn't resolve the URI
        }

        if (mDrawableWidth == 0 || mDrawableHeight == 0) {
            return;     // nothing to draw (empty bounds)
        }

        if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) {
            mDrawable.draw(canvas);
        } else {
            final int saveCount = canvas.getSaveCount();
            canvas.save();

            if (mCropToPadding) {
                final int scrollX = mScrollX;
                final int scrollY = mScrollY;
                canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop,
                        scrollX + mRight - mLeft - mPaddingRight,
                        scrollY + mBottom - mTop - mPaddingBottom);
            }

            canvas.translate(mPaddingLeft, mPaddingTop);

            if (mDrawMatrix != null) {
                canvas.concat(mDrawMatrix);
            }
            mDrawable.draw(canvas);
            canvas.restoreToCount(saveCount);
        }
    }   

可以看到这个函数最后几行代码中会将 matrix 应用到 canvas 中去,并绘制出来
这个 mDrawMatrix 其实就是在刚才的setImageMatrix(matrix)函数中的

  configureBounds()

中被赋值给 matrix 的。不过还需要一个条件就是 ImageView 的 ScaleType 为 Matrix

最终方案

private void initRotateBgImageView() {
        mBgBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.img_building_overlay);
        mBgImageView.setImageBitmap(mBgBitmap);
        mBgImageView.setScaleType(ImageView.ScaleType.MATRIX);
        mScreenWidth = getDialog().getWindow().getWindowManager().getDefaultDisplay().getWidth();
        mScreenHeight = getDialog().getWindow().getWindowManager().getDefaultDisplay().getHeight();
        double r = Math.sqrt(mScreenWidth * mScreenWidth + mScreenHeight * mScreenHeight);
        int min = Math.min(mBgBitmap.getWidth(), mBgBitmap.getHeight());
        float scale = (float) Math.ceil(r / min);
        float px = (scale * (mBgBitmap.getWidth() / 2)) - mScreenWidth / 2;
        float py = (scale * (mBgBitmap.getHeight() / 2)) - mScreenHeight / 2;
        //放大bitmap已达到旋转后的bitmap仍然能够充满屏幕
        matrix.postScale(scale, scale);
        //移动放大后的bitmap的中心到屏幕的中心
        matrix.postTranslate(-px, -py);
        mBgImageView.setImageMatrix(matrix);
        mHandler.sendEmptyMessageDelayed(MSG_ROTATE_ANIMATOR, 50);
    }

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (MSG_ROTATE_ANIMATOR == msg.what) {
                if (mBgImageView == null) {
                    return;
                }
                //旋转操作
                matrix.postRotate(ORIENTATION_DEGREE, mScreenWidth / 2, mScreenHeight / 2);
                mBgImageView.setImageMatrix(matrix);
                mHandler.sendEmptyMessageDelayed(MSG_ROTATE_ANIMATOR, 50);
            }
        }
    };

最后

谢谢阅读!