阅读 1401

使用Matrix做一个图片过渡动画吧!

前言

最近在做关于图片查看的优化,陆陆续续的花了一周多的时间(加上其他需求修Bug的时间),恼火。

恼火.jpg

不过确实有点收获,所以后续的几篇文章都会跟图片或者动画相关,本篇是个开胃菜,跟大家介绍一下如何使用Matrix。

看一下学习完 Matrix 可以写出的动画:

Matrix动画.gif

一、Matrix是什么?

作为一个 Android 开发仔,我们直接使用 Matrix 的机会比较少。

但是我们却一直在和 Matrix 打交道,比如 ImageView,通过设置不同的 ScaleType 得到不同的 Matrix,从而展现不同的效果。再深一点,各种 View 的背后,都有 Matrix 的一份功劳!

Matrix 是一个 3*3 的矩阵,如果你已经忘了矩阵是什么?可以在 【这里】 回顾一下。

链表结构@2x (1).png

既然是一个 3*3 矩阵,所以他由9个参数组成:

上面的6个数字分别控制着缩放、斜切和平移,缩放和斜切一起控制,可以达到旋转的效果。

下面的三个参数可以控制 3D 变换,本篇中我们不做过多的介绍。

说明

本节剩下的这一部分来自 GcsSloop 的文章:《Matrix简介》

1.缩放(Scale)

用矩阵表示:

你可能注意到了,我们坐标多了一个1,这是使用了齐次坐标系的缘故,在数学中我们的点和向量都是这样表示的(x, y),两者看起来一样,计算机无法区分,为此让计算机也可以区分它们,增加了一个标志位,增加之后看起来是这样:

(x, y, 1) - 点
(x, y, 0) - 向量

另外,齐次坐标具有等比的性质,(2,3,1)、(4,6,2)...(2N,3N,N)表示的均是(2,3)这一个点。(将MPERSP_2解释为scale这一误解就源于此)。

图例:

2.错切(Skew)

错切存在两种特殊错切,水平错切(平行X轴)和垂直错切(平行Y轴)。

水平错切

用矩阵表示:

图例:

垂直错切

用矩阵表示:

图例:

复合错切

水平错切和垂直错切的复合。

用矩阵表示:

图例:

3.旋转(Rotate)

假定一个点 A(x0, y0) ,距离原点距离为 r, 与水平轴夹角为 α 度, 绕原点旋转 θ 度, 旋转后为点 B(x, y) 如下:

用矩阵表示:

图例:

4.平移(Translate)

此处也是使用齐次坐标的优点体现之一,实际上前面的三个操作使用 2x2 的矩阵也能满足需求,但是使用 2x2 的矩阵,无法将平移操作加入其中,而将坐标扩展为齐次坐标后,将矩阵扩展为 3x3 就可以将算法统一,四种算法均可以使用矩阵乘法完成。

用矩阵表示:

图例:

二、ScaleType和Matrix

从上面我们跟着GcsSloop大佬学习了基本的 Matrix 操作,现在我们再来了解 ImageView 中的 ScaleType 和其对应 Matrix

我们都知道,ImageView 中对应的 ScaleTyp 有:CENTER_CROPCENTERCENTER_INSIDEFIT_ENDFIT_CENTERFIT_STARTFIT_XYMATRIX

假设图中的 ImageView 的宽和高都为 300 像素,图片的宽高分别为 1200 和 600 像素。

一开始Matrix.png

1. CENTER_CROP

CENTER_CROP 是我们平时用的最多的一种类型。规则:图片宽高等比缩放,直到宽高都大于或者等于控件的宽高,图片多出来的部分会被裁切

如何得到 CENTER_CROP 对应的 Matrix 呢?其实也很简单,分两步:

第一步:缩放图片,将图片宽高任意一边都能够大于或者等于控件的长度。

控件长 / 图片长 = o.25
控件宽 / 图片宽 = 0.5
复制代码

