Android Canvas 3D视图构建

一、前言

之前写过一篇《Android Canvas 3D视图探索》,在那篇内容中,我们尝试使用Camera去构建3D视觉,但是由于Canvas 是2D体系,必然3D只能投影到2D平面,且最终坍塌成平面图,有点像三体世界的降维打击(当然我的方法可能也有问题),因此本篇将抛弃Camera,通过2D平面去构建三维图形,而不是直接使用Camera构建三维图形。

当然你可能觉得(x,y,z)坐标怎么表示,我们可以将其想象成(x,y,1),z轴用来做透视除法,类似open gl中的(x,y,z,w) ,其中w用来做透视除法,当然本篇也没有用到z轴,主要是时间关系,懒得处理原理屏幕的画面大小了。

透视除法: 类似(x,y,z)转嫁为(x/z,y/z)的一种投影

构建原理:

  • 二维平面看三维平面是动态的,二维无法向三位形成闭合Path,因此我们采用运动方程实现点与点之间的连接,类似open gl 中的三角扇。

  • 图元组合,主要原因是只有通过这种方式才能生成闭合Path,当然本篇没有将点连成线,而是写了一些数字

效果图

很炫吧,当然我觉得你可能喜欢下面的

或者下面的

当然,上面的图片我们没有做透视除法,因此原理屏幕的元素稍微有些大,但并不影响你拿去做扩展,比如开发循环View。

二、模型

2.1 、关于矩阵

我们知道2D平面的矩阵是

objectivec 复制代码
MSCALE_X,MSKEW_X,MTRANS_X,
MSKEW_Y,MSCALE_Y,MTRANS_Y,
MPERSP_0,MPERSP_2,MPERSP_2

实际上MSCALE_X和MSCALE_Y 替换成 sinθ或者cosθ不就意味着可以旋转了么 ?答案是肯定的,但是和旋转矩阵不一样了

注意了,在Canvas 2D种没有Z轴,如果那公式去套代码可能有问题,比如你知道Camera.rotateX创建的矩阵Z分量没有任何变化,具体我也不知道为什么,知道的可能在评论区留言。

2.2 矩阵转换为公式

我们使用矩阵与向量相乘操作还是太麻烦,那能不能有简单的方法呢?

如果绕Y轴旋转,只需要修改X分量即可

  • Y = N (N 为定值)
  • X = Radius * Sinθ 或者Radius * Cosθ

反过来绕X轴旋转,只需要修改Y分量

  • X = N (N 为定值)

  • Y = Radius * Sinθ或者 Radius * Cosθ

进一步,为了让原理屏幕的有一定的夹角,N值可以乘以Cosθ

再进一步,为了让原理屏幕的变小,X/Z,Y/Z即可,当然本篇没有做这个。

三、代码实现

核心代码

ini 复制代码
        float degree = 360f;
        char[] text = "1234567890".toCharArray();
        int num = text.length;

        //5组平分半径
        int group = 5;
        float v = radius / group;  //y位置
        // y = R - v*i
        //  Math.sqrt(radius*radius - v*v)  推出  x轴小圆 半径


        for (int k = 0; k < group; k++) {
            for (int i = 0; i < num; i++) {
                float ty = radius - (radius / group) * (k + 1);  //平分最大圆半径
                float r = (float) Math.sqrt(radius * radius - ty * ty);  //勾股定理求出小圆的半径
                //cy是Y坐标,50是给的一定的倾角,看起来更加动态
                 float cy = (float) (-ty - 50 * Math.cos(Math.toRadians(degree / num * i + rotate)));
/               //通过半径计算出平面小圆的在旋转角度的半径
                 float cx = (float) (r * Math.sin(Math.toRadians(degree / num * i + rotate)));
                canvas.drawText(text, num - i - 1, 1, cx, cy, mCommonPaint);
                if (k < (group - 1)) {
                    canvas.drawText(text, num - i - 1, 1, cx, -cy, mCommonPaint);
                }
            }
        }

全部代码

ini 复制代码
public class MatrixCircleView extends BaseView {
    private static final String TAG = "MatrixLearningView";
    private final DisplayMetrics mDM;
    private final Bitmap mBitmap;
    private TextPaint mCommonPaint;
    Matrix matrix = new Matrix();
    private float[] vertex = new float[9];
    private float padding = 40F;

    private Random random = new Random();

    private float rotate = 0f;

    public MatrixCircleView(Context context) {
        this(context, null);
    }

