天天看點

Android自定義控件進階10-控件核心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自定義控件進階10-控件核心Matrix Camera
至于為什麼要用左手坐标系呢?~~大概是因為趕工的時候右手不友善比劃吧,大霧。~~實際上不同平台上使用的坐标系也有不同,有的是左手,有的是右手,貌似并沒有統一的标準,隻需要記住 Android 平台上面使用的是左手坐标系即可。

2D 和 3D 坐标是通過Matrix關聯起來的,是以你可以認為兩者是同一個坐标系,但又有差别,重點就是y軸方向不同。

坐标系 2D坐标系 3D坐标系
原點預設位置 左上角 左上角
X 軸預設方向
Y 軸預設方向
Z 軸預設方向 垂直螢幕向内

3D坐标系在螢幕中各個坐标軸預設方向展示:

注意y軸預設方向是向上,而2D則是向下,另外本圖不代表3D坐标系實際位置。
Android自定義控件進階10-控件核心Matrix Camera

三維投影

三維投影是将三維空間中的點映射到二維平面上的方法。由于目前絕大多數圖形資料的顯示方式仍是二維的,是以三維投影的應用相當廣泛,尤其是在計算機圖形學,工程學和工程制圖中。

三維投影一般有兩種,正交投影 和 透視投影。

正交投影就是我們數學上學過的 “正視圖、正視圖、側視圖、俯視圖” 這些東西。

透視投影則更像拍照片,符合近大遠小的關系,有立體感,我們此處使用的就是透視投影。

錄影機

如果你學過Unity,那麼你對錄影機這一個概念應該會有比較透徹的了解。在一個虛拟的3D的立體空間中,由于我們無法直接用眼睛去觀察這一個空間,是以要借助錄影機采集資訊,制成2D影像供我們觀察。簡單來說,錄影機就是我們觀察虛拟3D空間的眼睛。

Android 上面觀察View的錄影機預設位置在螢幕左上角,而且是距螢幕有一段距離的,假設灰色部分是手機螢幕,白色是上面的一個View,錄影機位置看起來大緻就是下面這樣子的(為了更好的展示錄影機的位置,做了一個空間轉換效果的動圖)。

Android自定義控件進階10-控件核心Matrix Camera

錄影機的位置預設是 (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的大小以及距離左上角(錄影機在螢幕投影位置)的距離。
Android自定義控件進階10-控件核心Matrix Camera

至于為什麼會這樣,因為我們人眼視覺就是這樣的,當我們看向遠方的時候,視線最終都會消失在視平線上,如果你站在兩條平行線中間,看起來它們會在遠方(視平線上)相交,雖然在3D空間上兩者距離不變,但在2D投影上卻是越來越接近,如下圖(圖檔來自網絡):

Android自定義控件進階10-控件核心Matrix Camera

結論:

關于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);
           

這個東西瞎扯理論也不好了解,直接上圖:

Android自定義控件進階10-控件核心Matrix Camera
Android自定義控件進階10-控件核心Matrix Camera
Android自定義控件進階10-控件核心Matrix Camera

以上三張圖分别為,繞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呢?),并且很多人不知道怎麼修改。

不知諸位在使用的時候可否發現了一個問題,同一份代碼在不同手機上顯示效果也是不同的,在像素密度較低的手機上,旋轉效果比較正常,但是在像素密度較高的手機上顯示效果則會很誇張,具體會怎樣的,下面就來看一下具體效果。

Android自定義控件進階10-控件核心Matrix Camera

可以看到,圖檔不僅因為形變失真,而且在中間一段因為形變過大導緻圖檔無法顯示,當然了,單個手機失真,你可以用

depthZ

忽悠過去,當

depthZ

設定的數值比較大大時候,圖像在翻轉同時會遠離攝像頭,距離比較遠,失真就不會顯得很嚴重,但這仍掩蓋不了在不同手機上顯示效果不同。

如何解決這一問題呢?

想要解決其實也不難,隻要修改兩個數值就可以了,這兩個數值就是在Matrix中一直被衆多開發者忽略的

MPERSP_0

MPERSP_1

Android自定義控件進階10-控件核心Matrix Camera

下面是修改後的代碼(重點部分都已經标注出來了):

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);
    }
}
           

修改後效果:

Android自定義控件進階10-控件核心Matrix Camera

上下對比差别還是很大的,順便附上測試代碼吧,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自定義控件進階10-控件核心Matrix Camera
Android自定義控件進階10-控件核心Matrix Camera

續集:

  • 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-特殊控件的事件處理方案

粉絲技術交流扣裙

Android自定義控件進階10-控件核心Matrix Camera