本篇依舊屬于Matrix,主要講解Camera,Android下有很多相機應用,其中的美顔相機更是不少,不過今天這個Camera可不是我們平時拍照的那個相機,而是graphic包下的Camera,專業給View拍照的相機,不過既然是相機,作用都是類似的,主要是将3D的内容拍扁變成2D的内容。
衆所周知,我們的手機螢幕是一個2D的平面,是以也沒辦法直接顯示3D的資訊,是以我們看到的所有3D效果都是3D在2D平面的投影而已,而本文中的Camera主要作用就是這個,将3D資訊轉換為2D平面上的投影,實際上這個類更像是一個操作Matrix的工具類,使用Camera和Matrix可以在不使用OpenGL的情況下制作出簡單的3D效果。
⚠️ 警告:測試本文章示例之前請關閉硬體加速。
Camera常用方法表
方法類别 | 相關API | 簡介 |
---|---|---|
基本方法 | save、restore | 儲存、 復原 |
常用方法 | getMatrix、applyToCanvas | 擷取Matrix、應用到畫布 |
平移 | translate | 位移 |
旋轉 | rotat (API 12)、rotateX、rotateY、rotateZ | 各種旋轉 |
相機位置 | setLocation (API 12)、getLocationX (API 16)、getLocationY (API 16)、getLocationZ (API 16) | 設定與擷取相機位置 |
Camera的方法并不是特别多,很多内容與之前的講解的Canvas和Matrix類似,不過又稍有不同,之前的畫布操作和Matrix主要是作用于2D空間,而Camera則主要作用于3D空間。
基礎概念
在具體講解方法之前,先補充幾個基礎概念,以便于後面了解。
3D坐标系
我們Camera使用的3維坐标系是左手坐标系,即左手手臂指向x軸正方向,四指彎曲指向y軸正方向,此時展開大拇指指向的方向是z軸正方向。
至于為什麼要用左手坐标系呢?~~大概是因為趕工的時候右手不友善比劃吧,大霧。~~實際上不同平台上使用的坐标系也有不同,有的是左手,有的是右手,貌似并沒有統一的标準,隻需要記住 Android 平台上面使用的是左手坐标系即可。
2D 和 3D 坐标是通過Matrix關聯起來的,是以你可以認為兩者是同一個坐标系,但又有差别,重點就是y軸方向不同。
坐标系 | 2D坐标系 | 3D坐标系 |
---|---|---|
原點預設位置 | 左上角 | 左上角 |
X 軸預設方向 | 右 | 右 |
Y 軸預設方向 | 下 | 上 |
Z 軸預設方向 | 無 | 垂直螢幕向内 |
3D坐标系在螢幕中各個坐标軸預設方向展示:
注意y軸預設方向是向上,而2D則是向下,另外本圖不代表3D坐标系實際位置。
三維投影
三維投影是将三維空間中的點映射到二維平面上的方法。由于目前絕大多數圖形資料的顯示方式仍是二維的,是以三維投影的應用相當廣泛,尤其是在計算機圖形學,工程學和工程制圖中。
三維投影一般有兩種,正交投影 和 透視投影。
正交投影就是我們數學上學過的 “正視圖、正視圖、側視圖、俯視圖” 這些東西。
透視投影則更像拍照片,符合近大遠小的關系,有立體感,我們此處使用的就是透視投影。
錄影機
如果你學過Unity,那麼你對錄影機這一個概念應該會有比較透徹的了解。在一個虛拟的3D的立體空間中,由于我們無法直接用眼睛去觀察這一個空間,是以要借助錄影機采集資訊,制成2D影像供我們觀察。簡單來說,錄影機就是我們觀察虛拟3D空間的眼睛。
Android 上面觀察View的錄影機預設位置在螢幕左上角,而且是距螢幕有一段距離的,假設灰色部分是手機螢幕,白色是上面的一個View,錄影機位置看起來大緻就是下面這樣子的(為了更好的展示錄影機的位置,做了一個空間轉換效果的動圖)。
錄影機的位置預設是 (0, 0, -576)。其中 -576= -8 x 72,雖然官方文檔說距離螢幕的距離是 -8, 但經過測試實際距離是 -576 像素,當距離為 -10 的時候,實際距離為 -720 像素。我使用了3款手機測試,螢幕大小和像素密度均不同,但結果都是一樣的。
這個魔數可以在 Android 底層的圖像引擎 Skia 中找到。在 Skia 中,Camera 的位置機關是英寸,英寸和像素的換算機關在 Skia 中被固定為 72 像素,而 Android 中把這個換算機關照搬了過來。
基本方法
基本方法就有兩個
save
和
restore
,主要作用為
儲存目前狀态和恢複到上一次儲存的狀态
,通常成對使用,常用格式如下:
camera.save(); // 儲存狀态
... // 具體操作
camera.retore(); // 復原狀态
常用方法
這兩個方法是Camera中最基礎也是最常用的方法。
getMatrix
void getMatrix (Matrix matrix)
計算目前狀态下矩陣對應的狀态,并将計算後的矩陣指派給參數matrix。
applyToCanvas
void applyToCanvas (Canvas canvas)
計算目前狀态下單矩陣對應的狀态,并将計算後的矩陣應用到指定的canvas上。
平移
聲明:以下示例中 Matrix 的平移均使用 postTranslate 來示範,實際情況中使用set、pre 或 post 需要視情況而定。
void translate (float x, float y, float z)
和2D平移類似,隻不過是多出來了一個次元,從隻能在2D平面上平移到在3D空間内平移,不過,此處仍有幾個要點需要重點對待。
沿x軸平移
camera.translate(x, 0, 0);
matrix.postTranslate(x, 0);
兩者x軸同向,是以 Camera 和 Matrix 在沿x軸平移上是一緻的。
結論:
一緻是指平移方向和平移距離一緻,在預設情況下,上面兩種均可以讓坐标系向右移動x個機關。
沿y軸平移
這個就有點意思了,兩個坐标系互相關聯,但是兩者的y軸方向是相反的,很容易把人搞迷糊。你可以這麼玩:
Camera camera = new Camera();
camera.translate(0, 100, 0); // camera - 沿y軸正方向平移100像素
Matrix matrix = new Matrix();
camera.getMatrix(matrix);
matrix.postTranslate(0,100); // matrix - 沿y軸正方向平移100像素
Log.i(TAG, "Matrix: "+matrix.toShortString());
在上面這種寫法,雖然用了5行代碼,但是效果卻和
Matrix matrix = new Matrix();
一樣,結果都是機關矩陣。而且看起來貌似沒有啥問題,畢竟兩次平移都是正向100。(如果遇見不懂技術的上司嫌你寫代碼量少,你可以這樣多寫幾遍,反正一般人是看不出問題的。)
Matrix: [1.0, 0.0, 0.0][0.0, 1.0, 0.0][0.0, 0.0, 1.0]
結論:
由于兩者y軸相反,是以
camera.translate(0, -y, 0);
與
matrix.postTranslate(0, y);
平移方向和距離一緻,在預設情況下,這兩種方法均可以讓坐标系向下移動y個機關。
沿z軸平移
這個不僅有趣,還容易蒙逼,上面兩種情況再怎麼鬧騰也隻是在2D平面上,而z軸的出現則讓其有了空間感。
當View和錄影機在同一條直線上時: 此時沿z軸平移相當于縮放的效果,縮放中心為錄影機所在(x, y)坐标,當View接近錄影機時,看起來會變大,遠離錄影機時,看起來會變小,近大遠小。
當View和錄影機不在同一條直線上時: 當View遠離錄影機的時候,View在縮小的同時也在不斷接近錄影機在螢幕投影位置(通常情況下為Z軸,在平面上表現為接近坐标原點)。相反,當View接近錄影機的時候,View在放大的同時會遠離錄影機在螢幕投影位置。
我知道,這樣說你們肯定是蒙逼的,話說為啥遠離錄影機的時候會接近錄影機在螢幕投影位置(´・_・`),肯定覺得我在逗你們玩,完全是前後沖突,邏輯都不通,不過這個在這裡的确是不沖突的,因為遠離是在3D空間裡的情況,而接近隻是在2D空間的投影,看下圖。
假設大矩形是手機螢幕,白色小矩形是View,錄影機位于螢幕左上角,請注意上面View與錄影機的距離以及下方View的大小以及距離左上角(錄影機在螢幕投影位置)的距離。
至于為什麼會這樣,因為我們人眼視覺就是這樣的,當我們看向遠方的時候,視線最終都會消失在視平線上,如果你站在兩條平行線中間,看起來它們會在遠方(視平線上)相交,雖然在3D空間上兩者距離不變,但在2D投影上卻是越來越接近,如下圖(圖檔來自網絡):
結論:
關于3D效果的平移說起來比較麻煩,但你可以自己實際的體驗一下,畢竟我們是生活在3D空間的,拿一張紙片來模拟View,用眼睛當做錄影機,在眼前來回移動紙片,多試幾次大緻就明白是怎麼回事了。
平移 | 重點内容 |
---|---|
x軸 | 2D 和 3D 相同。 |
y軸 | 2D 和 3D 相反。 |
z軸 | 近大遠小、視線相交。 |
旋轉
旋轉是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軸旋轉的情況,至于為什麼沒有顯示z軸,是因為z軸是垂直于手機螢幕的,在螢幕上的投影就是一個點。
關于旋轉,有以下幾點需要注意:
旋轉中心
旋轉中心預設是坐标原點,對于圖檔來說就是左上角位置。
我們都知道,在2D中,不論是旋轉,錯切還是縮放都是能夠指定操作中心點位置的,但是在3D中卻沒有預設的方法,如果我們想要讓圖檔圍繞中心點旋轉怎麼辦? 這就要使用到我們在Matrix原理提到過的方法:
Matrix temp = new Matrix(); // 臨時Matrix變量
this.getMatrix(temp); // 擷取Matrix
temp.preTranslate(-centerX, -centerY); // 使用pre将旋轉中心移動到和Camera位置相同。
temp.postTranslate(centerX, centerY); // 使用post将圖檔(View)移動到原來的位置
官方示例-Rotate3dAnimation
說到3D旋轉,最經典的應該就是ApiDemo裡面的 Rotate3dAnimation 了,見過不少博文都是根據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);
}
}
可以看到,短短的幾十行代碼就完成了,而核心代碼(有注釋部分)僅僅幾行而已,簡潔易懂。不過呢,這一份代碼依舊是一份未完成的代碼(不然怎麼叫ApiDemo呢?),并且很多人不知道怎麼修改。
不知諸位在使用的時候可否發現了一個問題,同一份代碼在不同手機上顯示效果也是不同的,在像素密度較低的手機上,旋轉效果比較正常,但是在像素密度較高的手機上顯示效果則會很誇張,具體會怎樣的,下面就來看一下具體效果。
可以看到,圖檔不僅因為形變失真,而且在中間一段因為形變過大導緻圖檔無法顯示,當然了,單個手機失真,你可以用
depthZ
忽悠過去,當
depthZ
設定的數值比較大大時候,圖像在翻轉同時會遠離攝像頭,距離比較遠,失真就不會顯得很嚴重,但這仍掩蓋不了在不同手機上顯示效果不同。
如何解決這一問題呢?
想要解決其實也不難,隻要修改兩個數值就可以了,這兩個數值就是在Matrix中一直被衆多開發者忽略的
MPERSP_0
和
MPERSP_1
下面是修改後的代碼(重點部分都已經标注出來了):
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);
}
}
修改後效果:
上下對比差别還是很大的,順便附上測試代碼吧,layout檔案就不寫了,随便放一個ImageView就行了。
setContentView(R.layout.activity_test_camera_rotate2);
ImageView view = (ImageView) findViewById(R.id.img);
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, 2);
rotation.setDuration(3000); //設定動畫時長
rotation.setFillAfter(true); //保持旋轉後效果
rotation.setInterpolator(new LinearInterpolator()); //設定插值器
v.startAnimation(rotation);
}
});
相機位置
我們可以使用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
小技巧:關于錄影機和View的位置,你可以打開手機後置攝像頭,拿一張卡片來回的轉動平移或者移動手機位置,觀察卡片在螢幕上的變化,
總結
本篇主要講解了關于Camera和Matrix的一些基礎知識,Camera運用得當的話是能夠制造出很多炫酷的效果的,我這裡算是抛磚引玉,推薦一些比較炫酷的控件。
FlipShare
從零開始打造一個Android 3D立體旋轉容器
續集:
- Android自定義控件進階01-自定義控件開發套路與流程
- Android自定義控件進階02-Canvas之繪制圖形
- Android自定義控件進階03-Canvas之畫布操作
- Android自定義控件進階04-Canvas之圖檔文字
- Android自定義控件進階05-Path之基本操作
- Android自定義控件進階06-Path之貝塞爾曲線
- Android自定義控件進階07-Path之完結篇
- Android自定義控件進階08-PathMeasure詳解
- Android自定義控件進階09-控件核心Matrix原理
- Android自定義控件進階10-控件核心Matrix Camera
- Android自定義控件進階11-事件分發機制原理
- Android自定義控件進階11-事件分發機制原理01
- Android自定義控件進階12-事件分發機制原理02
- Android自定義控件進階13-MotionEvent詳解
- Android自定義控件進階14-特殊控件的事件處理方案
粉絲技術交流扣裙