为了能将图片充满控件,保证不留空白,需要选择控件图片比大的那一边,所以我们现在的 Matrix 为:

[0.5  0    0]
[0    0.5  0]
[0    0    1]
复制代码

当前图片和控件就变成了:

活动状态.png

第二步:居中平移,如上图,图片的宽仍大于控件的宽,为了让图片的中间位置显示在控件上,需要对图片做一定的平移。

translationX = (imageViewWidth - drawableWidth * scale) / 2,需要平移矩阵左乘缩放矩阵,更新后的 Matrix 为:

[1  0  -150]     [0.5  0  0]    [0.5  0  -150]
[0  1     0]  *  [0  0.5  0] =  [0  0.5     0]
[0  0     1]     [0  0    1]    [0  0       1]
复制代码

当前图片和控件就变成了:

最终图形.png

这也是应用了 CENTER_CROPImageView 的最终形态。

2. Center

Center 规则:图片不缩放,只平移,最终将图片中心区域展示在控件上。

所以经计算:

translationX = (imageViewWidth - drawableWidth) / 2;
translationY = (imageViewHeight - drawableHeight) / 2;
复制代码

Matrix 为:

[1  0  -450]
[0  1  -150]
[0  0     1]
复制代码

得到的图形为:

Center效果

CENTER_INSIDE

CENTER_INSIDE 规则:等比缩小图片,使得控件可以展示完整的图片并居中展示,当图片小于控件的宽高时不放大。

CenterInside效果.png

FIT_XY

FIT_XY 规则:分别缩放图片宽高,缩放后的宽高会等于控件的宽高,图片可能会变形。

FitXY效果.png

FIT_START

FIT_START 规则:等比缩放图片宽高,控件可以完整展示图片,平移至头部区域。

Fit_start效果.png

FIT_CENTER

FIT_CENTER 规则:等比缩放图片宽高,控件可以完整展示图片,平移至控件中心区域,当图片宽高小于控件宽高的时会对图片放大。

CenterInside效果.png

FIT_END

FIT_END 规则:等比缩放图片宽高,控件可以完整展示图片,平移到控件底部和尾部。

Fit_end效果.png

Matrix

Matrix 规则:自定义 Matrix

三、做一个Matrix动画

做一个 Matrix 过渡动画还是比较简单的,因为估值器 MatrixEvaluator 是现成的,只要确定前后两个或者几个 Matrix 就行, Matrix 的使用方法:

val evaluator = MatrixEvaluator()
val matrix = evaluator.evaluate(it.animatedFraction, firstMatrix, secondMatrix)
iv.imageMatrix = matrix
复制代码

在本文开始的动画中,一共需要6个 Matrix(1和5一样):

Fit_end效果@2x.png

那如何获取这些 Matrix 呢?ImageView 为我们提供 ImageView#getImageMatrix() 方法,帮助我们获取当前 ImageView 的 Matrix,我们再对每幅图分析一下:

  1. 第一张:当前 Matrix 不变。
  2. 第二张:当前 Matrix 向右平移一半宽。
  3. 第三张:当前 Matrix 向右平移一半宽,向下平移一半高。
  4. 第四张:当前 Matrix 向下平移一半高。
  5. 第五张:当前 Matrix 同第一张。
  6. 第六张:当前 Matrix 向右平移1/4宽,向下平移1/4高。
  7. 第七张:当前 Matrix 不变。

剩下的就是动画代码:

