关于Imageview的scaleType的理解,网上有太多的实例和解释,不过,都是就介绍了
它的使用效果,最新产品有个需求,要实现图片内容的放大,移动动画,原本来要用
opengl通过纹理坐标实现,后来同事说应该自带的矩阵可以,我就看了下代码,发现真的可以。
首先,还是解释下scaletype各个参数的意思吧,字面理解,sacletype其实就是“缩放类型”,
内部其实就是通过matrix来对drawable做各种变化(解释就不打字了,直接copy别人的解释吧)
1:fitXY 把图片片不按比例扩大/缩小到View的大小显示
2:fitStart 把图片按比例扩大/缩小到View的宽度,显示在View的上部分位置
3:fitCenter 把图片按比例扩大/缩小到View的宽度,居中显示
4:fitEnd 把图片按比例扩大/缩小到View的宽度,显示在View的下部分位置
5:center 按图片的原来size居中显示,当图片宽超过View的宽,则截取图片的居中部分显示,当图片宽小于View的宽,则图片居中显示
6:centerCrop 按比例扩大/缩小图片的size居中显示,使得图片的高等于View的高,使得图片宽等于或大于View的宽
7:centerInside 将图片的内容完整居中显示,使得图片按比例缩小或原来的大小(图片比View小时)使得图片宽等于或小于View的宽(图片会完整显示)
8:matrix 用矩阵来绘制(从左上角起始的矩阵区域)
其实的scaletype都不用解释,很简单,要做这个动画,关键需要用到matrix这个scaletype。
首先来说,ImageView的getImageMatrix()方法可以拿到该图片相关的matrix,那么这个matrix是如何起作用的呢?
我们先看一段源码分析下。通过分析iamgeView的源码我们可以知道,如下图所示
这些方法都能更新imageiew的显示内容,这几个方法内部,其实都是调用了configureBounds这个方法来处理具体的业务逻辑。
private void configureBounds() {
// 如果mDrawable为空或者该View的位置还没有被固定,那么直接return
// mHaveFrame是父类layout的时候通过setFrame方法调用的,目的就是
// 用来设置view的显示位置
if (mDrawable == null || !mHaveFrame) {
return;
}
// 拿到图片的宽和高
int dwidth = mDrawableWidth;
int dheight = mDrawableHeight;
// 拿到当前view的宽和高
int vwidth = getWidth() - mPaddingLeft - mPaddingRight;
int vheight = getHeight() - mPaddingTop - mPaddingBottom;
// fits简单理解就是代码当前图片的宽高和控件的宽高是否完全匹配 如果匹配,就不需要矩阵来进行缩放了
boolean fits = (dwidth < 0 || vwidth == dwidth) &&
(dheight < 0 || vheight == dheight);
// If the drawable has no intrinsic size, or we're told to scaletofit, then we just fill our entire view.
// 当图片的大小未知或者ScaleType为FIT_XY时,
// 将drawable的边界设置为控件的大小,并且将图片matrix对象设置为null。
// setBounds方法用以设置Drawable绘制区域的大小,是一个矩形对象。
// 在ImageView的onDraw方法中,最终会将Drawable表示的图片绘制到该区域上。
// 在这里,就是将图片绘制到ImageView大小的区域上,也就是所谓的图片完整显示,
// 填充满ImageView。注意此处并未对Matrix对象进行操作,而是直接设置为null,
// 所以当ImageView的ScaleType为FIT_XY时,getImageMatrix将获取到一个初始Matrix对象。
if (dwidth <= 0 || dheight <= 0 || ScaleType.FIT_XY == mScaleType) {
mDrawable.setBounds(0, 0, vwidth, vheight);
mDrawMatrix = null;
} else {
// We need to do the scaling ourself, so have the drawable
// use its native size.
// 当ScaleType不为FIT_XY并且图片宽高不为0,
// 就会把drawable的绘制区域设成图片实际大小。
// 注:此处的实际大小不是指文件大小,而是指源文件大小和屏幕分辨率计算后的结果。
// 比如源文件是200*300的图片,但是当前屏幕分辨率是dp= 3,
// 那么最后加载进来的drawable大小就是600 * 900,你的明白?
mDrawable.setBounds(0, 0, dwidth, dheight);
// 接下去判断ScaleType是否为MATRIX,如果为MATRIX类型,
// 就会把当前绘制用的的Matrix赋值为mMatrix,
// mMatrix是通过setImageMatrix方法赋值的。
// 这之后还有一个if(fits)判断,上面代码已经说过fits的含义,
// 假如fits为true,表示图片大小等于ImageView大小,也不需要进行缩放操作了,
// setImageMatrix方法如下,可以看出其实就是对mMatrix进行操作
// public void setImageMatrix(Matrix matrix) {
// // collapse null and identity to just null
// if (matrix != null && matrix.isIdentity()) {
// matrix = null;
// }
//
// // don't invalidate unless we're actually changing our matrix
// if (matrix == null && !mMatrix.isIdentity() ||
// matrix != null && !mMatrix.equals(matrix)) {
// mMatrix.set(matrix);
// configureBounds();
// invalidate();
// }
// }
if (ScaleType.MATRIX == mScaleType) {
// Use the specified matrix as-is.
if (mMatrix.isIdentity()) {
mDrawMatrix = null;
} else {
mDrawMatrix = mMatrix;
}
} else if (fits) {
// The bitmap fits exactly, no transform needed.
mDrawMatrix = null;
} else if (ScaleType.CENTER == mScaleType) {
// Center bitmap in view, no scaling.
// 当ScaleType类型为CENTER时,实际操作是将图片进行右移和下移操作,
// 移动的数值分别为为ImageView宽度、高度与图片宽度、高度差值的一半,
// 这样就达到了居中显示的效果。注意,CENTER不进行缩放操作只进行位移操作,
// 所以图片的显示大小并未改变,当图片大于控件时,只显示居中部分,
// 大于控件的部分未显示。
mDrawMatrix = mMatrix;
mDrawMatrix.setTranslate(Math.round((vwidth - dwidth) * 0.5f),
Math.round((vheight - dheight) * 0.5f));
} else if (ScaleType.CENTER_CROP == mScaleType) {
// 在CENTER_CROP中,首先对dwidth * vheight > vwidth * dheight进行了一个判断,
// 这个判断是什么意思呢?其实换成这样比较容易理解:dwidth/vwidth>dheight/vheight,
// 即判断到底是图片的宽度比较接近控件宽度还是图片高度比较控件高度,
// 最终会取相差较大的项,
// 将其放大至控件对应的值。而另外一项将超出控件大小。之后,对其进行位移,
// 使超出控件大小的一项居中显示。注:当图片大于控件时,是将超出更少的项进行缩放。
// CROP的目的在于对图片宽高进行变换,使其中一项和控件的值相等,
// 另外一项大于控件的值。
// 说白了简单理解是就是加入照片宽高是200* 300,控件宽高是100 * 100,
// 那么显示的逻辑就是先将照片缩放成100* 150,
// 然后将高度150平移(150 - 100) /2 = 25个像素就ok了
mDrawMatrix = mMatrix;
float scale;
float dx = 0, dy = 0;
if (dwidth * vheight > vwidth * dheight) {
scale = (float) vheight / (float) dheight;
dx = (vwidth - dwidth * scale) * 0.5f;
} else {
scale = (float) vwidth / (float) dwidth;
dy = (vheight - dheight * scale) * 0.5f;
}
mDrawMatrix.setScale(scale, scale);
mDrawMatrix.postTranslate(Math.round(dx), Math.round(dy));
} else if (ScaleType.CENTER_INSIDE == mScaleType) {
//当为CENTER_INSIDE时,若图片高宽均小于控件高宽,
// 则不进行缩放只进行偏移,偏移方式跟其他情况相同。
// 否则,将根据图片和控件的宽高比例差距更大的一项进行缩放,
// 缩放的结果就是其中一项和控件相同,另外一项小于控件值。
// 这个刚好和CENTER_CROP相反。
mDrawMatrix = mMatrix;
float scale;
float dx;
float dy;
if (dwidth <= vwidth && dheight <= vheight) {
scale = 1.0f;
} else {
scale = Math.min((float) vwidth / (float) dwidth,
(float) vheight / (float) dheight);
}
dx = Math.round((vwidth - dwidth * scale) * 0.5f);
dy = Math.round((vheight - dheight * scale) * 0.5f);
mDrawMatrix.setScale(scale, scale);
mDrawMatrix.postTranslate(dx, dy);
} else {
// Generate the required transform.
// 最后三种FIT_CENTER、FIT_START、FIT_END
// 都是调用setRectToRect获取缩放偏移矩阵。
// setRectToRect返回一个Matrix,该Matrix表示从矩形mTempSrc到mTemDst的变换矩阵,
// 根据第三个参数Matrix.ScaleToFit来确定
// 缩放选项。Matrix.ScaleToFit定义了四种选项:
// CENTER: 保持坐标变换前矩形的长宽比,并最大限度的填充变换后的矩形。
// 至少有一边和目标矩形重叠。END:保持坐标变换前矩形的长宽比,
// 并最大限度的填充变换后的矩形。至少有一边和目标矩形重叠。
// END提供右下对齐。FILL: 可能会变换矩形的长宽比,
// 保证变换和目标矩阵长宽一致。START:保持坐标变换前矩形的长宽比,
// 并最大限度的填充变换后的矩形。至少有一边和目标矩形重叠。
// START提供左上对齐。 ScaleType的FIT_CENTER、FIT_START、
// FIT_END分别对应于这里的CENTER、END、START。
mTempSrc.set(0, 0, dwidth, dheight);
mTempDst.set(0, 0, vwidth, vheight);
mDrawMatrix = mMatrix;
mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType));
}
}
}
// 好了,理解了上面的代码,我们也知道了如何来做通过变化matrix来进行
// iamgeView的各种scaleType的变化下面的这个实现类,
// 我们就实现了使用该iamgeView来播放动画的功能
/**
* ImageView to display center-crop scale of an image view.
*
* @author robin
*/
public class MatrixCenterCropImageView extends ImageLoaderView {
private static final String TAG = "MatrixCenterCropImageView";
// 动画执行时间
private static final int ANIM_TIME = 3000;
// 动画延迟执行时间
private static final int ANIM_DELAY_TIME = 0;
// 动画结束监听器
private AnimationEndListener mListener;
// 动画执行序列
private Queue<AnimType> mAnimTypeQueue = new ArrayDeque<>();
private ValueAnimator mAnimator;
// 动画类型
public enum AnimType {
ALPHA_0_TO_1_ANIM, ALPHA_1_TO_0_ANIM, SCALE_ANIM, TRANS_X_ANIM
}
public MatrixCenterCropImageView(Context context) {
super(context);
setScaleType(ScaleType.MATRIX);
}
public MatrixCenterCropImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setScaleType(ScaleType.MATRIX);
}
public MatrixCenterCropImageView(Context context, AttributeSet attrs) {
super(context, attrs);
setScaleType(ScaleType.MATRIX);
}
/**
* 自定义初始化动画执行序列
*
* @param animTypes
*/
public void initAnimOrder(AnimType... animTypes) {
mAnimTypeQueue.clear();
for (AnimType animType : animTypes) {
mAnimTypeQueue.add(animType);
}
}
/**
* 初始化默认动画执行序列
*/
public void initDefaultAnimOrder() {
mAnimTypeQueue.clear();
mAnimTypeQueue.add(AnimType.ALPHA_0_TO_1_ANIM);
mAnimTypeQueue.add(AnimType.SCALE_ANIM);
mAnimTypeQueue.add(AnimType.TRANS_X_ANIM);
mAnimTypeQueue.add(AnimType.ALPHA_1_TO_0_ANIM);
}
/**
* 开始执行动画
*/
public void startMatrixViewAnimation() {
oneByOneDoAnimation();
}
/**
* 顺序执行动画
*/
private void oneByOneDoAnimation() {
if (mAnimTypeQueue.size() == 0) return;
switch (mAnimTypeQueue.poll()) {
case ALPHA_0_TO_1_ANIM:
doAlphaAnimation(0f, 1, false);
break;
case SCALE_ANIM:
startScaleAnimation();
break;
case TRANS_X_ANIM:
startTranslateXAnimation();
break;
case ALPHA_1_TO_0_ANIM:
doAlphaAnimation(1, 0f, true);
break;
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
reComputeImgMatrix();
}
@Override
protected boolean setFrame(int l, int t, int r, int b) {
reComputeImgMatrix();
return super.setFrame(l, t, r, b);
}
/**
* 重新计算图片第一次显示的时候的缩放尺寸和平移比例
* 目的是为了让图片居中显示
*/
private void reComputeImgMatrix() {
if (getDrawable() == null) return;
final Matrix matrix = getImageMatrix();
float scale;
final int viewWidth = getWidth() - getPaddingLeft() - getPaddingRight();
final int viewHeight = getHeight() - getPaddingTop() - getPaddingBottom();
final int drawableWidth = getDrawable().getIntrinsicWidth();
final int drawableHeight = getDrawable().getIntrinsicHeight();
L.i("drawableHeight = " + drawableHeight + " drawableWidth = " + drawableWidth);
if (drawableWidth * viewHeight > drawableHeight * viewWidth) {
scale = (float) viewHeight / (float) drawableHeight;
} else {
scale = (float) viewWidth / (float) drawableWidth;
}
matrix.setScale(scale, scale);
float tr = 0, th = 0;
if ((drawableWidth * scale) > viewWidth) {
tr = -(((drawableWidth * scale) - viewWidth) / 2);
}
if ((drawableHeight * scale) > viewHeight) {
th = -(((drawableHeight * scale) - viewHeight) / 2);
}
matrix.postTranslate(tr, th);
setImageMatrix(matrix);
}
/**
* 执行alpha的动画
*
* @param startAlpha 开始alpha值
* @param endAlpha 结束alpha值
* @param isNotifyCallBack 是否动画结束回调监听器
*/
private void doAlphaAnimation(float startAlpha, float endAlpha, final boolean isNotifyCallBack) {
mAnimator = ValueAnimator.ofFloat(startAlpha, endAlpha);
mAnimator.setInterpolator(new LinearInterpolator());
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float alpha = (float) animation.getAnimatedValue();
setAlpha(alpha);
}
});
mAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
oneByOneDoAnimation();
if (isNotifyCallBack && mListener != null) mListener.onAnimationEnd();
}
});
mAnimator.setDuration(ANIM_TIME);
mAnimator.setInterpolator(new DecelerateInterpolator());
mAnimator.setStartDelay(ANIM_DELAY_TIME);
mAnimator.start();
}
/**
* 开始进行缩放动画
*/
private void startScaleAnimation() {
float[] values = new float[9];
getImageMatrix().getValues(values);
float startScale = values[Matrix.MSCALE_X];
mAnimator = ValueAnimator.ofFloat(startScale, startScale + startScale * 0.3f);
MyScaleAnimatorListener listener = new MyScaleAnimatorListener();
mAnimator.addUpdateListener(listener);
mAnimator.addListener(listener);
mAnimator.setDuration(ANIM_TIME);
mAnimator.setInterpolator(new DecelerateInterpolator());
mAnimator.setStartDelay(ANIM_DELAY_TIME);
mAnimator.start();
}
/**
* 开始进行X轴缩放动画
*/
private void startTranslateXAnimation() {
float[] values = new float[9];
getImageMatrix().getValues(values);
float scaleX = values[Matrix.MSCALE_X];
final int viewWidth = getWidth() - getPaddingLeft() - getPaddingRight();
final int drawableWidth = getDrawable().getIntrinsicWidth();
int transX_Max = 0;
if ((drawableWidth * scaleX) > viewWidth) {
transX_Max = (int) -(((drawableWidth * scaleX) - viewWidth) / 2);
}
L.dt(TAG, "setTranslateXAnimation =" + transX_Max + " drawableWidth = " + drawableWidth + " scaleX = " + scaleX + " viewWidth = " + viewWidth);
mAnimator = ValueAnimator.ofInt(0, transX_Max);
MyTransXAnimatorListener listener = new MyTransXAnimatorListener(getImageMatrix());
mAnimator.addUpdateListener(listener);
mAnimator.addListener(listener);
mAnimator.setDuration(ANIM_TIME);
mAnimator.setInterpolator(new LinearInterpolator());
mAnimator.setStartDelay(ANIM_DELAY_TIME);
mAnimator.start();
}
private class MyScaleAnimatorListener extends AnimatorListenerAdapter implements ValueAnimator.AnimatorUpdateListener {
public MyScaleAnimatorListener() {
}
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// 缩放动画实现类,原理就是拿到当前的缩放值进行缩放设置
// 并根据最新的缩放值设置偏移值,这样才能保证drawable在控件中间
float scale = (Float) animation.getAnimatedValue();
Matrix matrix = new Matrix(getImageMatrix());
matrix.setScale(scale, scale);
final int viewWidth = getWidth() - getPaddingLeft() - getPaddingRight();
final int viewHeight = getHeight() - getPaddingTop() - getPaddingBottom();
final int drawableWidth = getDrawable().getIntrinsicWidth();
final int drawableHeight = getDrawable().getIntrinsicHeight();
float tr = 0, th = 0;
if ((drawableWidth * scale) > viewWidth) {
tr = -(((drawableWidth * scale) - viewWidth) / 2);
}
if ((drawableHeight * scale) > viewHeight) {
th = -(((drawableHeight * scale) - viewHeight) / 2);
}
matrix.postTranslate(tr, th);
setImageMatrix(matrix);
}
@Override
public void onAnimationEnd(Animator animation) {
L.dt(TAG, "onAnimationEnd");
oneByOneDoAnimation();
}
}
private class MyTransXAnimatorListener extends AnimatorListenerAdapter implements ValueAnimator.AnimatorUpdateListener {
private Matrix mPrimaryMatrix;
public MyTransXAnimatorListener(Matrix matrix) {
mPrimaryMatrix = new Matrix(matrix);
}
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int dx = (Integer) animation.getAnimatedValue();
Matrix matrix = new Matrix(mPrimaryMatrix);
matrix.postTranslate(dx, 0);
setImageMatrix(matrix);
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
oneByOneDoAnimation();
}
}
/**
* 停止动画
*/
public void stopAnimation() {
if (mAnimator != null) {
if (mAnimator.isRunning()) {
mAnimator.cancel();
}
mAnimator.removeAllUpdateListeners();
mAnimator.removeAllListeners();
}
}
/**
* 设置控件动画全部结束的回调
*
* @param listener 动画结束回调接口
*/
public void setListener(AnimationEndListener listener) {
mListener = listener;
}
/**
* 动画结束回调接口
*/
public interface AnimationEndListener {
void onAnimationEnd();
}
}
使用方式:
1:添加布局文件
<MatrixCenterCropImageView
android:id="@+id/bg_view"
android:layout_width="match_parent"
android:layout_height="230dp"
android:src="@drawable/ic_camera_focus_infinity"
android:scaleType="matrix"/>
2:MatrixCenterCropImageView mFavoriteBgView = (MatrixCenterCropImageView) findViewById(R.id.bg_view);
mFavoriteBgView.setListener(this--自己new一个或者实现一个);
mFavoriteBgView.initDefaultAnimOrder();
mFavoriteBgView.startMatrixViewAnimation();