图片进行不同分割采用不同的动效就会产生不同的效果,比如广告轮播图等,将图片裁剪后进行不同的移动方式既可以增加新鲜感,同时展示的内容也更丰富
可以先看一下效果:
要实现上面的效果我们有两个问题要考虑:
一、图片裁剪问题
先来看第一个问题,图片裁剪的实现,刚做这个效果的时候自己也没想到合适的实现,经过网上一番搜索,发现了一个比较好用的工具BitmapShader,可能这个工具大家平时用的不多,了解OpenGL的应该会比较熟悉,它相当于一个图片纹理,可以和Path结合绘制部分图片区域,就像OpenGL里面的顶点坐标和纹理坐标类似,来看一下具体的实现:
private void createAreaBitmap() {
if (bitmap == null) {
return;
}
Paint bitmapPaint = new Paint(ANTI_ALIAS_FLAG);
BitmapShader bitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
bitmapPaint.setShader(bitmapShader);
bitmapShader.setLocalMatrix(shaderMatrix);
//生成左边裁剪图片
Bitmap temp = bitmapLeft == null ? Bitmap.createBitmap(getWidth() + 2, getHeight() + 2, bitmap.getConfig()) : bitmapLeft;
temp.eraseColor(Color.TRANSPARENT);
Canvas canvas = new Canvas(temp);
pathLeft.reset();
//生成裁剪图片记得外层留出一个透明像素,以防止移动时拉伸现象
for (int i = 0; i < leftAreaPoints.size(); i++) {
float tempX = leftAreaPoints.get(i).x;
float tempY = leftAreaPoints.get(i).y;
float x = tempX == 0 ? 1 : tempX * getWidth();
float y = tempY == 0 ? 1 : tempY * getHeight();
if (i == 0) {
pathLeft.moveTo(x, y);
} else {
pathLeft.lineTo(x, y);
}
}
canvas.drawPath(pathLeft, bitmapPaint);
//bitmapShader的bitmap不能回收,不然在小米机器上会崩溃
bitmapLeftShader = new BitmapShader(temp, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
bitmapLeftPaint.setShader(bitmapLeftShader);
//生成右边裁剪图片
temp = bitmapRight == null ? Bitmap.createBitmap(getWidth() + 2, getHeight() + 2, bitmap.getConfig()) : bitmapRight;
temp.eraseColor(Color.TRANSPARENT);
canvas = new Canvas(temp);
pathRight.reset();
for (int i = 0; i < rightAreaPoints.size(); i++) {
float tempX = rightAreaPoints.get(i).x;
float tempY = rightAreaPoints.get(i).y;
float x = tempX == 0 ? 1 : tempX * getWidth();
float y = tempY == 0 ? 1 : tempY * getHeight();
if (i == 0) {
pathRight.moveTo(x, y);
} else {
pathRight.lineTo(x, y);
}
}
canvas.drawPath(pathRight, bitmapPaint);
bitmapRightShader = new BitmapShader(temp, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
bitmapRightPaint.setShader(bitmapRightShader);
}
还是上面说的原理,首先需要传入图片切割区域的边界点,边界点的计算等下会介绍,有了边界点之后先构造一个BitmapShader,将原图作为参数传入,接下来就是把它当做纹理来用了,path相当于顶点坐标,纹理坐标我们设置成和顶点坐标贴合,这样就能把图片指定区域绘制到底图,两张底图就相当于两部分的切割区域了
注意的是为了显示时候切割边界区域锯齿不会太明显,最好底图大小就是最终展示View的大小
切割完了,那我们如何实现视频里面沿切割线和图片边界消失呢,这个比起上面的切割来说显得更加重要,举个例子,如果我们只是普通的移动,那上下交互的两部分是不是就会产生重叠了,所以我们需要分别给两个切割区域设置边界线,不能让它们超出这个区域
那如何实现呢?切割区域是不规则区域,我们不能用两个控件来限制边界,也不好用clipRect来裁剪绘制区域,这时候就有点头疼了。不过回想一下我们上面提到的OpenGL,既然BitmapShader相当于一张图片纹理,那纹理是不是可以移动,然后用顶点坐标作为边界呢,事实证明是可行的,我们继续看一下onDraw:
protected void onDraw(Canvas canvas) {
if (mIsShowSrc) {
super.onDraw(canvas);
} else {
if (leftAreaPoints != null) {
leftAreaMatrix.reset();
leftAreaMatrix.postTranslate(leftAreaTranslateX, leftAreaTranslateY);
bitmapLeftShader.setLocalMatrix(leftAreaMatrix);
if (bitmap != null) {
canvas.drawPath(pathLeft, bitmapLeftPaint);
}
}
if (rightAreaPoints != null) {
rightAreaMatrix.reset();
rightAreaMatrix.postTranslate(rightAreaTranslateX, rightAreaTranslateY);
bitmapRightShader.setLocalMatrix(rightAreaMatrix);
if (bitmap != null) {
canvas.drawPath(pathRight, bitmapRightPaint);
}
}
}
}
可以看到onDraw非常简单,关键点就是纹理矩阵了,我们把path作为顶点坐标,这个是始终不变的,所以初始化一次就够了,然后每次动画开始通过设置纹理对应的矩阵来实现纹理的移动达到平移效果,同时又不会超出切割区域
注意:由于BitmapShader在超出图片本身大小时会进行拉伸,拉伸的方式根据你设置的三种其中一种来,而我们想要的效果是超出图片大小显示透明而不是其他东西,这又是一个问题,不过后面发现了一种可用的拉伸模式:Shader.TileMode.CLAMP,这种模式是拉伸取图片最外层像素,这样就有办法了,只要给图片最外层加一个透明像素就可以了。
二、图片动效实现
经过上面的一番折腾终于搞定了图片裁剪问题,接下来怎么实现图片的动画效果呢?
如上图,我们定义切线左上(先判断左,再判断上)为区域A,切线右下为区域B,A、B两区域都可以选择五种动效:
我们以第五种效果来分析,要想实现动画,首先得知道动画起始点和结束点,首先想一下终止点怎么取,按照我们上面说的沿边界消失为准则,区域A的终止点就是切线过c点,区域B的终止点就是切线过b点,由终止点我们反过来计算起始点,因为是垂直切线,所以 我们也就知道了过c点并且垂直切线的直线方程Z,然后由切线和直线Z求交点就是起始点了。同理可得区域B的起始点。
知道动画起始点和终止点后我们只要利用属性动画就能完成效果了,具体看一下代码:
/**
* @param cutView 切割图片展示控件
* @param first 第一个切割点 x:[0,1],y:[0,-1]
* @param second 第二个切割点 x:[0,1],y:[0,-1]
* @param modeLeftArea 左边区域动效模式
* @param modeRightArea 右边区域动效模式
* @param animTime 动效展示时长
*/
public void initAnimByMode(final CutImageView cutView, CutImageView.ClipPoint first, CutImageView.ClipPoint second, int modeLeftArea, int modeRightArea, long animTime, final AnimCallback callback) {
leftPoints.clear();
rightPoints.clear();
final Point leftAnimStartPoint = new Point();
final Point leftAnimEndPoint = new Point();
final Point rightAnimStartPoint = new Point();
final Point rightAnimEndPoint = new Point();
float k;
if (first.x == second.x) {
first.y = Math.abs(first.y);
second.y = Math.abs(second.y);
if (first.y > second.y) {
first.swap(second);
}
leftPoints.add(new CutImageView.ClipPoint(0, 0));
leftPoints.add(first);
leftPoints.add(second);
leftPoints.add(new CutImageView.ClipPoint(0, 1f));
leftPoints.add(new CutImageView.ClipPoint(0, 0));
rightPoints.add(first);
rightPoints.add(new CutImageView.ClipPoint(1f, 0));
rightPoints.add(new CutImageView.ClipPoint(1f, 1f));
rightPoints.add(second);
rightPoints.add(first);
if (modeLeftArea == MODE_OPPOSITE && modeRightArea == MODE_OPPOSITE) {
modeLeftArea = MODE_RIGHT_TO_LEFT;
modeRightArea = MODE_LEFT_TO_RIGHT;
}
} else if (first.y == second.y) {
first.y = Math.abs(first.y);
second.y = Math.abs(second.y);
if (first.x > second.x) {
first.swap(second);
}
leftPoints.add(new CutImageView.ClipPoint(0, 0));
leftPoints.add(new CutImageView.ClipPoint(1f, 0));
leftPoints.add(second);
leftPoints.add(first);
leftPoints.add(new CutImageView.ClipPoint(0, 0));
rightPoints.add(first);
rightPoints.add(second);
rightPoints.add(new CutImageView.ClipPoint(1f, 1f));
rightPoints.add(new CutImageView.ClipPoint(0, 1f));
rightPoints.add(first);
if (modeLeftArea == MODE_OPPOSITE && modeRightArea == MODE_OPPOSITE) {
modeLeftArea = MODE_BOTTOM_TO_TOP;
modeRightArea = MODE_TOP_TO_BOTTOM;
}
} else {
k = (second.y - first.y) / (second.x - first.x);
float b = ((second.y + first.y) - k * (second.x + first.x)) / 2f;
first.y = Math.abs(first.y);
second.y = Math.abs(second.y);
//左上角开始顺时针判断
if (k > 0) {
if (first.x < second.x) {
first.swap(second);
}
//切割点坐标,x: [0,1],y:[0,-1]
leftPoints.add(new CutImageView.ClipPoint(0, 0));
rightPoints.add(new CutImageView.ClipPoint(first));
CutImageView.ClipPoint rightTop = new CutImageView.ClipPoint(1f, 0);
if (rightTop.y > k * rightTop.x + b) {
leftPoints.add(rightTop);
} else {
rightPoints.add(rightTop);
}
leftPoints.add(first);
rightPoints.add(new CutImageView.ClipPoint(1f, 1f));
leftPoints.add(second);
CutImageView.ClipPoint leftBottom = new CutImageView.ClipPoint(0f, 1f);
//判断时要用切割点坐标判断,添加时用图片坐标
if (-leftBottom.y > k * leftBottom.x + b) {
leftPoints.add(leftBottom);
} else {
rightPoints.add(leftBottom);
}
rightPoints.add(second);
//闭合区域
leftPoints.add(new CutImageView.ClipPoint(leftPoints.get(0)));
rightPoints.add(new CutImageView.ClipPoint(rightPoints.get(0)));
if (modeLeftArea == MODE_OPPOSITE && modeRightArea == MODE_OPPOSITE) {
// 动效坐标,x:[0,width],y:[0,height],
// 方案是把切割线与边界最后一个交点作为终止点,求过改点并与切割线垂直的直线与切割线的交点作为起始点
//左半区域切割线斜率为正时,最后一个交点是左上角,斜率为负时最后一个交点是左下角
float verticalK = -1 / k;
float tempX = b / (verticalK - k);
leftAnimStartPoint.x = (int) (tempX * cutView.getWidth());
leftAnimStartPoint.y = -(int) ((k * tempX + b) * cutView.getHeight());
leftAnimEndPoint.x = 0;
leftAnimEndPoint.y = 0;
//右半区域切割线斜率为正时,最后一个交点是右下角,斜率为负时最后一个交点是右上角
tempX = (b + verticalK + 1) / (verticalK - k);
rightAnimStartPoint.x = (int) (tempX * cutView.getWidth());
rightAnimStartPoint.y = -(int) ((k * tempX + b) * cutView.getHeight());
rightAnimEndPoint.x = cutView.getWidth();
rightAnimEndPoint.y = cutView.getHeight();
}
} else {
if (first.x > second.x) {
first.swap(second);
}
//切割点坐标,x: [0,1],y:[0,-1]
rightPoints.add(first);
CutImageView.ClipPoint leftTop = new CutImageView.ClipPoint(0, 0);
if (leftTop.y < k * leftTop.x + b) {
leftPoints.add(leftTop);
} else {
rightPoints.add(leftTop);
}
leftPoints.add(first);
leftPoints.add(second);
rightPoints.add(new CutImageView.ClipPoint(1f, 0f));
CutImageView.ClipPoint rightftBottom = new CutImageView.ClipPoint(1f, 1f);
if (-rightftBottom.y < k * rightftBottom.x + b) {
leftPoints.add(rightftBottom);
} else {
rightPoints.add(rightftBottom);
}
leftPoints.add(new CutImageView.ClipPoint(0, 1f));
rightPoints.add(second);
//闭合区域
leftPoints.add(new CutImageView.ClipPoint(leftPoints.get(0)));
rightPoints.add(new CutImageView.ClipPoint(rightPoints.get(0)));
if (modeLeftArea == MODE_OPPOSITE && modeRightArea == MODE_OPPOSITE) {
// 动效坐标,x:[0,width],y:[0,height],
float verticalK = -1 / k;
float tempX = (b + 1) / (verticalK - k);
leftAnimStartPoint.x = (int) (tempX * cutView.getWidth());
leftAnimStartPoint.y = -(int) ((k * tempX + b) * cutView.getHeight());
leftAnimEndPoint.x = 0;
leftAnimEndPoint.y = cutView.getHeight();
tempX = (b + verticalK) / (verticalK - k);
rightAnimStartPoint.x = (int) (tempX * cutView.getWidth());
rightAnimStartPoint.y = -(int) ((k * tempX + b) * cutView.getHeight());
rightAnimEndPoint.x = cutView.getWidth();
rightAnimEndPoint.y = 0;
}
}
}
switch (modeLeftArea) {
case MODE_RIGHT_TO_LEFT:
float maxX = Math.max(first.x, second.x);
leftAnimStartPoint.x = (int) (maxX * cutView.getWidth());
leftAnimStartPoint.y = 0;
leftAnimEndPoint.x = 0;
leftAnimEndPoint.y = 0;
break;
case MODE_LEFT_TO_RIGHT:
maxX = Math.max(first.x, second.x);
leftAnimStartPoint.x = 0;
leftAnimStartPoint.y = 0;
leftAnimEndPoint.x = (int) (maxX * cutView.getWidth());
leftAnimEndPoint.y = 0;
break;
case MODE_TOP_TO_BOTTOM:
float minY = Math.min(first.y, second.y);
leftAnimStartPoint.y = (int) (minY * cutView.getHeight());
leftAnimStartPoint.x = 0;
leftAnimEndPoint.y = cutView.getHeight();
leftAnimEndPoint.x = 0;
break;
case MODE_BOTTOM_TO_TOP:
float maxY = Math.max(first.y, second.y);
leftAnimStartPoint.y = (int) (maxY * cutView.getHeight());
leftAnimStartPoint.x = 0;
leftAnimEndPoint.y = 0;
leftAnimEndPoint.x = 0;
break;
}
switch (modeRightArea) {
case MODE_RIGHT_TO_LEFT:
float minX = Math.min(first.x, second.x);
rightAnimStartPoint.x = cutView.getWidth();
rightAnimStartPoint.y = 0;
rightAnimEndPoint.x = (int) (minX * cutView.getWidth());
rightAnimEndPoint.y = 0;
break;
case MODE_LEFT_TO_RIGHT:
minX = Math.min(first.x, second.x);
rightAnimStartPoint.x = (int) (minX * cutView.getWidth());
rightAnimStartPoint.y = 0;
rightAnimEndPoint.x = cutView.getWidth();
rightAnimEndPoint.y = 0;
break;
case MODE_TOP_TO_BOTTOM:
float minY = Math.min(first.y, second.y);
rightAnimStartPoint.y = (int) (minY * cutView.getHeight());
rightAnimStartPoint.x = 0;
rightAnimEndPoint.y = cutView.getHeight();
rightAnimEndPoint.x = 0;
break;
case MODE_BOTTOM_TO_TOP:
float maxY = Math.max(first.y, second.y);
rightAnimStartPoint.y = (int) (maxY * cutView.getHeight());
rightAnimStartPoint.x = 0;
rightAnimEndPoint.y = 0;
rightAnimEndPoint.x = 0;
break;
}
cutView.setClipArea(leftPoints, rightPoints, true);
animator.setDuration(animTime);
animator.setInterpolator(new AccelerateInterpolator());
final float leftDistanceX = leftAnimEndPoint.x - leftAnimStartPoint.x;
final float leftDistanceY = leftAnimEndPoint.y - leftAnimStartPoint.y;
final float rightDistanceX = rightAnimEndPoint.x - rightAnimStartPoint.x;
final float rightDistanceY = rightAnimEndPoint.y - rightAnimStartPoint.y;
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Float value = (Float) animation.getAnimatedValue();
float leftAreaTranX = leftDistanceX * value;
float leftAreaTranY = leftDistanceY * value;
float rightAreaTranX = rightDistanceX * value;
float rightAreaTranY = rightDistanceY * value;
cutView.setLeftAreaTranslate(leftAreaTranX, leftAreaTranY);
cutView.setRightAreaTranslate(rightAreaTranX, rightAreaTranY);
cutView.postInvalidate();
}
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(final Animator animation) {
super.onAnimationEnd(animation);
cutView.setVisibility(View.INVISIBLE);
if (callback != null) {
callback.onAnimationEnd();
}
}
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
cutView.setVisibility(View.VISIBLE);
}
});
}
上面的实现就是按照所说的计算方式代入数据得到的,这也是最复杂的一部分,主要用到了直线方程来进行各种判断,比如区域A和B的判断就是通过判断图片四个顶点与切割线的上下关系来就行处理的,详细可以看一下代码
三、总结:
图片沿切割线消失的问题以及动画起始点和终止点的计算问题一开始自己也没想清楚,等真正动手去实现的时候才一直踩坑,不过真正从构思到最后实现还是学到蛮多的,特别之前对BitmapShader接触的很少,刚开始都不知道有这个东西,用了之后发现还是不错的。
完整项目可以参考:https://github.com/yjp123456/SlideEffect