mScaleAnimator = new ScaleAnimator(mOuterMatrix, animEnd);
mScaleAnimator.start();
这段代码很骚,我们先来梳理下缩放的思路:双击图片,肯定是要以动画的形式来做的,那么动画的开头,自然是当前的变换位置,变换到目标缩放值 nextScale
的倍数是 nextScale
/ currentScale
,遵从手势操作记录在外部矩阵 mOuterMatrix
的原则,动画初始 matrix
拷贝自 mOuterMatrix
。 这段代码其实是有问题的。innerScale
是对原图进行 fitCenter
变换后的缩放值,假设原图很大,变换后 innerScale
值为0.2f, maxScale
为2,没有进行过手势操作,outerScale
为1,这时候来看下算的结果:
就是说你双击一下,一下子看到的图片放大了10倍…… 要知道现在很多图宽高都是比手机屏幕大的…… ScaleAnimator
里只做了一件事,不断更新 mOuterMatrix
的值,然后 invalidate
,在 onDraw
里刷新视图。
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//获取动画进度
float value = (Float) animation.getAnimatedValue();
//根据动画进度计算矩阵中间插值
for (int i = 0; i < 9; i++) {
mResult[i] = mStart[i] + (mEnd[i] - mStart[i]) * value;
}
//设置矩阵并重绘
mOuterMatrix.setValues(mResult);
……
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
……
//在绘制前设置变换矩阵
setImageMatrix(getCurrentImageMatrix(matrix));
……
super.onDraw(canvas);
……
}
缩放平移后,图片可能出现边框进入图片控件的情况,此时需要修正位置。用最终缩放后的图片边界和控件边界对比矫正即可。
Matrix testMatrix = MathUtils.matrixTake(innerMatrix);
testMatrix.postConcat(animEnd);
RectF testBound = MathUtils.rectFTake(0, 0, getDrawable().getIntrinsicWidth(), getDrawable().getIntrinsicHeight());
testMatrix.mapRect(testBound);
刚才已经知道, animEnd
记录的是当前双击变换操作作用在外部矩阵的结果,把它和内部矩阵(innerMatrix
)相乘就得到了最终对原图(testBound
)的变换矩阵(testMatrix
)。
//修正位置
float postX = 0;
float postY = 0;
if (testBound.right - testBound.left < displayWidth) {
postX = displayWidth / 2f - (testBound.right + testBound.left) / 2f;
} else if (testBound.left > 0) {
postX = -testBound.left;
} else if (testBound.right < displayWidth) {
postX = displayWidth - testBound.right;
}
……
//应用修正位置
animEnd.postTranslate(postX, postY);
这里修正位置很容易看懂,就不说了,纠正源码的两个错误: postX = displayWidth / 2f - (testBound.right + testBound.left) / 2f;
里的 testBound.right + testBound.left
应为 testBound.right - testBound.left
。没贴出来的 postY
也要改下。
1.2 惯性滑动(Fling)
PinchImageView
的惯性滑动是自己处理衰减的…… 每次衰减的程度还一样,不支持插值器,比起PhotoView
使用 OverScroller
来处理滑动,就显得有点简陋了。 GestureDetector
的 onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)
包含x、y轴的加速度,加速度单位是像素/秒,每秒60帧,转换成像素/帧即 velocityX/60
、 velocityY/60
。PinchImageView
使用 FlingAnimator
来做动画,动画更新初始滑动距离 velocityX/60
,然后乘以衰减值(FLING_DAMPING_FACTOR
,0.9),待下次更新使用。
//移动图像并给出结果
boolean result = scrollBy(mVector[0], mVector[1], null);
mVector[0] *= FLING_DAMPING_FACTOR;
mVector[1] *= FLING_DAMPING_FACTOR;
//速度太小或者不能移动了就结束
if (!result || MathUtils.getDistance(0, 0, mVector[0], mVector[1]) < 1f) {
animation.cancel();
}
scrollBy(float xDiff, float yDiff, MotionEvent motionEvent)
方法处理滚动,主要考虑图片边界和控件边界的处理,跟上面缩放时的修正位置是一样的原理,图片边界的获取也跟缩放时是一样的。
//获取内部变换矩阵
matrix = getInnerMatrix(matrix);
//乘上外部变换矩阵
matrix.postConcat(mOuterMatrix);
rectF.set(0, 0, getDrawable().getIntrinsicWidth(), getDrawable().getIntrinsicHeight());
matrix.mapRect(rectF);
最后对 mOuterMatrix
进行平移变换(postTranslate
),invalidate
触发 onDraw
给图片设置新矩阵。
3.2 双指缩放、单指移动
双指缩放、单指移动是在 onTouch
里做的。
3.2.1 双指缩放
原理:记录双指在屏幕上距离,外部矩阵的缩放值与此距离相除的商为单位距离的缩放值,以这个缩放值去乘以双指滑动后的距离得到一个新的缩放值,用这个缩放值给外部矩阵做缩放变换得到最终的外部矩阵。
很明显,mScaleBase
这个单位距离的缩放值是斜率,决定了双指缩放的速度。那么决定双指缩放速度的因素有:当前外部矩阵的缩放大小、双指间初始距离。外部矩阵缩放越大,双指间初始距离越小,双指滑动缩放越快。 还有一个要注意的是图片的缩放中心点,在 PinchImageView
中,双指缩放变换是在单位矩阵中进行的。所以当双指按下的时候需要记录外部矩阵变换之前的中心点,源码里用 mScaleCenter
成员变量来记录这个点(PS:建议肉眼屏蔽源码里在所有用到这个变量地方的注释,你会晕的)。 快速看下相关的代码:
private PointF mScaleCenter = new PointF();
private float mScaleBase = 0;
……
public boolean onTouchEvent(MotionEvent event) {
……
int action = event.getAction() & MotionEvent.ACTION_MASK;
if (action == MotionEvent.ACTION_POINTER_DOWN) {
//切换到缩放模式
mPinchMode = PINCH_MODE_SCALE;
//保存缩放的两个手指
saveScaleContext(event.getX(0), event.getY(0), event.getX(1), event.getY(1));
}else if (action == MotionEvent.ACTION_MOVE) {
……
//两个缩放点间的距离
float distance = MathUtils.getDistance(event.getX(0), event.getY(0), event.getX(1), event.getY(1));
//保存缩放点中点
float[] lineCenter = MathUtils.getCenterPoint(event.getX(0), event.getY(0), event.getX(1), event.getY(1));
mLastMovePoint.set(lineCenter[0], lineCenter[1]);
//处理缩放
scale(mScaleCenter, mScaleBase, distance, mLastMovePoint);
……
}
}
在多指按下的时候记录当前的是双指缩放模式,saveScaleContext()
记录上面提到的 mScaleBase
和 mScaleCenter
。在 MotionEvent.ACTION_MOVE
里处理缩放逻辑。看下 saveScaleContext
的处理。
private void saveScaleContext(float x1, float y1, float x2, float y2) {
mScaleBase = MathUtils.getMatrixScale(mOuterMatrix)[0] / MathUtils.getDistance(x1, y1, x2, y2);
float[] center = MathUtils.inverseMatrixPoint(MathUtils.getCenterPoint(x1, y1, x2, y2), mOuterMatrix);
mScaleCenter.set(center[0], center[1]);
}
mScaleBase
上面已经讲过了,这里主要提下 inverseMatrixPoint
,看下方法定义:
public static float[] inverseMatrixPoint(float[] point, Matrix matrix) {
if (point != null && matrix != null) {
float[] dst = new float[2];
//计算matrix的逆矩阵
Matrix inverse = matrixTake();
matrix.invert(inverse);
//用逆矩阵变换point到dst,dst就是结果
inverse.mapPoints(dst, point);
//清除临时变量
matrixGiven(inverse);
return dst;
} else {
return new float[2];
}
}
srcMatrix.invert(targetMatrix)
把 srcMatrix
矩阵的逆矩阵存到 targetMatrix
中,martrix.mapPoints(targetPoint, srcPoint);
对 srcPoint
应用矩阵变换并存放到 targetPoint
中。很明显这个方法的作用的是得到经过矩阵变换之前的点。 mScaleCenter
存的正是外部矩阵变换之前的点的位置。 接下来看下缩放的处理。
private void scale(PointF scaleCenter, float scaleBase, float distance, PointF lineCenter) {
……
//计算图片从fit center状态到目标状态的缩放比例
float scale = scaleBase * distance;
Matrix matrix = MathUtils.matrixTake();
//按照图片缩放中心缩放,并且让缩放中心在缩放点中点上
matrix.postScale(scale, scale, scaleCenter.x, scaleCenter.y);
//让图片的缩放中点跟随手指缩放中点
matrix.postTranslate(lineCenter.x - scaleCenter.x, lineCenter.y - scaleCenter.y);
mOuterMatrix.set(matrix);
……
}
很容易看懂,上面都讲过了。这里吐槽一下,如果 mOuterMatrix
发生过错切、旋转、透视变换,那不就废了吗? 还有一个多个手指抬起一个手指的情况。注释已经修改过了,很容易看懂。
if (action == MotionEvent.ACTION_POINTER_UP) {
if (mPinchMode == PINCH_MODE_SCALE) {
//event.getPointerCount()表示抬起手指时点的数量,包含抬起的那个点
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
面试宝典
面试必问知识点、BATJ历年历年面试真题+解析
学习经验总结
(一)调整好心态
心态是一个人能否成功的关键,如果不调整好自己的心态,是很难静下心来学习的,尤其是现在这么浮躁的社会,大部分的程序员的现状就是三点一线,感觉很累,一些大龄的程序员更多的会感到焦虑,而且随着年龄的增长,这种焦虑感会越来越强烈,那么唯一的解决办法就是调整好自己的心态,要做到自信、年轻、勤奋。这样的调整,一方面对自己学习有帮助,另一方面让自己应对面试更从容,更顺利。
(二)时间挤一挤,制定好计划
一旦下定决心要提升自己,那么再忙的情况下也要每天挤一挤时间,切记不可“两天打渔三天晒网”。另外,制定好学习计划也是很有必要的,有逻辑有条理的复习,先查漏补缺,然后再系统复习,这样才能够做到事半功倍,效果才会立竿见影。
(三)不断学习技术知识,更新自己的知识储备
对于一名程序员来说,技术知识方面是非常重要的,可以说是重中之重。**要面试大厂,自己的知识储备一定要非常丰富,若缺胳膊少腿,别说在实际工作当中,光是面试这一关就过不了。**对于技术方面,首先基础知识一定要扎实,包括自己方向的语言基础、计算机基础、算法以及编程等等。
一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
一旦下定决心要提升自己,那么再忙的情况下也要每天挤一挤时间,切记不可“两天打渔三天晒网”。另外,制定好学习计划也是很有必要的,有逻辑有条理的复习,先查漏补缺,然后再系统复习,这样才能够做到事半功倍,效果才会立竿见影。
(三)不断学习技术知识,更新自己的知识储备
对于一名程序员来说,技术知识方面是非常重要的,可以说是重中之重。**要面试大厂,自己的知识储备一定要非常丰富,若缺胳膊少腿,别说在实际工作当中,光是面试这一关就过不了。**对于技术方面,首先基础知识一定要扎实,包括自己方向的语言基础、计算机基础、算法以及编程等等。
一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-4wnaBpwq-1712613912214)]