一、前言
一个有趣的现象,抖音上一度热传九宫格视频,其本质都是利用视频合成算法将视频原有视频编辑裁剪,最终展示处理。但实际还有更简单的方法,无需编辑视频的情况下,同样也可以实现九宫格展示。
二、实现原理
2.1 原理
做音视频开发也有一段时间了,在这个领域很多看似高大上的东西,实际上往往都有很多简便的方法去代替,从视频编辑到多屏投影,某些情况下并非一定要学习open gl才可以做到。
Android中提供了Path工具,其功能非常强大,很多不规则形状往往都需要Path实现,同样,本篇会利用Path进行镂空视频画布。
Path.Op 作为多个Path合成的重要操作符,其功能同样可以实现将Path闭合空间进行挖空的操作,目前主要有以下操作符。
vbnet
Path.Op.DIFFERENCE Path1调用合并函数:减去Path2后Path1区域剩下的部分
Path.Op.INTERSECT 保留Path2 和 Path1 共同的部分
Path.Op.UNION 保留Path1 和 Path 2
Path.Op.XOR 保留Path1 和 Path2 + 共同的部分
Path.Op.REVERSE_DIFFERENCE 与 Path.Op.DIFFERENCE相反,减去Path1后Path2区域剩下的部分
今天我们主要用到Path.Op.DIFFERENCE ,原因是XOR 多次存在叠加问题,下图Path节点的地方,实际上正如XOR所述进行了叠加,因此这里使用XOR效果不符合预期。
2.2 核心代码
ini
float columWidth = clipRect.width() / col; //每列的宽度
float rowHeight = clipRect.height() / row; //每行的高度
for (int i = 1; i < col; i++) {
tmpPath.reset();
float position = i * columWidth - lineWidth/2;
tmpPath.addRect(offsetLeft + position, offsetTop, offsetLeft + position + lineWidth / 2, height - offsetBottom, Path.Direction.CCW);
clipPath.op(tmpPath, Path.Op.XOR);
}
for (int i = 1; i < row; i++) {
tmpPath.reset();
float position = i * rowHeight - lineWidth/2;
tmpPath.addRect(offsetLeft , offsetTop + position, width - offsetRight, offsetTop + position + lineWidth / 2, Path.Direction.CCW);
clipPath.op(tmpPath, Path.Op.XOR);
}
尝试修改行列的效果
三、完整代码
ini
public class GridFrameLayout extends FrameLayout {
private Path clipPath;
private Path tmpPath = new Path();
private RectF clipRect;
private Paint paint;
//由于有的视频存在黑边,添加如下offset便于剔除黑边,保留纯画面区域
private int offsetTop = 0;
private int offsetBottom = 0;
private int offsetRight = 0;
private int offsetLeft = 0;
private PaintFlagsDrawFilter mPaintFlagsDrawFilter;
private int row = 3; //行数
private int col = 4; //列数
private int lineWidth = 0;
public GridFrameLayout(Context context) {
super(context);
init();
}
public GridFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public GridFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaintFlagsDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.WHITE);
paint.setStrokeWidth(10);
paint.setStyle(Paint.Style.STROKE);
lineWidth = dpToPx(5);
}
public void setRow(int row) {
this.row = row;
}
public void setColum(int col) {
this.col = col;
}
public void setOffsetTop(int offsetTop) {
this.offsetTop = offsetTop;
}
public void setOffsetBottom(int offsetBottom) {
this.offsetBottom = offsetBottom;
}
public void setOffsetRight(int offsetRight) {
this.offsetRight = offsetRight;
}
public void setOffsetLeft(int offsetLeft) {
this.offsetLeft = offsetLeft;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
clipRect = null;
}
@Override
protected void dispatchDraw(Canvas canvas) {
int height = getHeight();
int width = getWidth();
DrawFilter drawFilter = canvas.getDrawFilter();
int saveCount = canvas.save();
//保存当前状态
canvas.setDrawFilter(mPaintFlagsDrawFilter);
if (clipPath == null) {
clipPath = new Path();
}
if (clipRect == null) {
clipRect = new RectF(offsetLeft, offsetTop, width - offsetRight, height - offsetBottom);
} else {
clipRect.set(offsetLeft, offsetTop, width - offsetRight, height - offsetBottom);
}
clipPath.reset();
float radius = dpToPx(10);
float[] radii = new float[]{
radius, radius,
radius, radius,
radius, radius,
radius, radius
};
clipPath.addRoundRect(clipRect, radii, Path.Direction.CCW);
float columWidth = clipRect.width() / col;
float rowHeight = clipRect.height() / row;
for (int i = 1; i < col; i++) {
tmpPath.reset();
float position = i * columWidth - lineWidth / 2;
tmpPath.addRect(offsetLeft + position, offsetTop, offsetLeft + position + lineWidth / 2, height - offsetBottom, Path.Direction.CCW);
clipPath.op(tmpPath, Path.Op.DIFFERENCE);
}
for (int i = 1; i < row; i++) {
tmpPath.reset();
float position = i * rowHeight - lineWidth / 2;
tmpPath.addRect(offsetLeft, offsetTop + position, width - offsetRight, offsetTop + position + lineWidth / 2, Path.Direction.CCW);
clipPath.op(tmpPath, Path.Op.DIFFERENCE);
}
canvas.clipPath(clipPath);
//裁剪画布,注意,这里不仅裁剪外围,内部挖空区域也会被裁剪
//为什么在dispatchDraw中使用,因为dispatchDraw方便控制子View的绘制
super.dispatchDraw(canvas);
canvas.restoreToCount(saveCount);
//恢复到之前的区域
canvas.setDrawFilter(drawFilter);
if (hasFocus()) {
canvas.drawPath(clipPath, paint); //有焦点时画一个边框
}
}
private int dpToPx(int dps) {
return Math.round(getResources().getDisplayMetrics().density * dps);
}
public void setLineWidth(int lineWidth) {
this.lineWidth = lineWidth;
}
}
四、总结
Canvas 作为2D绘制常用的组件,其实有很高级功能,如Matrix、Camera、Shader、drawBitmapMesh等,正确的使用往往能带来事半功倍的效果,因此有必要通过不断的摸索才能发挥极致。