Camera与Matrix的那些事儿

1、引子

笔者刚开始工作时,做的第一个模块是手机中的launcher,launcher可自由选择滑屏效果,甚至还有三D效果,酷炫的动画让人震惊同时也感到非常迷惑,这动画效果怎么实现的呢?帧动画?补间动画还是属性动画,都不能实现以上效果。它们都与本文中提到的Camera相关

ps:自定义view是android应用开发工程师的必备技能,除了需要了解view原理、touch事件分发等等,还需要了解绘制相关的东西,例如canvas、matrix、camera等等

2、camera

android底层所有的绘制都是通过opengl进行的,为了方便用户使用,android封装了一些常用接口,用户可使用camera进行旋转、移动等等

A camera instance can be used to compute 3D transformations and generate a matrix that can be applied, for instance, on aCanvas.
一个照相机实例可以被用于计算3D变换,生成一个可以被使用的Matrix矩阵,一个实例,用在画布上。

camera的常用方法为:

Camera() 创建一个没有任何转换效果的新的Camera实例
applyToCanvas(Canvas canvas) 根据当前的变换计算出相应的矩阵,然后应用到制定的画布上
getLocationX() 获取Camera的x坐标
getLocationY() 获取Camera的y坐标
getLocationZ() 获取Camera的z坐标
getMatrix(Matrixmatrix) 获取转换效果后的Matrix对象
restore() 恢复保存的状态
rotate(float x, float y, float z) 沿X、Y、Z坐标进行旋转
rotateX(float deg)
rotateY(float deg)
rotateZ(float deg)
save() 保存状态
setLocation(float x, float y, float z)
translate(float x, float y, float z)沿X、Y、Z轴进行平移

可将camera想象成一个处于坐标原点的相机,而view在空间中的某一点,通过操作相机,view在相机中看到的就不一样了,可实现旋转、移动等等,如果沿Z轴移动,view还能放大缩小

上文中提到旋转,其实canvas本身也可以旋转,也可以平衡。但camera比canvas的强大之处在于,camera的坐标系为空间坐标系,它有Z轴,它是立体的。而canvas的坐标系是平面的。

camera的坐标系为左手坐标系,伸出左手,大姆指朝x轴正向,食指朝Y轴方向,中指垂直于view平面,指向Z轴。

左手坐标系.png

3、matrix

matrix代表着一个矩阵,view的平移、旋转等等,系统都会封装成矩阵运算,将结果保存在矩阵中,根据矩阵绘制view

matrix类中封装了一些接口,开发者不需要直接写矩阵的值,就可以实现平移、旋转、缩放等。

setTranslate(floatdx,floatdy):控制Matrix进行平移
setSkew(floatkx,floatky,floatpx,floatpy):控制Matrix以px,py为轴心进行倾斜,kx,ky为X,Y方向上的倾斜距离
setRotate(floatdegress):控制Matrix进行旋转,degress控制旋转的角度
setRorate(floatdegress,floatpx,floatpy):设置以px,py为轴心进行旋转,degress控制旋转角度
setScale(floatsx,floatsy):设置Matrix进行缩放,sx,sy控制X,Y方向上的缩放比例
setScale(floatsx,floatsy,floatpx,floatpy):设置Matrix以px,py为轴心进行缩放,sx,sy控制X,Y方向上的缩放比例

matrix还提供了pre和post两种类型操作。pre代表前乘,post代表后乘,因为矩阵是不满足交换律的,所以pre和post操作完全不一样

4、旋转中心

view常用操作有三种,平移、缩放、旋转,其中平移最简单,缩放和旋转略复杂,下面以旋转为例说明。
view的旋转中心默认是坐标原点,如果view在旋转前不作任何操作,往往达不到用户想要的效果。往往需要在旋转前将view的中心点移到原点处,再旋转,再将中心点移回原位,这样view将按照用户标明的中心点旋转

    matrix.preTranslate(-centerX, -centerY);
    matrix.postTranslate(centerX, centerY);

