熟悉的陌生人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);
}
}
};
最后
谢谢阅读!