一、前言
之前写过一篇《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
衍生效果大家可以自行实现。