    public MatrixCircleView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MatrixCircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mDM = getResources().getDisplayMetrics();
        initPaint();
        mBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.mm_010);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        if (widthMode != MeasureSpec.EXACTLY) {
            widthSize = mDM.widthPixels / 2;
        }

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        if (heightMode != MeasureSpec.EXACTLY) {
            heightSize = widthSize / 2;
        }
        setMeasuredDimension(widthSize, heightSize);
    }

    @Override
    protected void init(Context context) {

    }


    public float dp2px(float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
    }

    public float sp2px(float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, dp, mDM);
    }


    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
    }

    RectF bitmapRect = new RectF();

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        drawCoordinate(canvas);
        int width = getWidth();
        int height = getHeight();
        if (width <= padding || height <= padding) {
            return;
        }

        float radius = Math.min(width, height) / 2F - padding * 2;
        int count = canvas.save();

        vertex[0] = 1;
        vertex[1] = 0;
        vertex[2] = width / 2F;

        vertex[3] = 0;
        vertex[4] = 1;
        vertex[5] = height / 2F;

        vertex[6] = 0;
        vertex[7] = 0;
        vertex[8] = 1;
        matrix.reset();
        matrix.setValues(vertex);
        canvas.concat(matrix);


        float degree = 360f;
        char[] text = "1234567890".toCharArray();
        int num = text.length;

        //5组平分半径
        int group = 5;
        float v = radius / group;  //y位置
        // y = R - v*i
        //  Math.sqrt(radius*radius - v*v)  = >  x 半径


        for (int k = 0; k < group; k++) {
            for (int i = 0; i < num; i++) {
                float ty = radius - (radius / group) * (k + 1);
                float r = (float) Math.sqrt(radius * radius - ty * ty);  //勾股定理求出小圆的半径
                float cy = (float) (-ty - 50 * Math.cos(Math.toRadians(degree / num * i + rotate)));
                float cx = (float) (r * Math.sin(Math.toRadians(degree / num * i + rotate)));
                canvas.drawText(text, num - i - 1, 1, cx, cy, mCommonPaint);
                if (k < (group - 1)) {
                    canvas.drawText(text, num - i - 1, 1, cx, -cy, mCommonPaint);
                }
            }
        }

//        下面是画图片的
//
//        float ty = 0;
//        float r = (float) Math.sqrt(radius * radius - ty * ty);  //勾股定理求出小圆的半径
//        for (int i = 0; i < num; i++) {
//            float cy = (float) (-ty - 90 * Math.cos(Math.toRadians(degree / num * i + rotate)));
//            float cx = (float) (r * Math.sin(Math.toRadians(degree / num * i + rotate)));
//            bitmapRect.set(cx - 50, cy - 50, cx + 50, cy + 50);
//            canvas.drawBitmap(mBitmap,null,bitmapRect,mCommonPaint);
//        }

        //下面是画辅助线的

        //     mCommonPaint.setStyle(Paint.Style.STROKE);
        //   canvas.drawCircle(0,0,radius,mCommonPaint);


        if (rotate > 360f) {
            rotate = rotate - 360;
        }
        rotate += 5;
        canvas.restoreToCount(count);
        postInvalidateDelayed(32);
    }

    private void initPaint() {
        //否则提供给外部纹理绘制
        mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mCommonPaint.setAntiAlias(true);
        mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
        mCommonPaint.setStrokeWidth(dp2px(1));
        mCommonPaint.setTextSize(sp2px(12));
        mCommonPaint.setColor(argb(random.nextFloat(), random.nextFloat(), random.nextFloat()));
    }


    public static int argb(float red, float green, float blue) {
        return ((int) (1 * 255.0f + 0.5f) << 24) |
                ((int) (red * 255.0f + 0.5f) << 16) |
                ((int) (green * 255.0f + 0.5f) << 8) |
                (int) (blue * 255.0f + 0.5f);
    }

}

四、总结

本篇到这里基本结束了,这是个骨架代码,后续可以衍生出更多效果,当然本次是X轴旋转,Z轴旋转没有实现,不过原理是相同的。

衍生效果:

  • 旋转木马

  • 旋转相册

  • 地球

  • 无限循环菜单

  • 裸眼3D

  • css动画

衍生效果大家可以自行实现。

相关推荐
LuciferHuang41 分钟前
震惊!三万star开源项目竟有致命Bug?
前端·javascript·debug
GISer_Jing42 分钟前
前端实习总结——案例与大纲
前端·javascript
天天进步20151 小时前
前端工程化:Webpack从入门到精通
前端·webpack·node.js
何盖(何松影)2 小时前
Android T startingwindow使用总结
android
姑苏洛言2 小时前
编写产品需求文档:黄历日历小程序
前端·javascript·后端
知识分享小能手2 小时前
Vue3 学习教程,从入门到精通,使用 VSCode 开发 Vue3 的详细指南(3)
前端·javascript·vue.js·学习·前端框架·vue·vue3
姑苏洛言2 小时前
搭建一款结合传统黄历功能的日历小程序
前端·javascript·后端
小李飞飞砖3 小时前
Android 依赖注入框架详解
android
SUNxuetian3 小时前
【Android Studio】升级AGP-8.6.1,Find Usage对Method失效的处理方法!
android·ide·gradle·android studio·安卓