背景
接上篇文章用Kotlin+Retrofit+RxKotlin+MVP撸了一个小项目。
这个效果是在逛今日头条时候看到一条宝马车的广告有这样的效果。觉得很有意思,网上应该也有很多实现的例子,但是就想着自己思考实现一下。emmm,模拟器不能模拟出传感器的效果,然后不知道怎么录真机的视频上传。。。所以效果脑补一下吧。(一张很大的图片,手机往左边偏,图片往右边移动。。。)
技术探索
- 传感器技术
- Matrix
要想监听到手机晃动,肯定只有用传感器啦,由于Android系统的强大,我们使用各种传感器也是很方便的啦。
另外图片的左右移动之类的效果,之前想着用位移动画。。然后想到动画是让整个 View 在移动,而不是图片在移动,达不到效果。然后就决定用Matrix来做。事实证明是真的简单,就几行代码。当然要闹懂原理还是需要深入学习的。
代码
布局
布局很简单,就是一个固定大小的ImaveView
。 这里有很重要的一点就是scaleType
一定要设置成 matrix
不然是没有效果的,原因我们后面讲。
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/sensor_iv"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_200"
android:layout_marginTop="@dimen/dimen_100"
android:contentDescription="@string/app_name"
android:scaleType="matrix"
android:src="@drawable/aaaa" />
</FrameLayout>
复制代码
传感器
首先来看一下传感器世界的坐标系是怎样的:
手机平放在桌面上,横向的是X轴,纵向是Y轴,与屏幕垂直的方向是Z轴。
然后传感器的使用也很简单 Android sensor framework 为我们提供了丰富我类和接口。 常用的类如下:
-
SensorManager
你能使用这个类来创建传感器服务的实例。这个类提供了各种方法类访问和列举传感器,注册和注销传感器的事件监听,获取相应的信息。这个类也提供了一些传感器的常量:报告传感器的精确度,设置数据获取频率,和校准传感器。 -
Sensor
通过这个类能够创建一个特定传感器的视力,这个类提过了各种检测传感器能力的方法。 -
SensorEvent
系统使用这个类来创建一个传感器的事件对象,它提供传感器事件的信息。传感器事件包含以下信息:原始传感器数据,产生这个事件的传感器类型,数据的准确性,以及事件的时间戳。
获取传感器
private var manager: SensorManager? = null
private var sensor: Sensor? = null
....
//获取传感器管理对象
manager = activity.getSystemService(Context.SENSOR_SERVICE) as SensorManager
sensor = manager!!.getDefaultSensor(Sensor.TYPE_ORIENTATION) //获取向传感器
复制代码
由于我们要实现的功能只需要方向传感器就可以了,所以我们获取了方向传感器对象,当然系统还有更多的传感器对象,这里我就不一一介绍了,大家可以阅读文档或者百度一下。另外由于手机硬件的区别,请判断传感器对象是否可用。(方向传感器大部分手机应该都有吧哈哈)
定义监听
private val mListener = object : SensorEventListener {
override fun onSensorChanged(event: SensorEvent) {
//传感器值发生改变
val values = event.values
//Log.e(TAG, "onSensorChanged: Z轴 = "+values[0]);
//Log.e(TAG, "onSensorChanged: X轴 = " + values[1]);
//Log.e(TAG, "onSensorChanged: Y轴 = " + values[2]);
}
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
//传感器精度发生改变
}
}
复制代码
注册和解除监听
override fun onResume() {
super.onResume()
Log.e(TAG, "onResume: ")
manager!!.registerListener(mListener, sensor, SensorManager.SENSOR_DELAY_UI)
}
override fun onDestroy() {
super.onDestroy()
Log.e(TAG, "onDestroy: ")
manager!!.unregisterListener(mListener)
}
复制代码
使用SensorManager
的registerListener
方法可以为我们注册所需要的传感器监听。 其中最后一个参数是控制回调频率的,有如下几个值:
- SensorManager.SENSOR_DELAY_FASTEST:最快,延迟最小,同时也最消耗资源,一般只有特别依赖传感器的应用使用该频率,否则不推荐。
- SensorManager.SENSOR_DELAY_GAME:适合游戏的频率,一般有实时性要求的应用适合使用这种频率。
- SensorManager.SENSOR_DELAY_NORMAL:正常频率,一般对实时性要求不高的应用适合使用这种频率。
- SensorManager.SENSOR_DELAY_UI:适合普通应用的频率,这种模式比较省电,而且系统开销小,但延迟大,因此只适合普通小程序使用。
图片移动
传感器搞定了,回调值拿到了,接下来就应该让图片动起来了。前面说过代码很少,是真的很少,
private val mListener = object : SensorEventListener {
override fun onSensorChanged(event: SensorEvent) {
//传感器值发生改变
val values = event.values
//Log.e(TAG, "onSensorChanged: 绕Z轴旋转 = "+values[0]);
//Log.e(TAG, "onSensorChanged: 绕X轴翘起 = " + values[1]);
//Log.e(TAG, "onSensorChanged: 绕Y轴翘起 = " + values[2]);
val x = values[1]
val y = values[2]
//TODO 边界控制
val imageMatrix = Matrix()
imageMatrix.preTranslate(-y * scale, -x * scale)
imageView!!.imageMatrix = imageMatrix
}
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
//传感器精度发生改变
}
}
复制代码
让图片移动起来的代码就三句:
val imageMatrix = Matrix()
imageMatrix.preTranslate(-y * scale, -x * scale)
imageView!!.imageMatrix = imageMatrix
复制代码
这三句是怎么起作用的呢?我们一一来分析
首先创建了一个新的 Matrix
对象,那么 Matrix
(矩阵)是怎么作用于图像的呢? 这里简单说一下我的理解 (这方面涉及到矩阵的乘法运算)可以参考:矩阵的数学知识--维基百科
图形处理所用到的是一个 3*3 的矩阵:
矩阵的这些参数分别控制的效果如下:
我们知道一张图片是由像素点组成的,我们对每个像素点进行位移,那么图片也就会跟着位移了,所以看一下矩阵怎么对一个像素点进行作用的。前面讲到屏幕的坐标系中有Z轴的概念,也就是空间的概念,一个点就是(x,y,z) 用矩阵表示就是
然后看平移操作的图例
如图,A点移动到B点,移动的距离分别是 △x,△y 。那么
x=x0+△x
y=y0+△y
用矩阵表示就是:
也就是用我们设定的矩阵去乘以A点,就得到了B点。等号右边最终运算的结果就是
x0+△x
y0+△y
1
就得到了上面的数学式子了。(这里有一个前乘和后乘的概念,我才疏学浅,这里就不讲了,大家可以自己去了解一下)
代码表示
当然这些运算系统已经为我们封装好了
val imageMatrix = Matrix()
构建一个单位矩阵。单位矩阵如下
preTranslate
方法就是矩阵的前乘。参数分别是 x,y 移动的距离。
这里注意:传感器返回的值是 绕某一轴翘起的角度
比如 x的值表示手机顶部或尾部翘起的高度。当手机绕着X轴倾斜时,该角度值发生变化,该角度的取值范围是-180~180度。假设手机屏幕朝上水平放在桌子上,如果桌子是完全水平的,该角度值应该是0度。假如从手机顶部开始抬起,直到将手机沿X轴旋转180度(屏幕向下水平放在桌子上),在这个旋转的过程中,该角度值会从0度变化到-180度。也就是说,从手机顶部抬起时,该角度的值会逐渐减少,直到等于-180度;如果从手机底部开始抬起,直到将手机沿X轴旋转180度(屏幕向下水平放在桌子上),该角度的值会从0度变化到180度,也就是说,从手机底部抬起时,该角度的值会逐渐增大,直到等于180度。
那么想象一下,手机底部翘起了,我们想要的则是图片往下移动则得以看到图片上面隐藏的内容。所以传入的参数则是相反的,数值也应该是相反的。 imageMatrix.preTranslate(-y * scale, -x * scale)
这个 scale
则是为了增大移动的幅度而乘的。
imageView!!.imageMatrix = imageMatrix
这里是调用了 ImageView的setImageMatrix
方法。改变内部的矩阵,达到我们想要的效果。
接下来就是填坑了
第一个是布局文件中android:scaleType="matrix"
看Imageview源码:
源码中针对我们设置的scaleType
都有分别的处理,我们设置了matrix
的逻辑中 mDrawMatrix = mMatrix;
把我们设置的矩阵赋给了用于绘制的矩阵且没有做其他处理。
而我们调用的setImageMatrix
内部是这样的
里面有对参数传入的矩阵与内部的矩阵进行比较和单位矩阵的校验。
最后有一点是边界控制没有做到,有木有大神评论指导一下下。。嘤嘤嘤