写在前面:
本文主要通过学习安卓自定义View进阶-Matrix Camera这篇文章去学习android.graphics.Camera,因此大部分文字出自这篇文章。
在Camera学习记录_1中,大致学习了camera的原理和简单应用。本篇的目的是Camera的旋转。旋转是Camera制作3D效果的核心。
旋转
旋转是Camera制作3D效果的核心,不过它制作出来的并不能算是真正的3D,而是伪3D,因为View是没有厚度的。
// (API 12) 可以控制View同时绕x,y,z轴旋转,可以由下面几种方法复合而来。
void rotate (float x, float y, float z);
// 控制View绕单个坐标轴旋转
void rotateX (float deg);
void rotateY (float deg);
void rotateZ (float deg);
绕X轴旋转
绕Y轴旋转
绕Z轴旋转
以上三张图分别为,绕x轴,y轴,z轴旋转的情况,至于为什么没有显示z轴,是因为z轴是垂直于手机屏幕的,在屏幕上的投影就是一个点。
关于旋转,有以下几点需要注意:
旋转中心
旋转中心默认是坐标原点,对于图片来说就是左上角位置。
我们都知道,在2D中,不论是旋转,错切还是缩放都是能够指定操作中心点位置的(可以用这个方法xxxRotate(angle, pivotX, pivotY)
),但是在3D中却没有默认的方法,如果我们想要让图片围绕中心点旋转怎么办? 这就要使用到在Matrix原理提到过的方法:
1. 先将坐标系原点移动到指定位置,使用平移 T
2. 对坐标系进行旋转,使用旋转 S (围绕原点旋转)
3. 再将坐标系平移回原来位置,使用平移 -T
伪代码如下
Matrix temp = new Matrix(); // 临时Matrix变量
this.getMatrix(temp); // 获取Matrix
temp.preTranslate(-centerX, -centerY); // 使用pre将旋转中心移动到和Camera位置相同。
temp.postTranslate(centerX, centerY); // 使用post将图片(View)移动到原来的位置
这里讲了大致思路。详细部分看下面的例子。
官方示例-Rotate3dAnimation
public class Rotate3dAnimation extends Animation {
private final float mFromDegrees;
private final float mToDegrees;
private final float mCenterX;
private final float mCenterY;
private final float mDepthZ;
private final boolean mReverse;
private Camera mCamera;
/**
* 创建一个绕y轴旋转的3D动画效果,旋转过程中具有深度调节,可以指定旋转中心。
*
* @param fromDegrees 起始时角度
* @param toDegrees 结束时角度
* @param centerX 旋转中心x坐标
* @param centerY 旋转中心y坐标
* @param depthZ 最远到达的z轴坐标
* @param reverse true 表示由从0到depthZ,false相反
*/
public Rotate3dAnimation(float fromDegrees, float toDegrees,
float centerX, float centerY, float depthZ, boolean reverse) {
mFromDegrees = fromDegrees;
mToDegrees = toDegrees;
mCenterX = centerX;
mCenterY = centerY;
mDepthZ = depthZ;
mReverse = reverse;
}
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
mCamera = new Camera();
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
final float fromDegrees = mFromDegrees;
float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime);
final float centerX = mCenterX;
final float centerY = mCenterY;
final Camera camera = mCamera;
final Matrix matrix = t.getMatrix();
camera.save();
// 调节深度
if (mReverse) {
camera.translate(0.0f, 0.0f, mDepthZ * interpolatedTime);
} else {
camera.translate(0.0f, 0.0f, mDepthZ * (1.0f - interpolatedTime));
}
// 绕y轴旋转
camera.rotateY(degrees);
camera.getMatrix(matrix);
camera.restore();
// 调节中心点
matrix.preTranslate(-centerX, -centerY);
matrix.postTranslate(centerX, centerY);
}
}
xml
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/test"/>
</LinearLayout>
MainActivity
public class MainActivity extends AppCompatActivity{
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ImageView view = (ImageView) findViewById(R.id.image);
assert view != null;
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 计算中心点(这里是使用view的中心作为旋转的中心点)
final float centerX = v.getWidth() / 2.0f;
final float centerY = v.getHeight() / 2.0f;
final Rotate3dAnimation rotation = new Rotate3dAnimation( 0, 180, centerX, centerY, 0f, true);
rotation.setDuration(3000); //设置动画时长
rotation.setFillAfter(true); //保持旋转后效果
rotation.setInterpolator(new LinearInterpolator()); //设置插值器
v.startAnimation(rotation);
}
});
}
}
但是官方的api demo旋转会失真。
1.gif
如何解决这一问题呢?
解决问题关键Matrix的是MPERSP_0
和 MPERSP_1
加了 <-------
的部分为修改部分。
Rotate3dAnimation
public class Rotate3dAnimation extends Animation {
private final float mFromDegrees;
private final float mToDegrees;
private final float mCenterX;
private final float mCenterY;
private final float mDepthZ;
private final boolean mReverse;
private Camera mCamera;
float scale = 1; // <------- 像素密度
/**
* 创建一个绕y轴旋转的3D动画效果,旋转过程中具有深度调节,可以指定旋转中心。
* @param context <------- 添加上下文,为获取像素密度准备
* @param fromDegrees 起始时角度
* @param toDegrees 结束时角度
* @param centerX 旋转中心x坐标
* @param centerY 旋转中心y坐标
* @param depthZ 最远到达的z轴坐标
* @param reverse true 表示由从0到depthZ,false相反
*/
public Rotate3dAnimation(Context context, float fromDegrees, float toDegrees,
float centerX, float centerY, float depthZ, boolean reverse) {
mFromDegrees = fromDegrees;
mToDegrees = toDegrees;
mCenterX = centerX;
mCenterY = centerY;
mDepthZ = depthZ;
mReverse = reverse;
// <-------获取手机像素密度 (即dp与px的比例)
scale = context.getResources().getDisplayMetrics().density;
}
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
mCamera = new Camera();
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
final float fromDegrees = mFromDegrees;
float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime);
final float centerX = mCenterX;
final float centerY = mCenterY;
final Camera camera = mCamera;
final Matrix matrix = t.getMatrix();
camera.save();
// 调节深度
if (mReverse) {
camera.translate(0.0f, 0.0f, mDepthZ * interpolatedTime);
} else {
camera.translate(0.0f, 0.0f, mDepthZ * (1.0f - interpolatedTime));
}
// 绕y轴旋转
camera.rotateY(degrees);
camera.getMatrix(matrix);
camera.restore();
// <-------修正失真,主要修改 MPERSP_0 和 MPERSP_1
float[] mValues = new float[9];
matrix.getValues(mValues); //获取数值
mValues[6] = mValues[6]/scale; //数值修正
mValues[7] = mValues[7]/scale; //数值修正
matrix.setValues(mValues); //重新赋值
// 调节中心点
matrix.preTranslate(-centerX, -centerY);
matrix.postTranslate(centerX, centerY);
}
}
MainActivity
public class MainActivity extends AppCompatActivity{
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ImageView view = (ImageView) findViewById(R.id.image);
assert view != null;
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 计算中心点(这里是使用view的中心作为旋转的中心点)
final float centerX = v.getWidth() / 2.0f;
final float centerY = v.getHeight() / 2.0f;
//<------- 括号内参数分别为(上下文,开始角度,结束角度,x轴中心点,y轴中心点,深度,是否扭曲)
final Rotate3dAnimation rotation = new Rotate3dAnimation(MainActivity.this, 0, 180, centerX, centerY, 0f, true);
rotation.setDuration(3000); //设置动画时长
rotation.setFillAfter(true); //保持旋转后效果
rotation.setInterpolator(new LinearInterpolator()); //设置插值器
v.startAnimation(rotation);
}
});
}
}
这个例子中 通过 给Matrix 的MPERSP_0 和 MPERSP_1重新赋值来修正bug。
MPERSP_0 = MPERSP_0 / density ,MPERSP_1 = MPERSP_1 / density 。具体原理未知。
首先来看PERSP是什么。
出处:Unity下Iso和Persp两种模式的区别
image.png
Unity中有一种Persp模式,透视视野。在persp模式下,物体在scene界面上所呈现的画面是给人一种距离摄像头近的物体显示的大,距离摄像头远的物体显示的小。
没有找到具体Persp是什么的答案,只知道有一种模式叫Persp模式,这种模式是透视视野。这种模式跟android中的camera的视野类似。
再来回顾一下 缩放因子density:density = dpi / 160,常见取值1.0,2.0,3.0。 dp与px换算公式 :px = dp * density。
以下部分为个人猜测,说错了不负责。
发生失真的原因应该是操作过程中单位不一致导致的。
android的世界中单位是dp,而是图像和camera 的 单位是px。
MPERSP_0 / density ,MPERSP_1 / density 是为了把px转成dp,从而使旋转过程中单位统一。
相机位置
我们可以使用translate和rotate来控制拍摄对象,也可以移动相机自身的位置,不过这些方法并不常用(看添加时间就知道啦)。
void setLocation (float x, float y, float z); // (API 12) 设置相机位置,默认位置是(0, 0, -8)
float getLocationX (); // (API 16) 获取相机位置的x坐标,下同
float getLocationY ();
float getLocationZ ();
我们知道近大远小,而物体之间的距离是相对的,让物体远离相机和让相机远离物体结果是一样的,实际上设置相机位置基本可以使用translate替代。
虽然设置相机位置用处并不大,但还是要提几点注意事项:
相机和View的z轴距离不能为0
这个比较容易理解,当你把一个物体和相机放在同一个位置的时候,相机是拍摄不到这个物体的,正如你拿一张卡片放在手机侧面,摄像头是拍摄不到的。
虚拟相机前后均可以拍摄
当View不断接近摄像机并越过摄像机位置时,仍能看到View,并且View大小会随着距离摄像机的位置越来越远而逐渐变小,你可以理解为它有前置摄像头和后置摄像头。
摄像机右移等于View左移
View的状态只取决于View和摄像机之间的相对位置,不过由于单位不同,摄像机平移一个单位等于View平移72个像素。下面两段代码是等价的:
Camera camera = new Camera();
camera.setLocation(1,0,-8); // 摄像机默认位置是(0, 0, -8)
Matrix matrix = new Matrix();
camera.getMatrix(matrix);
Log.e(TAG, "location: "+matrix.toShortString() );
Camera camera2 = new Camera();
camera2.translate(-72,0,0);
Matrix matrix2 = new Matrix();
camera2.getMatrix(matrix2);
Log.e(TAG, "translate: "+matrix2.toShortString() );
结果:
location: [1.0, 0.0, -72.0][0.0, 1.0, 0.0][0.0, 0.0, 1.0]
translate: [1.0, 0.0, -72.0][0.0, 1.0, 0.0][0.0, 0.0, 1.0
要点
View显示状态取决于View和摄像机之间的相对位置
View和相机的Z轴距离不能为0
网友评论