一. 前言
上次打开掌阅的时候看到书籍打开动画的效果还不错,正好最近也在做阅读器的项目,所以想在项目中实现一下。
二. 思路
讲思路之前,先看一下实现效果吧:
书籍打开关闭动画.gif
看完实现效果,我们再来讲一下实现思路:
书籍打开动画的思路.png
获取RecyclerView(或GridView)中的子View里面的ImageView在屏幕的位置,因为获取的是Window下的位置,所以Y轴位置取出来还要减去状态栏的高度。
图书的封面和内容页(其实是两个ImageView)设置成刚刚取出的子View里面的ImageView的位置和大小。
设置动画,这边缩放动画的轴心点的计算方式需要注意一下,等下文讲解代码的时候再具体解释,还有就是利用Camera类(非平常的相机类)实现的打开和关闭动画(如果你对Camera不熟悉,建议先看GcsSloop大佬的这篇Matrix Camera)。
三. 具体实现
我会在这个过程中一步一步教你如何实现这个效果:
1. 布局
activity_open_book.xml:
recycler_item_book.xml:
RecylerVIew中的子布局,其实也就是ImageView和TextView,这里就不贴放了。
2. 动画
我们只讲解旋转动画,因为旋转动画中也会涉及缩放动画。
想一下,如果想要在界面中实现缩放动画,我们得找好轴心点,那么,轴心点的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的坐标可以用同样的方式求得。
下面我们来看代码:
publicclass Rotate3DAnimation extends Animation {privatestaticfinalStringTAG ="Rotate3DAnimation";privatefinalfloatmFromDegrees;privatefinalfloatmToDegrees;privatefinalfloatmMarginLeft;privatefinalfloatmMarginTop;// private final float mDepthZ;privatefinalfloatmAnimationScale;privatebooleanreverse;privateCamera mCamera;// 旋转中心privatefloatmPivotX;privatefloatmPivotY;privatefloatscale=1;// <------- 像素密度publicRotate3DAnimation(Context context,floatmFromDegrees,floatmToDegrees,floatmMarginLeft,floatmMarginTop,floatanimationScale,booleanreverse) {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; } @Overridepublicvoidinitialize(intwidth,intheight,intparentWidth,intparentHeight) {super.initialize(width,height, parentWidth, parentHeight); mCamera =newCamera(); mPivotX = calculatePivotX(mMarginLeft, parentWidth,width); mPivotY = calculatePivotY(mMarginTop, parentHeight,height); Log.i(TAG,"width:"+width+",height:"+height+",pw:"+parentWidth+",ph:"+parentHeight); Log.i(TAG,"中心点x:"+mPivotX+",中心点y:"+mPivotY); } @OverrideprotectedvoidapplyTransformation(floatinterpolatedTime, Transformation t) {super.applyTransformation(interpolatedTime, t);floatdegrees=reverse? mToDegrees + (mFromDegrees - mToDegrees) * interpolatedTime : mFromDegrees + (mToDegrees - mFromDegrees) * interpolatedTime; Matrix matrix = t.getMatrix(); Cameracamera= mCamera;camera.save();camera.rotateY(degrees);camera.getMatrix(matrix);camera.restore();// 修正失真,主要修改 MPERSP_0 和 MPERSP_1float[] mValues =newfloat[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 缩放中心点的横坐标
*/publicfloatcalculatePivotX(floatmarginLeft,floatparentWidth,floatwidth) {returnparentWidth * marginLeft / (parentWidth -width); }/**
* 计算缩放的中心点的纵坐标
*
* @param marginTop 该View顶部距离父布局顶部的距离
* @param parentHeight 父布局的高度
* @param height 子布局的高度
* @return 缩放的中心点的纵坐标
*/publicfloatcalculatePivotY(floatmarginTop,floatparentHeight,floatheight) {returnparentHeight * marginTop / (parentHeight -height); }publicvoidreverse() {reverse= !reverse; }}
计算缩放点我们在上面已经讨论过,这里我们就只看函数applyTransformation(float interpolatedTime, Transformation t),我们先判断我们当前是打开书还是合上书的状态(这两个状态使得动画正好相反),计算好当前旋转度数再取得Camera,利用camera.rotateY(degrees)实现书本围绕Y轴旋转,之后拿到我们的矩阵,围绕计算出的中心点进行缩放。
3. 使用
这一步我们需要将动画运用到我们的界面上去,当点击我们的RecyclerView的时候,我们需要取出RecyclerView中的子View中的ImageView,在适配器中利用监听器传出:
public interface OnBookClickListener{ void onItemClick(int pos,View view);}
接着,我们在OpenBookActivity中实现OnBookClickListener接口,省略了一些代码:
publicclassOpenBookActivityextendsAppCompatActivityimplementsAnimation.AnimationListener,BookAdapter.OnBookClickListener{privatestaticfinalString TAG ="OpenBookActivity";// 一系列变量 此处省略...// 记录View的位置privateint[] location =newint[2];// 内容页privateImageView mContent;// 封面privateImageView mFirst;// 缩放动画privateContentScaleAnimation scaleAnimation;// 3D旋转动画privateRotate3DAnimation threeDAnimation;@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState); setContentView(R.layout.activity_open_book); initWidget(); }privatevoidinitWidget(){ ...// 获取状态栏高度statusHeight = -1;//获取status_bar_height资源的IDintresourceId = getResources().getIdentifier("status_bar_height","dimen","android");if(resourceId >0) {//根据资源ID获取响应的尺寸值statusHeight = getResources().getDimensionPixelSize(resourceId); } initData(); ... }// 重复添加数据privatevoidinitData(){for(inti =0;i<10;i++){ values.add(R.drawable.preview); } }@OverrideprotectedvoidonRestart(){super.onRestart();// 当界面重新进入的时候进行合书的动画if(isOpenBook) { scaleAnimation.reverse(); threeDAnimation.reverse(); mFirst.clearAnimation(); mFirst.startAnimation(threeDAnimation); mContent.clearAnimation(); mContent.startAnimation(scaleAnimation); } }@OverridepublicvoidonAnimationEnd(Animation animation){if(scaleAnimation.hasEnded() && threeDAnimation.hasEnded()) {// 两个动画都结束的时候再处理后续操作if(!isOpenBook) { isOpenBook =true; BookSampleActivity.show(this); }else{ isOpenBook =false; mFirst.clearAnimation(); mContent.clearAnimation(); mFirst.setVisibility(View.GONE); mContent.setVisibility(View.GONE); } } }@OverridepublicvoidonItemClick(intpos,View view){ mFirst.setVisibility(View.VISIBLE); mContent.setVisibility(View.VISIBLE);// 计算当前的位置坐标view.getLocationInWindow(location);intwidth = view.getWidth();intheight = 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); }// 初始化动画privatevoidinitAnimation(View view){floatviewWidth = view.getWidth();floatviewHeight = view.getHeight(); DisplayMetrics displayMetrics =newDisplayMetrics(); getWindow().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);floatmaxWidth = displayMetrics.widthPixels;floatmaxHeight = displayMetrics.heightPixels;floathorScale = maxWidth / viewWidth;floatverScale = maxHeight / viewHeight;floatscale = horScale > verScale ? horScale : verScale; scaleAnimation =newContentScaleAnimation(location[0], location[1], scale,false); scaleAnimation.setInterpolator(newDecelerateInterpolator());//设置插值器scaleAnimation.setDuration(1000); scaleAnimation.setFillAfter(true);//动画停留在最后一帧scaleAnimation.setAnimationListener(OpenBookActivity.this); threeDAnimation =newRotate3DAnimation(OpenBookActivity.this, -180,0, location[0], location[1], scale,true); threeDAnimation.setDuration(1000);//设置动画时长threeDAnimation.setFillAfter(true);//保持旋转后效果threeDAnimation.setInterpolator(newDecelerateInterpolator()); }}
第一个重点是复写的OnBookClickListener中的onItemClick方法,在该方法中:
我们根据取得的view(实际上是子View中的ImageView),计算出当前界面的两个ImageView的位置和大小。
计算缩放参数和播放动画的顺序,展开动画,和处理动画结束后的事件。
第二个重点是中心回到当前界面的时候,合上书的动画,就是刚刚的动画倒过来执行,在onRestart()方法中执行,执行完成之后隐藏两个ImageVIew。
四. 总结
总的来说就是Camera和Animation的简单使用,本人水平有限,难免不足,欢迎提出。
欢迎工作一到五年的Java工程师朋友们加入Java技术交流:585550789