前言
在之前的文章中,通过LED效果、马赛克效果两篇文章,介绍了分片绘制的效果的方法和原理,通过这两篇文章,相信大家都已经熟悉了分片绘制的思路。其实分片绘制不仅仅能实现LED、马赛克等特殊效果,实际上类似百叶窗、图片对角线锯齿过渡等,很多PPT中存在的特效,基本上也是按照这种原理来实现的。
分片可以有很多种意想不到的效,我们再来说一下分片特点:
- [1] 按一定的距离、大小、角度对区域进行对一张图片或者区域裁剪或者提取区域图像
- [2] 对提取出来的区域进行一系列变换,如百叶窗、微信摇一摇等
- [3] 被裁剪的区域可以还原回去
技术前景
其实单纯的分片可以做一些瓦片效果,当然还可以做一些组合效果,下面是一个github开源项目(Camera2DApplication)利用Camera和图片分片实现的效果,这个过程中对一张图片进行分片绘制。
代码中的逻辑不是很复杂,本质上就是利用2张图片实现的,我们先来看下代码实现,作者的代码很认真,注释都写了,涉及postTranslate比较难懂的操作我也进行了微调。
scss
/**
* 3d旋转效果
*
* @param canvas
*/
private void drawModeNormal(Canvas canvas) {
//VERTICAL时使用rotateY,HORIZONTAL时使用rotateX
if (orientation == VERTICAL) {
//如果是前进,则画当前图,后退则画上一张图,注释用的是前进情况
matrix.reset();
camera.save();
//旋转角度 0 - -maxDegress
camera.rotateX(-degress);
camera.getMatrix(matrix);
camera.restore();
//绕着图片top旋转
matrix.preTranslate(-viewWidth / 2f, 0);
//旋转轴向下平移,则图片也向下平移
matrix.postTranslate(viewWidth / 2f, rotatePivotY);
//如果是前进,则画当前图,后退则画上一张图,因为后退时,这里画的是动画下方出来的图片,而下方的图片是前一张图
canvas.drawBitmap(getBitmapScale(bitmapResourceIds.get(isForward ? currentIndex : preIndex), viewWidth, viewHeight),
matrix, mPaint);
//在处理下一张图片
matrix.reset();
camera.save();
//旋转角度 maxDegress - 0
camera.rotateX(maxDegress - degress);
camera.getMatrix(matrix);
camera.restore();
//绕着图片bottom旋转
matrix.preTranslate(-viewWidth / 2f, -viewHeight);
//旋转轴向下平移,则图片也向下平移
matrix.postTranslate(viewWidth / 2f, rotatePivotY);
//如果是前进,则画下一张图,后退则画当前图,后退时,这边代码画的是动画上方的图片,上方的图片是当前图片
canvas.drawBitmap(getBitmapScale(bitmapResourceIds.get(isForward ? nextIndex : currentIndex), viewWidth, viewHeight),
matrix, mPaint);
} else {
//如果是前进,则画当前图,后退则画上一张图,注释用的是前进情况
matrix.reset();
camera.save();
//旋转角度 0 - maxDegress
camera.rotateY(degress);
camera.getMatrix(matrix);
camera.restore();
//绕着图片left旋转
matrix.preTranslate(0, -viewHeight / 2);
//旋转轴向右平移,则图片也向右平移
matrix.postTranslate(rotatePivotX, viewHeight / 2);
//如果是前进,则画当前图,后退则画上一张图,因为后退时,这里画的是动画右方出来的图片,而右方的图片是前一张图
canvas.drawBitmap(getBitmapScale(bitmapResourceIds.get(isForward ? currentIndex : preIndex), viewWidth, viewHeight),
matrix, mPaint);
//在处理下一张图片
matrix.reset();
camera.save();
//旋转角度 -maxDegress - 0
camera.rotateY(-maxDegress + degress);
camera.getMatrix(matrix);
camera.restore();
//绕着图片right旋转
matrix.preTranslate(-viewWidth, -viewHeight / 2f);
//旋转轴向右平移,则图片也向右平移
matrix.postTranslate(rotatePivotX, viewHeight / 2f);
//如果是前进,则画下一张图,后退则画当前图,后退时,这边代码画的是动画左方的图片,左方的图片是当前图片
canvas.drawBitmap(getBitmapScale(bitmapResourceIds.get(isForward ? nextIndex : currentIndex), viewWidth, viewHeight),
matrix, mPaint);
}
}
分片操作
下面是分片操作,这个地方其实可以不用创建Bitmap缓存,创建Path就行,绘制时对Path区域利用Shader贴图即可。
arduino
private Bitmap getBitmapScale(int resId, float width, float height) {
if (ImageCache.getInstance().getBitmapFromMemCache(String.valueOf(resId)) != null) {
return ImageCache.getInstance().getBitmapFromMemCache(String.valueOf(resId));
}
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId);
//创建分片
Bitmap bitmapDst = Bitmap.createScaledBitmap(bitmap, (int) width, (int) height, false);
bitmap.recycle();
ImageCache.getInstance().addBitmapToMemoryCache(String.valueOf(resId)
, bitmapDst);
return bitmapDst;
}
小试一下
我们这里通过一个简单的Demo,实现一种特效,这次我们利用网格矩阵分片。说到矩阵,很多人面试的时候都会遇到一些算法题,比较幸运的人遇到的是矩阵旋转90度、逆时针打印矩阵、矩阵孤岛问题、从左上角开始进行矩阵元素搜索,运气稍差的会遇到由外到里顺时针打印矩阵和斜对角打印矩阵,后面两种看似简单的问题实际上做起来并不顺手,有点扯远了,我们来看看效果。
你没看错,这次遇到了算法问题,我这边用的空间换取时间的方法。
图像分片
将图片分片,计算出网格的列和行
sql
int col = (int) Math.ceil(mBitmaps[index].getWidth() / blockWidth);
int row = (int) Math.ceil(mBitmaps[index].getHeight() / blockWidth);
分片算法
这个算法实际上是每次将列数 +1,然后按对角分割,把符合的区域添加到path中
scss
int x = xPosition;
int y = 0;
while (x >= 0 && y <= row) {
if (x < col && y < row) {
dstRect.set((int) (x * blockWidth), (int) (y * blockWidth), (int) (x * blockWidth + blockWidth), (int) (y * blockWidth + blockWidth));
// bitmapCanvas.drawBitmap(mBitmaps[index], dstRect, dstRect, mCommonPaint);
path.addRect(dstRect, Path.Direction.CCW); //加入网格分片
}
x--;
y++;
}
Path 路径贴图
- Path过程中我们添加的rect是闭合区域,是可以贴图的,当然,一般有三种方法:
- Path的贴图一般使用 clipPath对图片裁剪然后贴图,当然还有将对应的图片区域绘制到View上
- Path 是Rect,按照Rect将图片区域绘制到Rect区域
- 使用BitmapShader一次性绘制
实际上我们应该尽可能使用Bitmap,因为BitmapShader唯一是不存在锯齿性能比较好的绘制方法。
ini
int save = bitmapCanvas.save();
mCommonPaint.setShader(new BitmapShader(mBitmaps[index], Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
bitmapCanvas.drawPath(path,mCommonPaint);
bitmapCanvas.restoreToCount(save);
其实我们的核心代码到这里就结束了,我们可以看到,分片可以的意义很重要的,当然,借助其他工具也可以实现,不过代码实现的好处是可以编辑和交互,不是所有的动画都可以产生交互。
到此,我们还可以对今天的demo添加一些想象
- 从中间外扩效果
- 奇偶行切换效果
- 国际象棋黑白格子变换效果
- ......
总结
这是我们的第三篇关于图片分片特效的博客,希望通过一些了的文章,熟悉一些技术,往往看似高大上的效果,其实就是通过普普通通的方法叠加在一起的,当然,让你的技术承载你的想象,才是最重要的。
本篇demo全部代码
实际上代码贴太多很可能没人看,但是依照惯例,我们给出完整代码。
ini
public class TilesView extends View {
private final DisplayMetrics mDM;
private TextPaint mCommonPaint;
private RectF mainRect = new RectF();
private BitmapCanvas bitmapCanvas; //Canvas 封装的
private Bitmap[] mBitmaps;
private RectF dstRect = new RectF();
Path path = new Path();
private float blockWidth = 50f;
private int xPosition = -2;
private int index = 0;
private boolean isTicking = false;
public TilesView(Context context) {
this(context, null);
}
public TilesView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TilesView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDM = getResources().getDisplayMetrics();
initPaint();
}
private void initPaint() {
//否则提供给外部纹理绘制
mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
mCommonPaint.setAntiAlias(true);
mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
mCommonPaint.setFilterBitmap(true);
mCommonPaint.setDither(true);
mBitmaps = new Bitmap[3];
mBitmaps[0] = decodeBitmap(R.mipmap.mm_013);
mBitmaps[1] = decodeBitmap(R.mipmap.mm_014);
mBitmaps[2] = decodeBitmap(R.mipmap.mm_015);
}
private Bitmap decodeBitmap(int resId) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
return BitmapFactory.decodeResource(getResources(), resId, options);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
widthSize = mDM.widthPixels / 2;
}
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (heightMode != MeasureSpec.EXACTLY) {
heightSize = widthSize / 2;
}
setMeasuredDimension(widthSize, heightSize);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (bitmapCanvas != null && bitmapCanvas.bitmap != null && !bitmapCanvas.bitmap.isRecycled()) {
bitmapCanvas.bitmap.recycle();
}
bitmapCanvas = null;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
if (width < 1 || height < 1) {
return;
}
if (bitmapCanvas == null || bitmapCanvas.bitmap == null || bitmapCanvas.bitmap.isRecycled()) {
bitmapCanvas = new BitmapCanvas(Bitmap.createBitmap(mBitmaps[index].getWidth(), mBitmaps[index].getHeight(), Bitmap.Config.ARGB_8888));
}
int nextIndex = (index + 1) % mBitmaps.length;
canvas.drawBitmap(mBitmaps[nextIndex],0,0,mCommonPaint);
int col = (int) Math.ceil(mBitmaps[index].getWidth() / blockWidth);
int row = (int) Math.ceil(mBitmaps[index].getHeight() / blockWidth);
mCommonPaint.setStyle(Paint.Style.FILL);
// path.reset();
// for (int x = 0; x < row; x++) {
// for (int y = 0; y < col; y++) {
// gridRectF.set(x * blockWidth, y * blockWidth, x * blockWidth + blockWidth, y * blockWidth + blockWidth);
// canvas.drawRect(gridRectF, mCommonPaint);
// path.addRect(gridRectF, Path.Direction.CCW);
// }
// }
diagonalEffect(col,row,xPosition,path);
canvas.drawBitmap(bitmapCanvas.bitmap, 0, 0, mCommonPaint);
if (isTicking && xPosition >= 0 && xPosition < col * 2) {
clockTick();
} else if(isTicking){
xPosition = -1;
index = nextIndex;
isTicking = false;
}
}
private void diagonalEffect(int col, int row, int xPosition,Path path) {
int x = xPosition;
int y = 0;
while (x >= 0 && y <= row) {
if (x < col && y < row) {
dstRect.set((int) (x * blockWidth), (int) (y * blockWidth), (int) (x * blockWidth + blockWidth), (int) (y * blockWidth + blockWidth));
// bitmapCanvas.drawBitmap(mBitmaps[index], dstRect, dstRect, mCommonPaint);
path.addRect(dstRect, Path.Direction.CCW); //加入网格分片
}
x--;
y++;
}
int save = bitmapCanvas.save();
mCommonPaint.setShader(new BitmapShader(mBitmaps[index], Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
bitmapCanvas.drawPath(path,mCommonPaint);
bitmapCanvas.restoreToCount(save);
}
public void tick() {
isTicking = true;
xPosition = -1;
path.reset();
clockTick();
}
private void clockTick() {
xPosition += 1;
postInvalidateDelayed(16);
}
public float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
}
public float sp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, dp, mDM);
}
static class BitmapCanvas extends Canvas {
Bitmap bitmap;
public BitmapCanvas(Bitmap bitmap) {
super(bitmap);
//继承在Canvas的绘制是软绘制,因此理论上可以绘制出阴影
this.bitmap = bitmap;
}
public Bitmap getBitmap() {
return bitmap;
}
}
}