本文已授權(quán)微信公眾號:鴻洋(hongyangAndroid)在微信公眾號平臺原創(chuàng)首發(fā)
一. 前言
上次打開掌閱的時候看到書籍打開動畫的效果還不錯,正好最近也在做閱讀器的項目变屁,所以想在項目中實現(xiàn)一下意狠。
二. 思路
講思路之前敞贡,先看一下實現(xiàn)效果吧:- 獲取
RecyclerView
(或GridView
)中的子View里面的ImageView
在屏幕的位置摄职,因為獲取的是Window下的位置,所以Y軸位置取出來還要減去狀態(tài)欄的高度
蛔垢。 - 圖書的封面和內(nèi)容頁(其實是兩個
ImageView
)設(shè)置成剛剛?cè)〕龅?code>子View里面的ImageView的位置和大小迫悠。 - 設(shè)置動畫,這邊縮放動畫的軸心點的計算方式需要注意一下创泄,等下文講解代碼的時候再具體解釋,還有就是利用
Camera
類(非平常的相機(jī)類)實現(xiàn)的打開和關(guān)閉動畫(如果你對Camera不熟悉饭聚,建議先看GcsSloop大佬的這篇Matrix Camera)搁拙。
三. 具體實現(xiàn)
我會在這個過程中一步一步教你如何實現(xiàn)這個效果:
1. 布局
activity_open_book.xml
:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
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"
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" />
<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
,這里就不貼放了兴垦。
2. 動畫
我們只講解旋轉(zhuǎn)動畫,因為旋轉(zhuǎn)動畫中也會涉及縮放動畫探越。想一下扶关,如果想要在界面中實現(xiàn)縮放動畫数冬,我們得找好軸心點,那么拐纱,軸心點的x,y坐標(biāo)如何計算呢揍庄?為了更好的求出坐標(biāo)东抹,我們先來看一張圖:
我們可以得出這樣的公式:
x / pl = vr / pr
,而對于pl
食茎、vr
和pr
馏谨,則有pl = ml + x
,vr = w - x
和pr = pw -pl
哎媚,綜合以上的公式喊儡,最終我們可以得出的x = ml * pw / (pw - w)
拨与,y的坐標(biāo)可以用同樣的方式求得截珍。下面我們來看代碼:
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;
// 旋轉(zhuǎn)中心
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;
// 獲取手機(jī)像素密度 (即dp與px的比例)
scale = context.getResources().getDisplayMetrics().density;
}
@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,"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); //獲取數(shù)值
mValues[6] = mValues[6] / scale; //數(shù)值修正
mValues[7] = mValues[7] / scale; //數(shù)值修正
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);
}
}
/**
* 計算縮放的中心點的橫坐標(biāo)
*
* @param marginLeft 該View距離父布局左邊的距離
* @param parentWidth 父布局的寬度
* @param width View的寬度
* @return 縮放中心點的橫坐標(biāo)
*/
public float calculatePivotX(float marginLeft, float parentWidth, float width) {
return parentWidth * marginLeft / (parentWidth - width);
}
/**
* 計算縮放的中心點的縱坐標(biāo)
*
* @param marginTop 該View頂部距離父布局頂部的距離
* @param parentHeight 父布局的高度
* @param height 子布局的高度
* @return 縮放的中心點的縱坐標(biāo)
*/
public float calculatePivotY(float marginTop, float parentHeight, float height) {
return parentHeight * marginTop / (parentHeight - height);
}
public void reverse() {
reverse = !reverse;
}
}
計算縮放點我們在上面已經(jīng)討論過钱床,這里我們就只看函數(shù)applyTransformation(float interpolatedTime, Transformation t)
埠居,我們先判斷我們當(dāng)前是打開書還是合上書的狀態(tài)(這兩個狀態(tài)使得動畫正好相反)事期,計算好當(dāng)前旋轉(zhuǎn)度數(shù)再取得Camera纸颜,利用camera.rotateY(degrees)
實現(xiàn)書本圍繞Y軸旋轉(zhuǎn),之后拿到我們的矩陣唠倦,圍繞計算出的中心點進(jìn)行縮放涮较。
3. 使用
這一步我們需要將動畫運(yùn)用到我們的界面上去,當(dāng)點擊我們的RecyclerView的時候候齿,我們需要取出RecyclerView中的子View中的ImageView
闺属,在適配器中利用監(jiān)聽器傳出:
public interface OnBookClickListener{
void onItemClick(int pos,View view);
}
接著,我們在OpenBookActivity
中實現(xiàn)OnBookClickListener
接口润匙,省略了一些代碼:
public class OpenBookActivity extends AppCompatActivity implements Animation.AnimationListener,BookAdapter.OnBookClickListener {
private static final String TAG = "OpenBookActivity";
// 一系列變量 此處省略
...
// 記錄View的位置
private int[] location = new int[2];
// 內(nèi)容頁
private ImageView mContent;
// 封面
private ImageView mFirst;
// 縮放動畫
private ContentScaleAnimation scaleAnimation;
// 3D旋轉(zhuǎn)動畫
private Rotate3DAnimation threeDAnimation;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_open_book);
initWidget();
}
private void initWidget() {
...
// 獲取狀態(tài)欄高度
statusHeight = -1;
//獲取status_bar_height資源的ID
int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
//根據(jù)資源ID獲取響應(yīng)的尺寸值
statusHeight = getResources().getDimensionPixelSize(resourceId);
}
initData();
...
}
// 重復(fù)添加數(shù)據(jù)
private void initData() {
for(int i = 0;i<10;i++){
values.add(R.drawable.preview);
}
}
@Override
protected void onRestart() {
super.onRestart();
// 當(dāng)界面重新進(jìn)入的時候進(jìn)行合書的動畫
if(isOpenBook) {
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()) {
// 兩個動畫都結(jié)束的時候再處理后續(xù)操作
if (!isOpenBook) {
isOpenBook = true;
BookSampleActivity.show(this);
} else {
isOpenBook = false;
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);
// 計算當(dāng)前的位置坐標(biāo)
view.getLocationInWindow(location);
int width = view.getWidth();
int height = view.getHeight();
// 兩個ImageView設(shè)置大小和位置
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);
// 設(shè)置內(nèi)容
Bitmap contentBitmap = Bitmap.createBitmap(width,height, Bitmap.Config.ARGB_8888);
contentBitmap.eraseColor(getResources().getColor(R.color.read_theme_yellow));
mContent.setImageBitmap(contentBitmap);
// 設(shè)置封面
Bitmap coverBitmap = BitmapFactory.decodeResource(getResources(),values.get(pos));
mFirst.setImageBitmap(coverBitmap);
// 設(shè)置封面
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()); //設(shè)置插值器
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); //設(shè)置動畫時長
threeDAnimation.setFillAfter(true); //保持旋轉(zhuǎn)后效果
threeDAnimation.setInterpolator(new DecelerateInterpolator());
}
}
第一個重點是復(fù)寫的OnBookClickListener
中的onItemClick
方法巍膘,在該方法中:
- 我們根據(jù)取得的
view
(實際上是子View中的ImageView),計算出當(dāng)前界面的兩個ImageView的位置和大小璃饱。 - 計算縮放參數(shù)和播放動畫的順序肪康,展開動畫,和處理動畫結(jié)束后的事件磷支。
第二個重點是中心回到當(dāng)前界面的時候,合上書的動畫廓潜,就是剛剛的動畫倒過來執(zhí)行,在onRestart()
方法中執(zhí)行呻畸,執(zhí)行完成之后隱藏兩個ImageVIew
悼院。
四. 總結(jié)
總的來說就是Camera
和Animation
的簡單使用,本人水平有限据途,難免不足昨凡,歡迎提出爽醋。
項目地址:Test
Over~
引用:
Matrix Camera