上述代码中也正好解释了matrix的pre和post操作。

5、使用Camera与Matrix实现三D容器

先看效果


效果.png

容器中有四个子view,子view大小和父view一样,且是竖直布局,在绘制时,根据容器的滚动距离计算子view的旋转角度。

  • 测量过程

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
      super.onMeasure(widthMeasureSpec, heightMeasureSpec);
      int w = MeasureSpec.getSize(widthMeasureSpec);
      int h = MeasureSpec.getSize(heightMeasureSpec);
      setMeasuredDimension(w, h);
      mWidth = w;
      mHeight = h;
      
      int childW = w - getPaddingLeft() - getPaddingRight();
      int childH = h - getPaddingTop() - getPaddingBottom();
      
      int childWSpec = MeasureSpec.makeMeasureSpec(childW, MeasureSpec.EXACTLY);
      int childHSpec = MeasureSpec.makeMeasureSpec(childH, MeasureSpec.EXACTLY);
      measureChildren(childWSpec, childHSpec);
      //默认此容器滚动到第二个view处
      scrollTo(0, mStartScreen*mHeight);
    }
    

测量过程较简单,但在最后时,让view滚动到第二屏。

  • 布局过程

    protected void onLayout(boolean changed, int l, int t, int r, int b) {
      for (int i = 0; i < getChildCount(); i++) {
          View child = getChildAt(i);
          int left = (getPaddingLeft() + getPaddingRight())/2;
          int top = (getPaddingTop() + getPaddingBottom())/2;
          //子view是竖直排列的,通过camera方式,旋转才看到三D效果
          child.layout(left, top + i*mHeight, left + mWidth, top + (i+1)*mHeight);
      }
    }
    

布局过程也比较简单,竖直排列

  • 绘制过程

    protected void dispatchDraw(Canvas canvas) {
      for (int i = 0; i < getChildCount(); i++) {
          drawScreen(canvas, i, getDrawingTime());
      }
    }
    
    private void drawScreen(Canvas canvas, int index, long time){
      int scrollHeight = mHeight * index;
      int scrollY = getScrollY();
      //view的位置明显看不到,则不需要绘制。比如滚动距离+view高度,还小于view的起始top值,则此view不绘制
      if (scrollHeight > scrollY + mHeight || scrollHeight < scrollY - mHeight) {
          return;
      }
      View child = getChildAt(index);
      //旋转中心点是旋转中的关键。view旋转的中心点都是0,0点,
      //所以需要先将中心点移到0,0点,旋转,再移动回来,看起来像view是在中心点旋转一样
      //如果是滚动距离大于view的top点,那么则y中心点则是view的bottom位置,否则则是top位置
      float centerX = mWidth/2;
      float centerY = (getScrollY() > scrollHeight) ? scrollHeight + mHeight : scrollHeight;
      //计算旋转角度
      float degree = mAngle * (getScrollY() - scrollHeight)/mHeight;
      if (degree > 90 || degree < -90) {
          return;
      }
      canvas.save();
      mCamera.save();
      matrix.reset();
      mCamera.rotateX(degree);
      mCamera.getMatrix(matrix);
      mCamera.restore();
      //移动到旋转中心点
      matrix.preTranslate(-centerX, -centerY);
      matrix.postTranslate(centerX, centerY);
      canvas.concat(matrix);
      drawChild(canvas, child, time);
      canvas.restore();
    }
    

绘制过程有两个难点:

  1. 角度计算
    在measure阶段,容器向下滚动mHeight距离(子view高度),用户看到的是第二个子view,第一个子view应该旋转角度为90度,第二个子view旋转角度为0度,第三个子view旋转角度为-90度。由此得出以下公式

float degree = mAngle * (getScrollY() - scrollHeight)/mHeight;

  1. 旋转中心点计算
    每个子view的旋转中心点是不一样的,为了达到协同的效果,旋转中心y值按如下公式计算

