先上效果图
一、需求分析
单点拖动图片对图片进行平移操作。双手缩放图片大小和旋转图片到一定的角度。图片缩放的时候 不能大于最大的缩放因子和小于最小的缩放因子。大于最大缩放因子或者小于最小缩放因子需要对图像进行回弹。图片旋转的角度只能为90度的倍数,不满足90度要进行回弹。图片回弹要一个渐变的效果。
二、 思路分析
大体思路:首先,Android中提供了Matrix类可以对图像进行处理。其次,要显示一张图片最容易想到的就是ImageView。回弹要求渐变的过程,可以通过属性动画进行设置。所以大体的思路是:继承ImageView,重写onTouchEvent()方法,判断事件类型,在对应的事件使用Matrix对图像进行变换。
Matrix是一个已经封装好的矩阵,最重要的作用就是对坐标点进行变换。
举个栗子:
1.某个点(x0,y0,1)通过单位矩阵E映射得到的点还是(x0,y0,1)。
2.调用单位矩阵E的postTranslate(dx,dy)。单位矩阵转换成矩阵T
3.点(x0,y0,1)通过矩阵T映射得到的点就会做如下的变换
可以看到点(x0,y0,1)经过T矩阵在x轴方向上平移了dx,在y轴方向上平移了dy。
通过以上的变换可以得到具体的思路:我们维护一个图像对应的矩阵mCurrentMatrix,该矩阵主要是对ImageView中的图像的各个点进行映射。ImageView在容器位置摆放完成之后,置mCurrentMatrix矩阵为单位矩阵。当onTouchEvent()方法中触发单点触控并且手指进行平移的时候,调用矩阵mCurrentMatrix的postTranslate(dx,dy),对mCurrentMatrix进行变换。当手指抬起,利用变换结束后的矩阵对图像的各个点进行映射,从而得到平移变换后的图像。同理可得,在两只手指进行缩放旋转的时候,我们对矩阵mCurrentMatrix进行各种变换,当缩放旋转的事件结束再利用变换完的矩阵去映射图像的各个点,从而得到缩放、旋转后的图像。
三、Matrix
安卓自定义View进阶 - Matrix原理
安卓自定义View进阶 - Matrix详解
对于Matrix的使用以上两篇博客中有详细的描述Matrix的使用方法以及原理。例如矩阵的第一行第三列的浮点数以及第二行第三列的浮点数是用来映射点的平移。
注意调用Matrix的setXxx方法,会将Matrix之前的变换都清除。
四、主要方法
首先理清事件的逻辑:
单只手指按下的时候是可以进行平移操作的,多点触控的时候不能进行平移操作。所以维护一个boolean变量mCanTranslate,在ACTION_DOWN的时候置位true,ACTION_POINTER_DOWN置为false。而平移操作的位移量需要手指滑动时与上一次点的坐标进行相比较,所以维护一个mLastSinglePoint记录手指按下以及手指滑动时上一次点的坐标。
当两只手指按下的时候是可以进行旋转和缩放操作的,维护两个boolean变量:mCanRotate、mCanScale在ACTION_POINTER_DOWN置为true,ACTION_POINTER_UP置为false。
图像的缩放比例因子采取的是前后两个手指间距离的比值。所以维护mLastDist,在ACTION_POINTER_DOWN的时候算出两只手指间的欧氏距离。在ACTION_MOVE的时候,算出新的手指间的距离,并和上一次手指间距离的运算求出比例,更新mLastDist为本次手指间的距离。
图像的旋转角度采取的是两只手构成的向量转过的角度。维护一个mLastVector。在ACTION_POINTER_DOWN的时候记录两只手指构成的向量。在ACTION_MOVE的时候,记录新的两只手指构成的向量,求出转过的角度,最后更新mLastVector为本次两手指构成的向量。
初始化图像大小和位置
缩放图像大小和控件大小自适应,平移图像中心和控件中心重合
private void init() {
mCurrentMatrix.reset();
upDateBoundRectF();
float scaleFactor = Math.min(getWidth() / mBoundRectF.width(), getHeight() / mBoundRectF.height());
mInitialScaleFactor = scaleFactor;
mTotalScaleFactor *= scaleFactor;
//以图片的中心点进行缩放,缩放图片大小和控件大小适应
mCurrentMatrix.postScale(scaleFactor, scaleFactor, mBoundRectF.centerX(), mBoundRectF.centerY());
//将图片中心点平移到和控件中心点重合
mCurrentMatrix.postTranslate(getPivotX() - mBoundRectF.centerX(), getPivotY() - mBoundRectF.centerY());
//对图片进行变换,并更新图片的边界矩形
transform();
}
onTouchEvent()函数
/**
* 当单点触控的时候可以进行平移操作
* 当多点触控的时候:可以进行图片的缩放、旋转
* ACTION_DOWN:标记能平移、不能旋转、不能缩放
* ACTION_POINTER_DOWN:如果手指个数为2,标记不能平移、能旋转、能缩放
* 记录平移开始时两手指的中点、两只手指形成的向量、两只手指间的距离
* ACTION_MOVE:进行平移、旋转、缩放的操作。
* ACTION_POINTER_UP:有一只手指抬起的时候,设置图片不能旋转、不能缩放,可以平移
*
* @param event 点击事件
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
//单点触控,设置图片可以平移、不能旋转和缩放
case MotionEvent.ACTION_DOWN:
mCanTranslate = true;
mCanRotate = false;
mCanScale = false;
//记录单点触控的上一个单点的坐标
mLastSinglePoint.set(event.getX(), event.getY());
break;
case MotionEvent.ACTION_POINTER_DOWN:
animator.cancel();
//多点触控,设置图片不能平移
mCanTranslate = false;
//当手指个数为两个的时候,设置图片能够旋转和缩放
if (event.getPointerCount() == 2) {
mCanRotate = true;
mCanScale = true;
//记录两手指的中点
PointF pointF = midPoint(event);
//记录开始滑动前两手指中点的坐标
mLastMidPoint.set(pointF.x, pointF.y);
//记录开始滑动前两个手指之间的距离
mLastDist = distance(event);
//设置向量,以便于计算角度
mLastVector.set(event.getX(0) - event.getX(1), event.getY(0) - event.getY(1));
}
break;
case MotionEvent.ACTION_MOVE:
//判断能否平移操作
if (mCanTranslate) {
float dx = event.getX() - mLastSinglePoint.x;
float dy = event.getY() - mLastSinglePoint.y;
//平移操作
translation(dx, dy);
//重置上一个单点的坐标
mLastSinglePoint.set(event.getX(), event.getY());
}
//判断能否缩放操作
if (mCanScale) {
float scaleFactor = distance(event) / mLastDist;
scale(scaleFactor);
//重置mLastDist,让下次缩放在此基础上进行
mLastDist = distance(event);
}
//判断能否旋转操作
if (mCanRotate) {
//当前两只手指构成的向量
PointF vector = new PointF(event.getX(0) - event.getX(1), event.getY(0) - event.getY(1));
//计算本次向量和上一次向量之间的夹角
float degree = calculateDeltaDegree(mLastVector, vector);
rotation(degree);
//更新mLastVector,以便下次旋转计算旋转过的角度
mLastVector.set(vector.x, vector.y);
}
//图像变换
transform();
break;
case MotionEvent.ACTION_POINTER_UP:
//当两只手指有一只抬起的时候,设置图片不能缩放和选择,能够进行平移
if (event.getPointerCount() == 2) {
mCanScale = false;
mCanRotate = false;
mCanTranslate = true;
//重置旋转和缩放使用到的中点坐标
mLastMidPoint.set(0f, 0f);
//重置两只手指的距离
mLastDist = 0f;
//重置两只手指形成的向量
mLastVector.set(0f, 0f);
}
//获得开始动画之前的矩阵
mCurrentMatrix.getValues(mBeginMatrixValues);
//缩放回弹
backScale();
upDateBoundRectF();
//旋转回弹
backRotation();
upDateBoundRectF();
//获得动画结束之后的矩阵
mCurrentMatrix.getValues(mEndMatrixValues);
animator.start();
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
backTranslation();
upDateBoundRectF();
mLastSinglePoint.set(0f, 0f);
mCanTranslate = false;
mCanScale = false;
mCanRotate = false;
break;
}
return true;
}
平移操作
将图像对应的矩阵进行变换。
protected void translation(float dx, float dy) {
//检查图片边界的平移是否超过控件的边界
if (mBoundRectF.left + dx > getWidth() - 20 || mBoundRectF.right + dx < 20
|| mBoundRectF.top + dy > getHeight() - 20 || mBoundRectF.bottom + dy < 20) {
return;
}
mCurrentMatrix.postTranslate(dx, dy);
}
缩放操作
mBoundRectF为记录图像边界的矩形。缩放的时候选取图像的中心进行缩放。
private void scale(float scaleFactor) {
//累乘得到总的的缩放因子
mTotalScaleFactor *= scaleFactor;
mCurrentMatrix.postScale(scaleFactor, scaleFactor, mBoundRectF.centerX(), mBoundRectF.centerY());
}
旋转操作
旋转的时候旋转的旋转中心也是图像的中心
private void rotation(float degree) {
//旋转变换
mCurrentMatrix.postRotate(degree, mBoundRectF.centerX(), mBoundRectF.centerY());
}
图像中各个点的映射
调用ImageView的setImageMatrix(Matrix matrix)会让ImageView根据设置的matrix去重新绘制图像。
private void transform() {
setImageMatrix(mCurrentMatrix);
upDateBoundRectF();
}
更新图像的矩形边界
获得图像的矩形,并根据矩阵映射矩形各个点的坐标。
private void upDateBoundRectF() {
if (getDrawable() != null) {
mBoundRectF.set(getDrawable().getBounds());
mCurrentMatrix.mapRect(mBoundRectF);
}
}
缩放回弹
private void backScale() {
float scaleFactor = 1.0f;
//如果总的缩放比例因子比初始化的缩放因子还小,进行回弹
if (mTotalScaleFactor / mInitialScaleFactor < mMinScaleFactor) {
//1除以总的缩放因子再乘初始化的缩放因子,求得回弹的缩放因子
scaleFactor = mInitialScaleFactor / mTotalScaleFactor * mMinScaleFactor;
//更新总的缩放因子,以便下次在此缩放比例的基础上进行缩放
mTotalScaleFactor = mInitialScaleFactor * mMinScaleFactor;
}
//如果总的缩放比例因子大于最大值,让图片放大到最大倍数
else if (mTotalScaleFactor / mInitialScaleFactor > mMaxScaleFactor) {
//求放大到最大倍数,需要的比例因子
scaleFactor = mInitialScaleFactor / mTotalScaleFactor * mMaxScaleFactor;
//更新总的缩放因子,以便下次在此缩放比例的基础上进行缩放
mTotalScaleFactor = mInitialScaleFactor * mMaxScaleFactor;
}
mCurrentMatrix.postScale(scaleFactor, scaleFactor, mBoundRectF.centerX(), mBoundRectF.centerY());
}
旋转回弹
private void backRotation() {
//x轴方向的单位向量,在极坐标中,角度为0
float[] x_vector = new float[]{1.0f, 0.0f};
//映射向量
mCurrentMatrix.mapVectors(x_vector);
//计算x轴方向的单位向量转过的角度
float totalDegree = (float) Math.toDegrees((float) Math.atan2(x_vector[1], x_vector[0]));
float degree = totalDegree;
degree = Math.abs(degree);
//如果旋转角度的绝对值在45-135度之间,让其旋转角度为90度
if (degree > 45 && degree <= 135) {
degree = 90;
} //如果旋转角度的绝对值在135-225之间,让其旋转角度为180度
else if (degree > 135 && degree <= 225) {
degree = 180;
} //如果旋转角度的绝对值在225-315之间,让其旋转角度为270度
else if (degree > 225 && degree <= 315) {
degree = 270;
}//如果旋转角度的绝对值在315-360之间,让其旋转角度为0度
else {
degree = 0;
}
degree = totalDegree < 0 ? -degree : degree;
//degree-totalDegree计算达到90的倍数角,所需的差值
mCurrentMatrix.postRotate(degree - totalDegree, mBoundRectF.centerX(), mBoundRectF.centerY());
}
一些计算方法
/**
* 计算两个手指头之间的中心点的位置
* x = (x1+x2)/2;
* y = (y1+y2)/2;
*
* @param event 触摸事件
* @return 返回中心点的坐标
*/
private PointF midPoint(MotionEvent event) {
float x = (event.getX(0) + event.getX(1)) / 2;
float y = (event.getY(0) + event.getY(1)) / 2;
return new PointF(x, y);
}
/**
* 计算两个手指间的距离
*
* @param event 触摸事件
* @return 放回两个手指之间的距离
*/
private float distance(MotionEvent event) {
float x = event.getX(0) - event.getX(1);
float y = event.getY(0) - event.getY(1);
return (float) Math.sqrt(x * x + y * y);//两点间距离公式
}
/**
* 计算两个向量之间的夹角
*
* @param lastVector 上一次两只手指形成的向量
* @param vector 本次两只手指形成的向量
* @return 返回手指旋转过的角度
*/
private float calculateDeltaDegree(PointF lastVector, PointF vector) {
float lastDegree = (float) Math.atan2(lastVector.y, lastVector.x);
float degree = (float) Math.atan2(vector.y, vector.x);
float deltaDegree = degree - lastDegree;
return (float) Math.toDegrees(deltaDegree);
}
五、动画
要求图像的变换是一个渐变的过程,很容易想到的就是属性动画。因为属性动画本身就是对值进行不断set的过程。而我们维护的矩阵也是一个值,所以很自然可以想到,如果得到回弹之前的矩阵的值以及回弹之后矩阵的值,就可以根据动画监听器中动画当前的系数值去改变矩阵的值。
/**
* 动画监听器
*/
private ValueAnimator.AnimatorUpdateListener animatorUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//获得动画过程当前的系数值
float animatedValue = (float) animation.getAnimatedValue();
for (int i = 0; i < 9; i++) {
//使用渐变过程中的系数值去变换矩阵
mTransformMatrixValues[i] = mBeginMatrixValues[i] + (mEndMatrixValues[i] - mBeginMatrixValues[i]) * animatedValue;
}
//动态更新矩阵中的值
mCurrentMatrix.setValues(mTransformMatrixValues);
//图像变化
transform();
}
};
/**
* 动画监听器
*/
private Animator.AnimatorListener animatorListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mCurrentMatrix.setValues(mEndMatrixValues);
transform();
}
};
对animator对象设置完监听器之后,就可以在手指抬起的时候调用属性动画的start()方法开启动画。
六、总结
自定义可平移、缩放、旋转的控件主要点有两个方面:一是onTouchEvent()中判断平移、旋转、缩放的触发条件,平移位移量、缩放比例因子、旋转角度的计算。二是Matrix矩阵的应用。