fun doSomeThingAnimation() {
    ivContent?.let { iv ->
        val imgWidth = iv.width
        val imgHeight = iv.height
        iv.layoutParams.width = imgWidth / 2
        iv.layoutParams.height = imgHeight / 2
        val tX = imgWidth.toFloat() / 2
        val ty = imgHeight.toFloat() / 2
        val initMatrix = iv.imageMatrix

        // 获取6个Matrix
        val oneMatrix = Matrix(initMatrix)
        val twoMatrix = Matrix(initMatrix).apply {
            postTranslate(-tX, 0f)
        }
        val threeMatrix = Matrix(initMatrix).apply {
            postTranslate(-tX, -ty)
        }
        val fourthMatrix = Matrix(initMatrix).apply {
            postTranslate(0f, -ty)
        }
        val fiveMatrix = Matrix(initMatrix).apply {
            postTranslate(-tX / 2, - ty / 2)
        }
        val sixMatrix = Matrix(initMatrix)

        iv.scaleType = ScaleType.MATRIX
        iv.imageMatrix = oneMatrix

        // 构建6段动画
        val evaluator = MatrixEvaluator()
        val firstAnimator  = ValueAnimator.ofFloat(0f, 1f)
        firstAnimator.addUpdateListener {
            val matrix = evaluator.evaluate(it.animatedFraction, oneMatrix, twoMatrix)
            iv.translationX = it.animatedFraction  * imgWidth / 2
            iv.imageMatrix = matrix
        }
        val secondAnimator  = ValueAnimator.ofFloat(0f, 1f)
        secondAnimator.addUpdateListener {
            val fraction = it.animatedFraction
            iv.translationY = fraction * imgHeight / 2
            val matrix = evaluator.evaluate(fraction, twoMatrix, threeMatrix)
            iv.imageMatrix = matrix
        }
        val threeAnimator  = ValueAnimator.ofFloat(0f, 1f)
        threeAnimator.addUpdateListener {
            val fraction = it.animatedFraction
            iv.translationX = ((1 - fraction) * imgWidth / 2)
            val matrix = evaluator.evaluate((fraction), threeMatrix, fourthMatrix)
            iv.imageMatrix = matrix
        }
        val fourAnimator  = ValueAnimator.ofFloat(0f, 1f)
        fourAnimator.addUpdateListener {
            val fraction = it.animatedFraction
            iv.translationY = ((1 - fraction) * imgHeight / 2)
            val matrix = evaluator.evaluate((fraction), fourthMatrix, oneMatrix)
            iv.imageMatrix = matrix
        }
        val fiveAnimator  = ValueAnimator.ofFloat(0f, 1f)
        fiveAnimator.addUpdateListener {
            val fraction = it.animatedFraction
            iv.translationY = (fraction * imgHeight / 4)
            iv.translationX = (fraction * imgWidth / 4)
            val matrix = evaluator.evaluate((fraction), oneMatrix, fiveMatrix)
            iv.imageMatrix = matrix
        }
        val sixAnimator  = ValueAnimator.ofFloat(0f, 1f)
        sixAnimator.addUpdateListener {
            val fraction = it.animatedFraction
            iv.layoutParams.width = (imgWidth / 2 + imgWidth / 2 * fraction).toInt()
            iv.layoutParams.height = (imgHeight / 2 + imgHeight / 2 * fraction).toInt()
            iv.translationY = ((1 - fraction) * imgHeight / 4)
            iv.translationX = ((1 - fraction) * imgWidth / 4)
            val matrix = evaluator.evaluate((fraction), fiveMatrix, sixMatrix)
            iv.imageMatrix = matrix
            iv.requestLayout()
        }
        val set = AnimatorSet()
        set.playSequentially(firstAnimator, secondAnimator, threeAnimator, fourAnimator, fiveAnimator, sixAnimator)
        set.duration = 1000
        set.start()
    }
}
复制代码

一个简单动画技能 Get!

总结

这个动画实际使用的场景并不多,但是当你需要同一个图片不同 ScaleType 之前切换的时候,比如说小图打开成大图的时候,Matrix 动画也许可以帮的上你的忙!

精彩内容

如果觉得本文不错,「点赞」是对作者最大的鼓励~

技术不止,文章有料,关注公众号 九心说,每周一篇高质好文,和九心在大厂路上肩并肩。

引用文章:

《Matrix简介》

文章分类
Android
文章标签