float centerY = (getScrollY() > scrollHeight) ? scrollHeight + mHeight : scrollHeight;

如果是滚动距离大于view的top点,那么则y中心点则是view的bottom位置,否则则是top位置。第一个子view以(w/2,h)为中心点旋转,而第二个子view也是以(w/2,h)为中心点旋转,符合上述公式。旋转中心点的问题需要认真思考,在纸上绘制示意图会帮助理解。

找到旋转中心点以及角度计算,则非常简单了,使用camera绕X轴旋转一定角度,得到对应的matrix,将矩阵应用到canvas上,同时进行旋转时的中心点平移工作,注意相关对象的状态保存及恢复,整个绘制程序则完成。

  • Touch事件处理
    需要view容器完成跟手操作,当手松开时,容器需要回到正确的状态(或到上一页、下一页等)。

子view的旋转角度与容器的滚动距离有关系,因此处理move事件时,滚动容器即可。当手松开时,需要计算容器的下一个状态是什么,容器是显示上一个子view还是显示下一个子view,还是停留在本页内,根据下一个状态让容器滚动适当的距离即可。

  case MotionEvent.ACTION_MOVE:
        mVelocityTracker.addMovement(event);
        float y = event.getY();
        float detal = y - mDownY;
        mDownY = y;
        //当scroller结束滚动时再响应move事件。
        if (mScroller.isFinished()) {
            moveScroll((int)detal);
        }
        break;
  case MotionEvent.ACTION_UP:
        mVelocityTracker.addMovement(event);
        mVelocityTracker.computeCurrentVelocity(1000);
        float vel = mVelocityTracker.getYVelocity();
  //            Log.i(TAG, "vel = " + vel);
        //y速度值为正则是往下滑动,为负则是往上滑动,以500为界定
        if (vel >= 500) {
            moveToNext();
            //滑动到下一屏
        }else if (vel <= -500) {
            //滑动到上一屏
            moveToPre();
        }else {
            //依然在当前屏
            moveNormal();
        }
        mVelocityTracker.clear();
        mVelocityTracker.recycle();
        mVelocityTracker = null;
        break;

当容器显示第一个子view时,用户还在向上翻动容器,为完成循环显示效果,需要将容器的第四个子view在原位置删除,添加到容器的0位置。同理,另一种情况下需要将第一个子view删除,将它添加到容器的3位置,这样用户将看到正方体旋转效果。

  private void moveToPre(){
    addPreView();
    int scrolly = getScrollY();
    //以从第二个view回到第一个view为例,第二个view的滚动距离为scrolly,而第一个view的正常位置则是滚动距离为0
    //所以从第二个view滚动回第一个view的真正距离就是scrolly,因为是向上,所以为负值
    int curY = scrolly + mHeight;
    setScrollY(curY);
    int detal = -(curY - mHeight);
    mScroller.startScroll(0, curY, 0, detal,500);
}

private void moveToNext(){
    addNextView();
    int scrolly = getScrollY();
    int curY = scrolly - mHeight;
    setScrollY(curY);
    int detal = mHeight - (curY);
    mScroller.startScroll(0, curY, 0, detal,500);
}

private void moveNormal(){
    int scrolly = getScrollY();
    int curY = scrolly;
    int detal = mHeight - curY;
    //Log.i(TAG, "cury = " + curY + "  detal = " + detal + "  mheight = " + mHeight);
    mScroller.startScroll(0, curY, 0, detal,500);
    //此处必须刷新,否则computeScroll不会执行
    invalidate();
}

6、后记

关于camera与matrix相关的文章,有不少大神已经写文说明了,本例也差不多,且多有借鉴,例如,http://www.jianshu.com/p/34e0fe5f9e31 ,非为抄袭,只为总结知识,自成知识体系。

代码均已上传到本人的github:https://github.com/okunu/DemoApp ,欢迎访问。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容