仿掌阅app打开书籍动画效果
最近看到了掌阅app的打开动画,感觉不错,模仿了一下。首先看一下效果。
这个效果通过打开书籍时的效果Camera来实现二维来模仿三维动画(View没有厚度是伪3D),并且放大画布来实现的。
什么是Camera
我们知道canvas用的是View的坐标系一个二维的坐标系。但是camera用的是三维的坐标系,x轴与二维坐标系相同,y轴与二维坐标系相反,z轴是朝着view里边的。
在z轴会存在一个相机,通过相机来产生投影,就是看到的效果。相机的默认位置是-8(英寸),可以通过setLocation调整相机的距离来实现最终结果的大小。下图为旋转后的投影出的图形
相机的投影.png这里Camera的轴心是canvas的坐标原点,我们可以利用canvas.translate或者Matrix 平移到中心,然后把投影之后的图形挪回来。
Camera常用方法
Camera一般是配合Matrix以及canvas使用。以下就是Camera最常见的方法
计算当前状态下单矩阵对应的状态,并将计算后的矩阵应用到指定的canvas上。
void applyToCanvas (Canvas canvas)
计算当前状态下矩阵对应的状态,并将计算后的矩阵赋值给参数matrix。
void getMatrix (Matrix matrix)
Camera可以利用自己的三维坐标系进行旋转。
Camera的旋转.jpg
void rotate (float x, float y, float z);
// 控制View绕单个坐标轴旋转
void rotateX (float deg);
void rotateY (float deg);
void rotateZ (float deg);
当然Camera也有自己的平移效果不过很少用到,前边说过x轴平移与二维坐标系相同,y轴相反,移动z轴坐标,当View和相机在同一条直线上时相当于缩放的效果,z轴平移的远,看到的效果越小。如果不在一条直线上,在缩小的同时也在不断接近摄像机在屏幕投影位置(通常情况下为Z轴,在平面上表现为接近坐标原点)。相反,当View接近摄像机的时候,View在放大的同时会远离摄像机在屏幕投影位置。
在z轴会存在一个相机,通过相机来产生投影,就是看到的效果。相机的默认位置是-8(英寸),可以通过setLocation调整相机的距离来实现最终结果的大小。下图为旋转后的投影出的图形
当然Camera也有自己的平移效果不过很少用到,前边说过x轴平移与二维坐标系相同,y轴相反,移动z轴坐标,当View和相机在同一条直线上时相当于缩放的效果,z轴平移的远,看到的效果越小。如果不在一条直线上,在缩小的同时也在不断接近摄像机在屏幕投影位置(通常情况下为Z轴,在平面上表现为接近坐标原点)。相反,当View接近摄像机的时候,View在放大的同时会远离摄像机在屏幕投影位置。
image.png假设大矩形是手机屏幕,白色小矩形是View,摄像机位于屏幕左上角,请注意上面View与摄像机的距离以及下方View的大小以及距离左上角(摄像机在屏幕投影位置)的距离。
实现打开书籍动画
这里利用自定义view的方式来处理,初始化数据,camera通过setLocation调整相机的位置,但是Camera 的位置单位是英寸,英寸和像素的换算单位在 Skia 中被写成了72 像素,8 x 72 = 576,所以它的默认位置是 (0, 0, -576)。所以这里需要做一个位置的适配。
public OpenBookView(Context context) {
super(context);
}
public OpenBookView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
camera = new Camera();
pageBackgroundPaint = new Paint();
pageBackgroundPaint.setColor(0xffFFD700);
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
float newZ = -displayMetrics.density * 6;
camera.setLocation(0, 0, newZ);
refreshData();
}
接下来看打开书籍的动画,很简单设置一个动画系数。
openBookview.setVisibility(View.VISIBLE);
Bitmap bitmap = ((BitmapDrawable) bookshlefBook.getBackground()).getBitmap();
openBookview.openAnimation(bitmap, bookshlefBook.getLeft(), bookshlefBook.getTop(), bookshlefBook.getWidth(), bookshlefBook.getHeight(), new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
Intent intent = new Intent(MainActivity.this, BrowserBookActivity.class);
startActivityForResult(intent, RESULT_FIRST_USER);
overridePendingTransition(0, 0);
}
});
public void openAnimation(Bitmap coverBitmap, float left, float top, float width, float height, AnimatorListenerAdapter adapter) {
this.coverBitmap = coverBitmap;
coverWidth = width;
coverHeight = height;
coverLeft = left;
coverTop = top;
isOpen = true;
refreshData();
startAnim(adapter);
}
private void startAnim(AnimatorListenerAdapter adapter) {
ObjectAnimator animator = ObjectAnimator.ofFloat(this, "scaleX", 0f, 1f);
animator.setDuration(DURATION);
animator.addListener(adapter);
animator.start();
}
@Keep
public void setScaleX(float scale) {
if (isOpen) {
this.scale = scale;
} else {
this.scale = 1f - scale;
}
postInvalidate();
}
自定义view
最后核心绘制代码,本质是将画布扩大并且使用camera旋转y轴进行投影操作。首先通过动画的scale平移当前的画布,然后根据bitmap的像素值和总大小判断扩大的比例,通过canvas.drawRect(bookRect, pageBackgroundPaint)绘制对应的黄色画纸,通过canvas.translate(0, -coverHeight / 2);调整camera轴心。从而实现效果。
canvas.save();
canvas.translate(coverLeft - coverLeft * scale, coverTop - coverTop * scale);
float scaleX = viewScaleWidth + (maxScaleWidth - viewScaleWidth) * scale;
float scaleY = viewScaleHeight + (maxScaleHeight - viewScaleHeight) * scale;
Log.e("scaleX", "" + scaleX);
canvas.scale(scaleX, scaleY);
canvas.drawRect(bookRect, pageBackgroundPaint);
camera.save();
canvas.save();
canvas.translate(0, -coverHeight / 2);
camera.rotateY(-180 * scale);
camera.applyToCanvas(canvas);
canvas.translate(0, coverHeight / 2);
canvas.drawBitmap(coverBitmap, 0, 0, pageBackgroundPaint);
camera.restore();
canvas.restore();
canvas.restore();
最后说一下,bitmap显示的比例,因为从原始书籍比例与达到最终效果的比例,在这里viewScaleWidth和viewScaleHeight bitmap和外部控件的比例,maxScaleWidth和maxScaleHeight是最终达到效果的比例,也就是说画布在扩大的时候按照原始书籍通过动画系数一直到最终比例。
private void refreshData() {
if (coverBitmap == null) {
return;
}
viewScaleWidth = coverWidth / coverBitmap.getWidth();
viewScaleHeight = coverHeight / coverBitmap.getHeight();
bookRect = new Rect(0, 0, coverBitmap.getWidth(), coverBitmap.getHeight());
resetWidthHeight();
}
private void resetWidthHeight() {
if (coverBitmap != null) {
maxScaleWidth = (float) width / coverBitmap.getWidth();
maxScaleHeight = (float) height / coverBitmap.getHeight();
}
}
float scaleX = viewScaleWidth + (maxScaleWidth - viewScaleWidth) * scale;
float scaleY = viewScaleHeight + (maxScaleHeight - viewScaleHeight) * scale;
canvas.scale(scaleX, scaleY);
链接地址
github链接