自定义查看大图控件
最近项目需要做一个换发型功能,正好去年写过一个查看大图的功能,稍微缝缝补补,这不是又能混三年?嘿嘿。但是公司不准用即时通讯工具,更不准用U盘,所以干脆写篇文章,自己也能稍微回顾一下。
查看此篇文章你需要了解
- android基础知识
- 基本自定义view知识
- 基本的android手势知识
- Matrix转换
如果你还不了解这些的话,可以自行前往搜索引擎搜索文章学习,这里提供一个关于Matrix比较不错的文章 https://www.jianshu.com/p/11e062284491
当我观看其他文章时,看代码片段经常看到头疼,一直要到结尾才能看到代码,或者跳转git才能拿到代码,于是我决定这片文章吧代码放在最前面,有基础的直接看代码就完事了,碰到不懂的,再回头看后面的解析。
首先是java代码
package com.wsn.wsn_firecontro.base_view;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.WindowManager;
import android.widget.ImageView;
import com.wsn.wsn_firecontro.R;
/**
* Created by Think on 2018/5/6.
*/
public class MyMatrixImg extends ImageView {
private static final String TAG = "CustomImageView";
//控件是否能平移
private boolean mCanTranslate = false;
//控件是否能旋转
private boolean mCanRotate = false;
//控件是否能缩放
private boolean mCanScale = false;
//控件是否能够平移回弹
private boolean mCanBackTranslate;
//控件是否能够旋转回弹
private boolean mCanBackRotate;
//控件是否能够缩放回弹
private boolean mCanBackSale;
//默认最大缩放比例因子
public static final float DEFAULT_MAX_SCALE_FACTOR = 3.0f;
//默认最小缩放比例因子
public static final float DEFAULT_MIN_SCALE_FACTOR = 0.8f;
//当前是否layout完成
private boolean isLayout;
//最大缩放比例因子
private float mMaxScaleFactor = DEFAULT_MAX_SCALE_FACTOR;
//最小缩放比例因子
private float mMinScaleFactor = DEFAULT_MIN_SCALE_FACTOR;
//用于平移、缩放、旋转变换图片的矩阵
private Matrix mCurrentMatrix = new Matrix();
//上一次单点触控的坐标
private PointF mLastSinglePoint = new PointF();
//记录上一次两只手指中点的位置
private PointF mLastMidPoint = new PointF();
//记录上一次两只手指之间的距离
private float mLastDist;
//图片的边界矩形
private RectF mBoundRectF = new RectF();
//记录上一次两只手指构成的一个向量
private PointF mLastVector = new PointF();
//记录onLayout之后的初始化缩放因子
private float mInitialScaleFactor = 1.0f;
//记录图片总的缩放因子
private float mTotalScaleFactor = 1.0f;
//动画开始时的矩阵值
private float[] mBeginMatrixValues = new float[9];
//动画结束时的矩阵值
private float[] mEndMatrixValues = new float[9];
//动画过程中的矩阵值
private float[] mTransformMatrixValues = new float[9];
//属性动画
private ValueAnimator mAnimator = ValueAnimator.ofFloat(0f, 1f);
//属性动画默认时间
public static final int DEFAULT_ANIMATOR_TIME = 300;
public MyMatrixImg(Context context) {
this(context, null);
}
public MyMatrixImg(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyMatrixImg(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
/**
* 初始化自定义属性以及动画
*
* @param context
* @param attrs
*/
private void init(Context context, AttributeSet attrs) {
setScaleType(ScaleType.MATRIX);
//获得自定义属性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyMatrixImg);
//最大缩放比例因子
float maxScaleFactor = typedArray.getFloat(R.styleable.MyMatrixImg_max_scale_factor, DEFAULT_MAX_SCALE_FACTOR);
//最小缩放比例因子
float minScaleFactor = typedArray.getFloat(R.styleable.MyMatrixImg_min_scale_factor, DEFAULT_MIN_SCALE_FACTOR);
//是否能够平移回弹
boolean canBackTranslate = typedArray.getBoolean(R.styleable.MyMatrixImg_can_back_translate, true);
//是否能够旋转回弹
boolean canBackRotate = typedArray.getBoolean(R.styleable.MyMatrixImg_can_back_rotate, true);
//是否能够缩放回弹
boolean canBackScale = typedArray.getBoolean(R.styleable.MyMatrixImg_can_back_scale, true);
//动画持续的事件
int animatorTime = typedArray.getInt(R.styleable.MyMatrixImg_animator_time, DEFAULT_ANIMATOR_TIME);
typedArray.recycle();
//设置自定义属性
setMaxScaleFactor(maxScaleFactor);
setMinScaleFactor(minScaleFactor);
setCanBackTranslate(canBackTranslate);
setCanBackRotate(canBackRotate);
setCanBackSale(canBackScale);
//设置监听器,监听0-1的变化
mAnimator.addUpdateListener(animatorUpdateListener);
//设置动画结束,必须让矩阵等于最后的矩阵
mAnimator.addListener(animatorListener);
setAnimatorTime(animatorTime);
}
/**
* 设置最大缩放比例因子
*
* @param mMaxScaleFactor 最大缩放比例因子
*/
public void setMaxScaleFactor(float mMaxScaleFactor) {
this.mMaxScaleFactor = mMaxScaleFactor;
}
/**
* 设置最小缩放比例因子
*
* @param mMinScaleFactor 最小缩放比例因子
*/
public void setMinScaleFactor(float mMinScaleFactor) {
this.mMinScaleFactor = mMinScaleFactor;
}
/**
* 设置是否能够平移回弹
*
* @param canBackTranslate
*/
public void setCanBackTranslate(boolean canBackTranslate) {
this.mCanBackTranslate = canBackTranslate;
}
/**
* 设置是否能够旋转回弹
*
* @param canBackRotate
*/
public void setCanBackRotate(boolean canBackRotate) {
this.mCanBackRotate = canBackRotate;
}
/**
* 是指是否能够缩放回弹
*
* @param canBackSale
*/
public void setCanBackSale(boolean canBackSale) {
this.mCanBackSale = canBackSale;
}
/**
* 设置动画时间
*
* @param animatorTime 动画的时间
*/
public void setAnimatorTime(long animatorTime) {
mAnimator.setDuration(animatorTime);
}
/**
* 初始化放置图片
* 将图片缩放和控件大小一致并移动图片中心和控件的中心重合
*
* @param changed
* @param left
* @param top
* @param right
* @param bottom
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
isLayout = true;
initImagePositionAndSize();
}
/**
* 初始化图片的大小与位置
*/
private void initImagePositionAndSize() {
if(getDrawable() == null) {
return;
}
mCurrentMatrix.reset();
BitmapDrawable bd = (BitmapDrawable) getDrawable();
Bitmap bitmap = bd.getBitmap();
upDateBoundRectF();
float scaleFactor = 1;
if(bitmap.getWidth() > bitmap.getHeight()) {
rotation(90);
scaleFactor = Math.min(getWidth() / mBoundRectF.width(), getHeight() / mBoundRectF.height());
}else {
scaleFactor = Math.min(getWidth() / mBoundRectF.height(), getHeight() / mBoundRectF.width());
}
mInitialScaleFactor = scaleFactor;
mTotalScaleFactor *= scaleFactor;
//以图片的中心点进行缩放,缩放图片大小和控件大小适应
mCurrentMatrix.postScale(scaleFactor, scaleFactor, mBoundRectF.centerX(), mBoundRectF.centerY());
//将图片中心点平移到和控件中心点重合
mCurrentMatrix.postTranslate(getPivotX() - mBoundRectF.centerX(), getPivotY() - mBoundRectF.centerY());
//对图片进行变换,并更新图片的边界矩形
transform();
}
@Override
public void setImageDrawable(@Nullable Drawable drawable) {
super.setImageDrawable(drawable);
if(isLayout) {
initImagePositionAndSize();
}
}
@Override
public void setImageBitmap(Bitmap bm) {
super.setImageBitmap(bm);
if(isLayout) {
initImagePositionAndSize();
}
}
/**
* 当单点触控的时候可以进行平移操作
* 当多点触控的时候:可以进行图片的缩放、旋转
* ACTION_DOWN:标记能平移、不能旋转、不能缩放
* ACTION_POINTER_DOWN:如果手指个数为2,标记不能平移、能旋转、能缩放
* 记录平移开始时两手指的中点、两只手指形成的向量、两只手指间的距离
* ACTION_MOVE:进行平移、旋转、缩放的操作。
* ACTION_POINTER_UP:有一只手指抬起的时候,设置图片不能旋转、不能缩放,可以平移
*
* @param event 点击事件
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
if(mAnimator.isStarted()) {
return true;
}
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:
mAnimator.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 = false;
//重置旋转和缩放使用到的中点坐标
mLastMidPoint.set(0f, 0f);
//重置两只手指的距离
mLastDist = 0f;
//重置两只手指形成的向量
mLastVector.set(0f, 0f);
}
//获得开始动画之前的矩阵
mCurrentMatrix.getValues(mBeginMatrixValues);
if (mCanBackSale) {
//缩放回弹
backScale();
upDateBoundRectF();
}
if (mCanBackRotate) {
//旋转回弹
backRotation();
upDateBoundRectF();
}
//获得动画结束之后的矩阵
mCurrentMatrix.getValues(mEndMatrixValues);
mAnimator.start();
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mLastSinglePoint.set(0f, 0f);
mCanTranslate = false;
mCanScale = false;
mCanRotate = false;
break;
}
return true;
}
/**
* 更新矩形边界
*/
private void upDateBoundRectF() {
if (getDrawable() != null) {
mBoundRectF.set(getDrawable().getBounds());
mCurrentMatrix.mapRect(mBoundRectF);
}
}
/**
* 计算两个手指头之间的中心点的位置
* 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 dx x方向的位移
* @param dy y方向的位移
*/
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);
}
/**
* 图像缩放操作
*
* @param scaleFactor 缩放比例因子
*/
protected void scale(float scaleFactor) {
//累乘得到总的的缩放因子
mTotalScaleFactor *= scaleFactor;
mCurrentMatrix.postScale(scaleFactor, scaleFactor, mBoundRectF.centerX(), mBoundRectF.centerY());
}
/**
* 旋转操作
*
* @param degree 旋转角度
*/
protected void rotation(float degree) {
//旋转变换
mCurrentMatrix.postRotate(degree, mBoundRectF.centerX(), mBoundRectF.centerY());
}
/**
* 代码调用旋转时,使用动画旋转
* @param degree
*/
public void AnimatorRotate(float degree) {
if(mAnimator.isStarted()) {
return;
}
//获取开始状态
mCurrentMatrix.getValues(mBeginMatrixValues);
//调用矩阵旋转
mCurrentMatrix.postRotate(degree, mBoundRectF.centerX(), mBoundRectF.centerY());
//拿到最终要显示矩阵
mCurrentMatrix.getValues(mEndMatrixValues);
//开始动画
mAnimator.start();
}
/**
* 计算两个向量之间的夹角
*
* @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);
}
/**
* 旋转回弹
*/
protected 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());
}
/**
* 缩放回弹
*/
protected 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());
}
/**
* 平移回弹
* 平移之后不能出现有白边的情况
*/
protected void backTranslation() {
float dx = 0;
float dy = 0;
//判断图片的宽度是否大于控件的宽度,若是要进行边界的判断
if (mBoundRectF.width() >= getWidth()) {
//左边界在控件范围内,或者图片左边界超出控件范围
if ((mBoundRectF.left > getLeft() && mBoundRectF.left <= getRight()) || mBoundRectF.left > getRight()) {
dx = getLeft() - mBoundRectF.left;
} //图片右边界在控件范围内,或者图片右边界超出控件范围
else if ((mBoundRectF.right >= getLeft() && mBoundRectF.right < getRight()) || mBoundRectF.right < getLeft()) {
dx = getRight() - mBoundRectF.right;
}
} //如果图片宽度小于控件宽度,移动图片中心x坐标和控件中心x坐标重合
else {
dx = getPivotX() - mBoundRectF.centerX();
}
//判断图片的高度是否大于控件的高度,若是要进行边界的判断
if (mBoundRectF.height() >= getHeight()) {
//图片上边界在控件范围内,或者图片上边界超出控件范围
if ((mBoundRectF.top > getTop() && mBoundRectF.top <= getBottom()) || mBoundRectF.top > getBottom()) {
dy = getTop() - mBoundRectF.top;
} //图片下边界在控件范围内,或者图片下边界超出控件范围
else if ((mBoundRectF.bottom < getBottom() && mBoundRectF.bottom >= getTop()) || mBoundRectF.bottom < getTop()) {
dy = getBottom() - mBoundRectF.bottom;
}
} //如果图片高度小于控件高度,移动图片中心y坐标和控件中心y坐标重合
else {
dy = getPivotY() - mBoundRectF.centerY();
}
mCurrentMatrix.postTranslate(dx, dy);
}
/**
* 图像变换并更新边界矩阵
*/
protected void transform() {
setImageMatrix(mCurrentMatrix);
upDateBoundRectF();
}
/**
* 对图像进行镜像变换
* 长按之后弹出PopupWindow
* 在PopupWindow中点击镜像调用该方法
*/
protected void clickMirror() {
mCurrentMatrix.postScale(-1, 1, mBoundRectF.centerX(), mBoundRectF.centerY());
transform();
}
/**
* 对图像进行90度旋转变换
* 长按之后弹出PopupWindow
* 在PopupWindow中点击旋转调用该方法
*/
protected void clickRotation() {
mCurrentMatrix.postRotate(90, mBoundRectF.centerX(), mBoundRectF.centerY());
transform();
}
/**
* 动画监听器
*/
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();
}
};
}
然后是自定义属性xml编写,在values下创建atts.xml文件然后吧这个写进去
<declare-styleable name="MyMatrixImg">
<!--最小缩放因子-->
<attr name="min_scale_factor" format="float" />
<!--最大缩放因子-->
<attr name="max_scale_factor" format="float" />
<!--开关平移回弹-->
<attr name="can_back_translate" format="boolean" />
<!--开关旋转回弹-->
<attr name="can_back_rotate" format="boolean" />
<!--开关缩放回弹-->
<attr name="can_back_scale" format="boolean" />
<!--回弹动画时间-->
<attr name="animator_time" format="integer" />
</declare-styleable>
好了,下面要开始具体分析了。 如果看上面代码比较吃力的童鞋可以接着往下看,如果能直接看明白的,就没必要往下看了。
首先我们继承于imageview,因为imageview为我们提供了一些已经封装好的图片显示操作的函数,比如setImageMatrix等等,这样可以让我们省不少事。
那么我们从成员变量开始讲起,设计一个功能之前,首先要搞清楚这个功能需要记录哪些参数,需要定义哪些基础常量等等,比如自定义view中最常见的偏移量,以及画笔啊,点位啥的。
根据我们的目前的需求,我定义了如下成员来保存状态。
//控件是否能平移
private boolean mCanTranslate = false;
//控件是否能旋转
private boolean mCanRotate = false;
//控件是否能缩放
private boolean mCanScale = false;
//控件是否能够平移回弹
private boolean mCanBackTranslate;
//控件是否能够旋转回弹
private boolean mCanBackRotate;
//控件是否能够缩放回弹
private boolean mCanBackSale;
//默认最大缩放比例因子
public static final float DEFAULT_MAX_SCALE_FACTOR = 3.0f;
//默认最小缩放比例因子
public static final float DEFAULT_MIN_SCALE_FACTOR = 0.8f;
//当前是否layout完成
private boolean isLayout;
//最大缩放比例因子
private float mMaxScaleFactor = DEFAULT_MAX_SCALE_FACTOR;
//最小缩放比例因子
private float mMinScaleFactor = DEFAULT_MIN_SCALE_FACTOR;
//用于平移、缩放、旋转变换图片的矩阵
private Matrix mCurrentMatrix = new Matrix();
//上一次单点触控的坐标
private PointF mLastSinglePoint = new PointF();
//记录上一次两只手指中点的位置
private PointF mLastMidPoint = new PointF();
//记录上一次两只手指之间的距离
private float mLastDist;
//图片的边界矩形
private RectF mBoundRectF = new RectF();
//记录上一次两只手指构成的一个向量
private PointF mLastVector = new PointF();
//记录onLayout之后的初始化缩放因子
private float mInitialScaleFactor = 1.0f;
//记录图片总的缩放因子
private float mTotalScaleFactor = 1.0f;
//动画开始时的矩阵值
private float[] mBeginMatrixValues = new float[9];
//动画结束时的矩阵值
private float[] mEndMatrixValues = new float[9];
//动画过程中的矩阵值
private float[] mTransformMatrixValues = new float[9];
//属性动画
private ValueAnimator mAnimator = ValueAnimator.ofFloat(0f, 1f);
//属性动画默认时间
public static final int DEFAULT_ANIMATOR_TIME = 300;
- 下面来看构造函数,构造函数主要就是调用了一个init函数来初始化一些配置,基础属性等等。
可以看到,init函数中主要就是吧xml中设置的参数获取到,保存起来,其次就是初始化了一下对于动画的监听,其他就没啥了。
public MyMatrixImg(Context context) {
this(context, null);
}
public MyMatrixImg(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyMatrixImg(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
/**
* 初始化自定义属性以及动画
*
* @param context
* @param attrs
*/
private void init(Context context, AttributeSet attrs) {
setScaleType(ScaleType.MATRIX);
//获得自定义属性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyMatrixImg);
//最大缩放比例因子
float maxScaleFactor = typedArray.getFloat(R.styleable.MyMatrixImg_max_scale_factor, DEFAULT_MAX_SCALE_FACTOR);
//最小缩放比例因子
float minScaleFactor = typedArray.getFloat(R.styleable.MyMatrixImg_min_scale_factor, DEFAULT_MIN_SCALE_FACTOR);
//是否能够平移回弹
boolean canBackTranslate = typedArray.getBoolean(R.styleable.MyMatrixImg_can_back_translate, true);
//是否能够旋转回弹
boolean canBackRotate = typedArray.getBoolean(R.styleable.MyMatrixImg_can_back_rotate, true);
//是否能够缩放回弹
boolean canBackScale = typedArray.getBoolean(R.styleable.MyMatrixImg_can_back_scale, true);
//动画持续的事件
int animatorTime = typedArray.getInt(R.styleable.MyMatrixImg_animator_time, DEFAULT_ANIMATOR_TIME);
typedArray.recycle();
//设置自定义属性
setMaxScaleFactor(maxScaleFactor);
setMinScaleFactor(minScaleFactor);
setCanBackTranslate(canBackTranslate);
setCanBackRotate(canBackRotate);
setCanBackSale(canBackScale);
//设置监听器,监听0-1的变化
mAnimator.addUpdateListener(animatorUpdateListener);
//设置动画结束,必须让矩阵等于最后的矩阵
mAnimator.addListener(animatorListener);
setAnimatorTime(animatorTime);
}
- 然后咋样了呢? 就这么点设置就没了吗? 当然不是,因为还有更重要的配置,我们看到下面还有一个函数initImagePositionAndSize,它被onlayout,setImageDrawable,setImageBitmap等几个函数调用到,谁先进来谁就先调用它,这个函数主要就是确认图片的位置,以及大小的。
仔细看里面的代码,发现做了如下工作
1.首先是让矩阵回到初始状态
2.获得图片bitmap对象,并获得其矩形边界位置
3.判断图片宽高,如果宽大于高设置为横屏显示,同时计算出铺满屏幕的缩放比例
4.最后操作矩阵来完成缩放和中心点校准
这里其实有几行是不需要的,不过为了防止某些特殊情况,还是留着吧。
/**
* 初始化图片的大小与位置
*/
private void initImagePositionAndSize() {
if(getDrawable() == null) {
return;
}
mCurrentMatrix.reset();
BitmapDrawable bd = (BitmapDrawable) getDrawable();
Bitmap bitmap = bd.getBitmap();
upDateBoundRectF();
float scaleFactor = 1;
if(bitmap.getWidth() > bitmap.getHeight()) {
rotation(90);
scaleFactor = Math.min(getWidth() / mBoundRectF.width(), getHeight() / mBoundRectF.height());
}else {
scaleFactor = Math.min(getWidth() / mBoundRectF.height(), getHeight() / mBoundRectF.width());
}
mInitialScaleFactor = scaleFactor;
mTotalScaleFactor *= scaleFactor;
//以图片的中心点进行缩放,缩放图片大小和控件大小适应
mCurrentMatrix.postScale(scaleFactor, scaleFactor, mBoundRectF.centerX(), mBoundRectF.centerY());
//将图片中心点平移到和控件中心点重合
mCurrentMatrix.postTranslate(getPivotX() - mBoundRectF.centerX(), getPivotY() - mBoundRectF.centerY());
//对图片进行变换,并更新图片的边界矩形
transform();
}
- 然后就来到了最关键的地方了,那就是我们的手势判断,这也是这个功能的核心。我们可以将其逻辑大致讲一下。
1.ACTION_DOWN 记录一个手指按下时,此时默认单点触控,但是我们对于单次点击并没有对应动作,所以只是改变某些状态。
2.然后ACTION_POINTER_DOWN,判断当两个手指按下时,我们保存住当前的一些状态,用于和后面手指一动的状态进行比对。
3.ACTION_MOVE 到了移动状态,这个时候我们需要根据移动状态来对矩阵进行改变(旋转,缩放等),通过计算手指间距离变化,来获取新的缩放因子。通过计算两指对应向量变化,来获取旋转角度等等。 最后再改变矩阵,setImageMatrix将改变好的矩阵set进去,改变图片的显示
4.ACTION_POINTER_UP 当手指抬起,我们的动作也就结束了,首先是判断手指抬起数量来改变一些限制操作的tag,然后就到了动画回弹了。动画回弹主要就是缩放超过最大最小倍数,旋转角度过小,或者接近下一个90度角,这个时候我们需要用动画来优雅的让界面回到我们限制的样子。回弹的思路就是,首先记录下当前的矩阵数值,然后通过计算获得到如果界面需要回到我们限制的样子需要改变成的举证的数值,最后开启动画,按百分比慢慢从用户操作后的值设置到限制值,来达到动画效果,不过刚刚捋了一下思路,发现还是有一些问题的,这里不管是有没有超过限制,都会执行一遍动画,只是不超过的时候,动画是不动的而已 - - ,很尴尬,想不起当时为啥要这么设计了,可能是图个方便,这也告诉了我们,一定要吾日三省吾身,否则就容易写这样逻辑又问题,但是能跑的代码。所以各位可以自己动手,将这里稍微改一下,判断只有超过限制值了才开启动画去变化。 如果能改好,说明你看命白逻辑到底咋回事了,如果改不好,那就再多看看。 嘿嘿
/**
* 当单点触控的时候可以进行平移操作
* 当多点触控的时候:可以进行图片的缩放、旋转
* ACTION_DOWN:标记能平移、不能旋转、不能缩放
* ACTION_POINTER_DOWN:如果手指个数为2,标记不能平移、能旋转、能缩放
* 记录平移开始时两手指的中点、两只手指形成的向量、两只手指间的距离
* ACTION_MOVE:进行平移、旋转、缩放的操作。
* ACTION_POINTER_UP:有一只手指抬起的时候,设置图片不能旋转、不能缩放,可以平移
*
* @param event 点击事件
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
if(mAnimator.isStarted()) {
return true;
}
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:
mAnimator.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 = false;
//重置旋转和缩放使用到的中点坐标
mLastMidPoint.set(0f, 0f);
//重置两只手指的距离
mLastDist = 0f;
//重置两只手指形成的向量
mLastVector.set(0f, 0f);
}
//获得开始动画之前的矩阵
mCurrentMatrix.getValues(mBeginMatrixValues);
if (mCanBackSale) {
//缩放回弹
backScale();
}
if (mCanBackRotate) {
//旋转回弹
backRotation();
}
upDateBoundRectF();
//获得动画结束之后的矩阵
mCurrentMatrix.getValues(mEndMatrixValues);
mAnimator.start();
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mLastSinglePoint.set(0f, 0f);
mCanTranslate = false;
mCanScale = false;
mCanRotate = false;
break;
}
return true;
}
下面再讲一下关键的地方,那也就是动画,这里我们其实不是设置了一个动画去改变这个view,而是通过属性动画ValueAnimator来按照百分比来改变矩阵的值,最后重新set矩阵来达到动画的效果。
- 对于属性动画的定义,就直接在成员变量那里直接生成了
private ValueAnimator mAnimator = ValueAnimator.ofFloat(0f, 1f);
然后给它附上监听
mAnimator.addUpdateListener(animatorUpdateListener);
mAnimator.addListener(animatorListener);
在AnimatorUpdateListener一步步去改变矩阵,并且重新设置到imageview中去
/**
* 动画监听器
*/
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();
}
};
- 在AnimatorListener去监听动画结束,校准矩阵,让最终结果等于最开始计算出来的限制后的最终矩阵
-
/** * 动画监听器 */ private Animator.AnimatorListener animatorListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mCurrentMatrix.setValues(mEndMatrixValues); transform(); } };
最后,小例子奉上
- xml布局
-
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true"> <com.wsn.wsn_firecontro.base_view.MyMatrixImg android:id="@+id/img_beauty" android:layout_centerInParent="true" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#eee" android:src="@drawable/timg1" app:min_scale_factor="0.6" app:max_scale_factor="2.0" /> <ImageView android:id="@+id/img_rotate_btn" android:layout_width="120px" android:layout_height="120px" android:padding="10px" android:layout_gravity="bottom|right" android:layout_marginRight="36px" android:layout_marginBottom="36px" android:src="@drawable/ic_left_rotate"/> </FrameLayout>
- java部分
-
import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.ImageView; import android.widget.Toast; import com.bumptech.glide.Glide; import com.wsn.wsn_firecontro.activity.AutoLayoutBaseActivity; import com.wsn.wsn_firecontro.application.ApiConfig; import com.wsn.wsn_firecontro.base_view.MyMatrixImg; import com.wsn.wsn_firecontro.utils.StringUilts; public class ShowBigImgaeActivity extends AutoLayoutBaseActivity implements View.OnClickListener{ public static final String IMG_KEY = "img key"; private MyMatrixImg mImgShowView; private ImageView mImgRotateBtn; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override protected void initView() { String imgUrl = getIntent().getStringExtra(IMG_KEY); if(StringUilts.isEmpty(imgUrl)) { Toast.makeText(this, "图片加载失败", Toast.LENGTH_SHORT).show(); finish(); return; } mImgShowView = findView(R.id.img_beauty); mImgRotateBtn = findView(R.id.img_rotate_btn); Glide.with(this).load(imgUrl).into(mImgShowView); mImgRotateBtn.setOnClickListener(this); } @Override protected void initEvent() { } @Override protected int getLayoutId() { return R.layout.activity_show_big_imgae; } @Override protected int getHeadColor() { return android.R.color.transparent; } @Override public void onClick(View v) { if(v.getId() == R.id.img_rotate_btn) { mImgShowView.AnimatorRotate(90); } } }
如果还有什么不懂的地方,可加群121606151去询问。