一. 前言
上次打开掌阅的时候看到书籍打开动画的效果还不错,正好最近也在做阅读器的项目,所以想在项目中实现一下。
二. 思路
讲思路之前,先看一下实现效果吧:
书籍打开关闭动画.gif
看完实现效果,我们再来讲一下实现思路:
书籍打开动画的思路.png
获取RecyclerView(或GridView)中的子View里面的ImageView在屏幕的位置,因为获取的是Window下的位置,所以Y轴位置取出来还要减去状态栏的高度。
图书的封面和内容页(其实是两个ImageView)设置成刚刚取出的子View里面的ImageView的位置和大小。
设置动画,这边缩放动画的轴心点的计算方式需要注意一下,等下文讲解代码的时候再具体解释,还有就是利用Camera类(非平常的相机类)实现的打开和关闭动画(如果你对Camera不熟悉,建议先看GcsSloop大佬的这篇Matrix Camera)。
三. 具体实现
我会在这个过程中一步一步教你如何实现这个效果:
- 布局
activity_open_book.xml:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.activity.OpenBookActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycle"
br/>xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.activity.OpenBookActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycle"
android:layout_width="match_parent"<ImageView
android:id="@+id/img_content"
br/>android:layout_height="match_parent"/>
<ImageView
android:id="@+id/img_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:contentDescription="@string/app_name" />br/><ImageView
android:id="@+id/img_first"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:visibility="gone"
android:contentDescription="@string/app_name" />
</RelativeLayout>
recycler_item_book.xml:
RecylerVIew中的子布局,其实也就是ImageView和TextView,这里就不贴放了。
- 动画
我们只讲解旋转动画,因为旋转动画中也会涉及缩放动画。
想一下,如果想要在界面中实现缩放动画,我们得找好轴心点,那么,轴心点的x,y坐标如何计算呢?
为了更好的求出坐标,我们先来看一张图:
缩放讲解图.png
我们可以得出这样的公式:x / pl = vr / pr,而对于pl、vr和pr,则有pl = ml + x,vr = w - x和pr = pw -pl,综合以上的公式,最终我们可以得出的x = ml * pw / (pw - w),y的坐标可以用同样的方式求得。
下面我们来看代码:
public class Rotate3DAnimation extends Animation {
private static final String TAG = "Rotate3DAnimation";
private final float mFromDegrees;
private final float mToDegrees;
private final float mMarginLeft;
private final float mMarginTop;
// private final float mDepthZ;
private final float mAnimationScale;
private boolean reverse;
private Camera mCamera;
// 旋转中心
private float mPivotX;
private float mPivotY;
private float scale = 1; // <------- 像素密度
public Rotate3DAnimation(Context context, float mFromDegrees, float mToDegrees, float mMarginLeft, float mMarginTop,
float animationScale, boolean reverse) {
this.mFromDegrees = mFromDegrees;
this.mToDegrees = mToDegrees;
this.mMarginLeft = mMarginLeft;
this.mMarginTop = mMarginTop;
this.mAnimationScale = animationScale;
this.reverse = reverse;
// 获取手机像素密度 (即dp与px的比例)
scale = context.getResources().getDisplayMetrics().density;br/>}
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
mCamera = new Camera();
mPivotX = calculatePivotX(mMarginLeft, parentWidth, width);
mPivotY = calculatePivotY(mMarginTop, parentHeight, height);Log.i(TAG,"中心点x:"+mPivotX+",中心点y:"+mPivotY);
}
@Override
br/>Log.i(TAG,"width:"+width+",height:"+height+",pw:"+parentWidth+",ph:"+parentHeight);
Log.i(TAG,"中心点x:"+mPivotX+",中心点y:"+mPivotY);
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
super.applyTransformation(interpolatedTime, t);
float degrees = reverse ? mToDegrees + (mFromDegrees - mToDegrees) interpolatedTime : mFromDegrees + (mToDegrees - mFromDegrees) interpolatedTime;
Matrix matrix = t.getMatrix();
Camera camera = mCamera;
camera.save();
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); //重新赋值
if (reverse) {
matrix.postScale(1 + (mAnimationScale - 1) interpolatedTime, 1 + (mAnimationScale - 1) interpolatedTime,
mPivotX - mMarginLeft, mPivotY - mMarginTop);
} else {
matrix.postScale(1 + (mAnimationScale - 1) (1 - interpolatedTime), 1 + (mAnimationScale - 1) (1 - interpolatedTime),
mPivotX - mMarginLeft, mPivotY - mMarginTop);
}
}
/**
- 计算缩放的中心点的横坐标
- @param marginLeft 该View距离父布局左边的距离
- @param parentWidth 父布局的宽度
- @param width View的宽度
- @return 缩放中心点的横坐标
/
public float calculatePivotX(float marginLeft, float parentWidth, float width) {
return parentWidth marginLeft / (parentWidth - width);
}
/** - 计算缩放的中心点的纵坐标
- @param marginTop 该View顶部距离父布局顶部的距离
- @param parentHeight 父布局的高度
- @param height 子布局的高度
- @return 缩放的中心点的纵坐标
/
public float calculatePivotY(float marginTop, float parentHeight, float height) {
return parentHeight marginTop / (parentHeight - height);
}
public void reverse() {
reverse = !reverse;
}
}
计算缩放点我们在上面已经讨论过,这里我们就只看函数applyTransformation(float interpolatedTime, Transformation t),我们先判断我们当前是打开书还是合上书的状态(这两个状态使得动画正好相反),计算好当前旋转度数再取得Camera,利用camera.rotateY(degrees)实现书本围绕Y轴旋转,之后拿到我们的矩阵,围绕计算出的中心点进行缩放。
- 使用
这一步我们需要将动画运用到我们的界面上去,当点击我们的RecyclerView的时候,我们需要取出RecyclerView中的子View中的ImageView,在适配器中利用监听器传出:
public interface OnBookClickListener{
void onItemClick(int pos,View view);
}
接着,我们在OpenBookActivity中实现OnBookClickListener接口,省略了一些代码:
public class OpenBookActivity extends AppCompatActivity implements Animation.AnimationListener,BookAdapter.OnBookClickListener {
private static final String TAG = "OpenBookActivity";
// 一系列变量 此处省略
...
// 记录View的位置
private int[] location = new int[2];
// 内容页
private ImageView mContent;
// 封面
private ImageView mFirst;
// 缩放动画
private ContentScaleAnimation scaleAnimation;
// 3D旋转动画
private Rotate3DAnimation threeDAnimation;br/>@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_open_book);
initWidget();
}
private void initWidget() {
...
// 获取状态栏高度
statusHeight = -1;
//获取status_bar_height资源的ID
int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
//根据资源ID获取响应的尺寸值
statusHeight = getResources().getDimensionPixelSize(resourceId);
}
initData();
...
}
// 重复添加数据
private void initData() {
for(int i = 0;i<10;i++){br/>values.add(R.drawable.preview);
}
}
@Override
protected void onRestart() {
super.onRestart();
// 当界面重新进入的时候进行合书的动画
if(isOpenBook) {br/>scaleAnimation.reverse();
threeDAnimation.reverse();
mFirst.clearAnimation();
mFirst.startAnimation(threeDAnimation);
mContent.clearAnimation();
mContent.startAnimation(scaleAnimation);
}
}
@Override
public void onAnimationEnd(Animation animation) {
if(scaleAnimation.hasEnded() && threeDAnimation.hasEnded()) {
// 两个动画都结束的时候再处理后续操作
if (!isOpenBook) {
isOpenBook = true;
BookSampleActivity.show(this);
} else {
isOpenBook = false;br/>mFirst.clearAnimation();
mContent.clearAnimation();
mFirst.setVisibility(View.GONE);
mContent.setVisibility(View.GONE);
}
}
}
@Override
public void onItemClick(int pos,View view) {
mFirst.setVisibility(View.VISIBLE);
mContent.setVisibility(View.VISIBLE);
// 计算当前的位置坐标
view.getLocationInWindow(location);
int width = view.getWidth();
int height = view.getHeight();
// 两个ImageView设置大小和位置
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) mFirst.getLayoutParams();
params.leftMargin = location[0];
params.topMargin = location[1] - statusHeight;
params.width = width;
params.height = height;
mFirst.setLayoutParams(params);
mContent.setLayoutParams(params);
// 设置内容
Bitmap contentBitmap = Bitmap.createBitmap(width,height, Bitmap.Config.ARGB_8888);
contentBitmap.eraseColor(getResources().getColor(R.color.read_theme_yellow));
mContent.setImageBitmap(contentBitmap);
// 设置封面
Bitmap coverBitmap = BitmapFactory.decodeResource(getResources(),values.get(pos));
mFirst.setImageBitmap(coverBitmap);
// 设置封面
initAnimation(view);
Log.i(TAG,"left:"+mFirst.getLeft()+"top:"+mFirst.getTop());
mContent.clearAnimation();
mContent.startAnimation(scaleAnimation);
mFirst.clearAnimation();
mFirst.startAnimation(threeDAnimation);
}
// 初始化动画
private void initAnimation(View view) {
float viewWidth = view.getWidth();
float viewHeight = view.getHeight();
DisplayMetrics displayMetrics = new DisplayMetrics();
getWindow().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
float maxWidth = displayMetrics.widthPixels;
float maxHeight = displayMetrics.heightPixels;
float horScale = maxWidth / viewWidth;
float verScale = maxHeight / viewHeight;
float scale = horScale > verScale ? horScale : verScale;
scaleAnimation = new ContentScaleAnimation(location[0], location[1], scale, false);
scaleAnimation.setInterpolator(new DecelerateInterpolator()); //设置插值器
scaleAnimation.setDuration(1000);
scaleAnimation.setFillAfter(true); //动画停留在最后一帧
scaleAnimation.setAnimationListener(OpenBookActivity.this);
threeDAnimation = new Rotate3DAnimation(OpenBookActivity.this, -180, 0
, location[0], location[1], scale, true);
threeDAnimation.setDuration(1000); //设置动画时长
threeDAnimation.setFillAfter(true); //保持旋转后效果
threeDAnimation.setInterpolator(new DecelerateInterpolator());
}
}
第一个重点是复写的OnBookClickListener中的onItemClick方法,在该方法中:
我们根据取得的view(实际上是子View中的ImageView),计算出当前界面的两个ImageView的位置和大小。
计算缩放参数和播放动画的顺序,展开动画,和处理动画结束后的事件。
第二个重点是中心回到当前界面的时候,合上书的动画,就是刚刚的动画倒过来执行,在onRestart()方法中执行,执行完成之后隐藏两个ImageVIew。
四. 总结
总的来说就是Camera和Animation的简单使用,本人水平有限,难免不足,欢迎提出。
欢迎工作一到五年的Java工程师朋友们加入Java技术交流:585550789
转载于:https://blog.51cto.com/14226273/2362971