安卓自定義View進階-Matrix Camera

本篇依舊屬於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 像素。不過這個數值72我也不明白是什麼東西,我使用了3款手機測試,屏幕大小和像素密度均不同,但結果都是一樣的,知道的小夥伴可以告訴我一聲。

基本方法

基本方法就有兩個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立體旋轉容器

 

參考資料

Camera
FlipShare
從零開始打造一個Android 3D立體旋轉容器

About Me

作者微博: @GcsSloop

發佈了32 篇原創文章 · 獲贊 10 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章