這篇文章在說什么冗恨?
3d翻頁部分其實比較簡單答憔,因為Google在ApiDemos里給了動畫部分的實現(xiàn)源碼。麻煩的是FragmentTransaction.setCustomAnimations如何設(shè)置一個特殊的不是通過xml創(chuàng)建的Animation掀抹。本文給了解決方法虐拓,以及是如何發(fā)現(xiàn)這個解決辦法的。
這個地址有完整的源碼傲武。
https://github.com/aesean/Rotate3d
源碼包含:
- 如何讓Fragment實現(xiàn)翻轉(zhuǎn)
- 如何讓View實現(xiàn)翻轉(zhuǎn)
- 以及一個跟Google的Rotate3dAnimation效果一摸一樣的Animator實現(xiàn)蓉驹。
正文
最近遇到一個需求,是某個界面有兩種顯示樣式揪利。然后有按鈕可以在這兩種樣式之間隨意切換态兴。大致有點像下面圖中效果。
最終效果差不多就是類似這個圖疟位。那假如第一次看到這個效果圖瞻润,思考下,我們應(yīng)該如何實現(xiàn)呢甜刻?
實現(xiàn)思路
雖然圖有左右兩個绍撞,實際其實只要實現(xiàn)其中一個另一個其實就做同樣實現(xiàn)就可以了。下面所有討論都只針對左半部分罢吃。
圖中效果就兩部分組成:View+動畫楚午。
- View
View的話用Fragment實現(xiàn)就OK(當然ViewGroup嵌套也能做到,但為了更方便的封裝復用尿招,顯然Fragment會更好)矾柜。 - 動畫
然后動畫的話可以直接用Fragment(V4)的CustomAnimations來實現(xiàn)阱驾。 - Rotate3dAnimation
剩下一個唯一難點,CustomAnimations是個Animation動畫怪蔑,那這個效果如何實現(xiàn)呢黔州?如果你看過或者用過Google在AndroidSDK中附帶的ApiDemos的話戳玫,有個類完全就是一摸一樣的效果莫绣。
https://android.googlesource.com/platform/development/+/master/samples/ApiDemos/src/com/example/android/apis/animation/Rotate3dAnimation.java
再梳理下思路材泄。左右兩部分都用Fragment實現(xiàn)。然后左邊是兩個Fragment(正面一個背面一個)弓坞,右邊也是兩個隧甚。然后需要切換的時候就通過transaction.setCustomAnimations設(shè)置切換需要的動畫,然后通過show/hide(根據(jù)你需要也可以add/remove)來控制Fragment的顯示與消失渡冻。這樣一來動畫效果完全與Fragment解耦戚扳,相當于是任意Fragment都可以使用,似乎沒什么問題族吻。
開始實現(xiàn)
- 定義Fragment
先定義好自己的Fragment帽借。左邊需要兩個Fragment,假如就叫:FragmentA和FragmentB超歌,分別對應(yīng)正面和背面砍艾。 - 控制顯示與消失
控制顯示與消失,可以用add/remove(每次會重新創(chuàng)建Fragment實例)巍举,也可以使用show/hide(會復用Fragment實例)脆荷。當然這里我們肯定用show/hide了。在Activity中你可能會寫出類似下面的代碼禀综。
private static final String FRAGMENT_TAG_A = "FRAGMENT_TAG_A";
private static final String FRAGMENT_TAG_B = "FRAGMENT_TAG_B";
public void showFragmentA() {
showFragment(FRAGMENT_TAG_A, FRAGMENT_TAG_B);
}
public void showFragmentB() {
showFragment(FRAGMENT_TAG_B, FRAGMENT_TAG_A);
}
public void showFragment(String showTag, String hideTag) {
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
// 設(shè)置動畫
transaction.setCustomAnimations(enterId ?, exitId ?);
Fragment fragment = getSupportFragmentManager().findFragmentByTag(showTag);
if (fragment == null) {
// 沒有找到表示沒有被創(chuàng)建過
fragment = new FragmentA();
// 直接add
transaction.add(R.id.fragment_content, fragment, showTag);
} else {
// 找到了简烘,表示已經(jīng)被add了苔严,所以直接show
transaction.show(fragment);
}
fragment = getSupportFragmentManager().findFragmentByTag(hideTag);
if (fragment != null) {
// 找到了定枷,直接hide
transaction.hide(fragment);
}
transaction.commit();
}
所有代碼都非常順利,唯獨
transaction.setCustomAnimations(enterId ?, exitId ?);
出問題了届氢,我們這里有個Google寫好的Rotate3dAnimation欠窒,但這里只能指定RId,也就是說這里只能指定xml定義的Animation退子。而且沒有任何重載方法可以設(shè)置Animation實例岖妄。
怎么設(shè)置自定義Animation實例
怎么辦呢?先看下setCustomAnimations注釋怎么寫的寂祥。
/**
* Set specific animation resources to run for the fragments that are
* entering and exiting in this transaction. These animations will not be
* played when popping the back stack.
*/
解釋的非常清楚荐虐,然并卵。
然后丸凭,最直接的就是把Fragment相關(guān)源碼讀一遍福扬,看下整個處理過程腕铸,看看Google有沒有留下什么方式能做到自定義Animation。
但Fragment源碼代碼量還是非常大的铛碑,如果你之前完全沒有細讀過Fragment實現(xiàn)狠裹,那效率會比較低,這里不急著看Fragment實現(xiàn)代碼汽烦,我們來猜測下Google這里是如何通過Rid來實現(xiàn)切換動畫的涛菠。
- 雖然這時候還沒細讀Fragment源碼,但這個轉(zhuǎn)場動畫撇吞,最終一定是把Animation作用到View上俗冻,而且代碼非常可能就是view.startAnimation牍颈。
- transaction.setCustomAnimations之后言疗,應(yīng)該是保存了動畫資源Id,然后再某個時候把xml加載成Animation颂砸。加載xml定義的Animation基本跑不了肯定就是AnimationUtils.loadAnimation
這時候最簡單的噪奄,先去Fragment類源碼中搜下“.startAnimation”和“AnimationUtils.loadAnimation”,非常遺憾都沒有找到人乓。
不要緊勤篮,F(xiàn)ragment有三個很重要的類:Fragment、FragmentTransaction和FragmentManager色罚。分別去另外兩個實現(xiàn)類中搜下碰缔。FragmentTransaction和實現(xiàn)類是BackStackRecord,F(xiàn)ragmentManager的實現(xiàn)類是FragmentManagerImpl戳护。
在FragmentManagerImpl類中搜到了startAnimation金抡,而且還不止一處。這里其實隨便選一處就可以了(幾個地方其實都能找到需要的信息)腌且。這里選個相關(guān)代碼最簡單的梗肝。
// run animations:
Animation anim = loadAnimation(f, f.getNextTransition(), true,
f.getNextTransitionStyle());
if (anim != null) {
setHWLayerAnimListenerIfAlpha(f.mView, anim);
f.mView.startAnimation(anim);
}
這里其實就是Animation實際是怎么從Rid變成Animation實例的。f.getNextTransition就是之前設(shè)置的動畫資源id铺董,true表示是enter還是exit巫击。這里通過loadAnimation方法來加載動畫。
Animation loadAnimation(Fragment fragment, int transit, boolean enter,
int transitionStyle) {
Animation animObj = fragment.onCreateAnimation(transit, enter, fragment.getNextAnim());
if (animObj != null) {
return animObj;
}
if (fragment.getNextAnim() != 0) {
Animation anim = AnimationUtils.loadAnimation(mHost.getContext(),
fragment.getNextAnim());
if (anim != null) {
return anim;
}
}
......
}
代碼不多精续,這里一下子答案就清晰了坝锰。最終確實是通過AnimationUtils.loadAnimation來加載動畫資源的。但在加載之前會先
調(diào)用fragment.onCreateAnimation方法重付,如果這個方法返回空才會去調(diào)用AnimationUtils.loadAnimation顷级。辦法來了,可以復寫Fragment的onCreateAnimation方法來攔截Animation的創(chuàng)建确垫。復寫這個方法弓颈,return Rotate3dAnimation就可以了拣凹。
這里我們創(chuàng)建兩個空xml anim(只是為了用這個id)。名字叫:rotate_3d_enter和rotate_3d_exit恨豁。實現(xiàn)都是空嚣镜。
然后復寫Fragment的
@Override
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
if (nextAnim == R.anim.rotate_3d_enter) {
return Rotate3dAnimation;
}
if (nextAnim == R.anim.rotate_3d_exit) {
return Rotate3dAnimation;
}
return super.onCreateAnimation(transit, enter, nextAnim);
}
這樣就可以使用Rotate3dAnimation了。
Rotate3dAnimation參數(shù)
現(xiàn)在可以transaction.setCustomAnimations已經(jīng)可以使用自定義的Animation了橘蜜。但上面還遺留了一個問題菊匿,怎么創(chuàng)建Rotate3dAnimation。這個類有6個參數(shù)计福。
float fromDegrees 起始角度
float toDegrees 結(jié)束角度
角度參數(shù)很簡單跌捆,正面的應(yīng)該是從0度到90度,背面的應(yīng)該是從270度到360度象颖。
float centerX 中心點x
float centerY 中心點y
float depthZ 深度
中心點第一感覺就是通過getView.getWidth()0.5f getView().getHeight()0.5f佩厚。實際這樣是會有問題的,因為onCreateAnimation并不一定就是在View全部繪制完成才回調(diào)的说订。但是因為initialize方法會把View實際大小傳過來抄瓦。所以我們可以不需要自己計算View的寬和高。
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
}
我們可以把構(gòu)造方法改造下陶冷,寬和高不再傳實際的像素钙姊,而是傳對應(yīng)的比例。
/**
* An animation that rotates the view on the Y axis between two specified angles.
* This animation also adds a translation on the Z axis (depth) to improve the effect.
*/
public class Rotate3dAnimation extends Animation {
private static final int TYPE_SCALE = 0;
private static final int TYPE_PX = 1;
private final float mFromDegrees;
private final float mToDegrees;
private float mCenterX;
private float mCenterY;
private float mDepthZ;
private int mType = TYPE_PX;
private final boolean mReverse;
private Camera mCamera;
/**
* Creates a new 3D rotation on the Y axis. The rotation is defined by its
* start angle and its end angle. Both angles are in degrees. The rotation
* is performed around a center point on the 2D space, definied by a pair
* of X and Y coordinates, called centerX and centerY. When the animation
* starts, a translation on the Z axis (depth) is performed. The length
* of the translation can be specified, as well as whether the translation
* should be reversed in time.
*
* @param fromDegrees the start angle of the 3D rotation
* @param toDegrees the end angle of the 3D rotation
* @param centerX the X center of the 3D rotation
* @param centerY the Y center of the 3D rotation
* @param reverse true if the translation should be reversed, false otherwise
*/
public Rotate3dAnimation(float fromDegrees, float toDegrees,
float centerX, float centerY, float depthZ, boolean reverse) {
mFromDegrees = fromDegrees;
mToDegrees = toDegrees;
mCenterX = centerX;
mCenterY = centerY;
mDepthZ = depthZ;
mReverse = reverse;
}
public Rotate3dAnimation(float fromDegrees, float toDegrees
, float centerX, float centerY, float depthZ
, boolean reverse, int type) {
mFromDegrees = fromDegrees;
mToDegrees = toDegrees;
mCenterX = centerX;
mCenterY = centerY;
mDepthZ = depthZ;
mReverse = reverse;
mType = type;
}
public Rotate3dAnimation(float fromDegrees, float toDegrees, boolean reverse) {
this(fromDegrees, toDegrees, 0.5f, 0.5f, 0.5f, reverse, TYPE_SCALE);
}
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
mCamera = new Camera();
if (mType == TYPE_SCALE) {
mCenterX = width * mCenterX;
mCenterY = height * mCenterY;
mDepthZ = width * mDepthZ;
}
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
final float fromDegrees = mFromDegrees;
float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime);
final float centerX = mCenterX;
final float centerY = mCenterY;
final Camera camera = mCamera;
final Matrix matrix = t.getMatrix();
camera.save();
if (mReverse) {
camera.translate(0.0f, 0.0f, mDepthZ * interpolatedTime);
} else {
camera.translate(0.0f, 0.0f, mDepthZ * (1.0f - interpolatedTime));
}
camera.rotateY(degrees);
camera.getMatrix(matrix);
camera.restore();
matrix.preTranslate(-centerX, -centerY);
matrix.postTranslate(centerX, centerY);
}
}
Rotate3dAnimation整個類被改造成上面的樣子埂伦∩范睿可以直接使用三個參數(shù)的構(gòu)造方法Rotate3dAnimation(float fromDegrees, float toDegrees, boolean reverse)以中心點為旋轉(zhuǎn)軸心,以寬度一半為旋轉(zhuǎn)深度沾谜。這里為什么要取一半呢膊毁?仔細思考下,當翻轉(zhuǎn)進行到一半的時候View處于什么狀態(tài)基跑?這時候View剛好與屏幕垂直婚温,View深度也剛好是View寬度的一半,而此時也是翻轉(zhuǎn)過程中的最大深度涩僻,所以默認取寬度一半的深度效果比較好缭召。
boolean reverse 反轉(zhuǎn)(這個參數(shù)Google給的注釋是:true if the translation should be reversed, false otherwise。這個參數(shù)看源碼會發(fā)現(xiàn)其實只影響深度depthZ逆日,表示深度是從0變到depthZ,還是從depthZ變到0)正面翻的時候應(yīng)該是從0到depthZ萄凤,而此時背面應(yīng)該是從depthZ到0室抽。
Rotate3dHelper
public class AnimationHelper {
private AnimationHelper(){
}
public static void setUpRotate3dAnimation(android.support.v4.app.FragmentTransaction transaction) {
transaction.setCustomAnimations(R.anim.rotate_3d_enter, R.anim.rotate_3d_exit);
}
public static Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
if (nextAnim == R.anim.rotate_3d_enter) {
final Rotate3dAnimation animation = new Rotate3dAnimation(270, 360, false);
animation.setDuration(600);
animation.setStartOffset(300);
animation.setFillAfter(false);
animation.setInterpolator(new DecelerateInterpolator());
return animation;
}
if (nextAnim == R.anim.rotate_3d_exit) {
Rotate3dAnimation animation = new Rotate3dAnimation(0, 90, true);
animation.setDuration(300);
animation.setFillAfter(false);
animation.setInterpolator(new AccelerateInterpolator());
return animation;
}
return null;
}
}
寫個工具類,方便調(diào)用靡努。然后在對應(yīng)需要用到這個效果的Fragment中添加下面的代碼坪圾。
@Override
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
Animation animation = AnimationHelper.onCreateAnimation(transit, enter, nextAnim);
if (animation == null) {
return super.onCreateAnimation(transit, enter, nextAnim);
} else {
return animation;
}
}
最終實現(xiàn)
此時問題就全部排除了晓折。前面顯示與隱藏Fragment的代碼改造成下面這樣:
private static final String FRAGMENT_TAG_A = "FRAGMENT_TAG_A";
private static final String FRAGMENT_TAG_B = "FRAGMENT_TAG_B";
public void showFragmentA() {
showFragment(FRAGMENT_TAG_A, FRAGMENT_TAG_B);
}
public void showFragmentB() {
showFragment(FRAGMENT_TAG_B, FRAGMENT_TAG_A);
}
public void showFragment(String showTag, String hideTag) {
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
// 設(shè)置動畫
AnimationHelper.setUpRotate3dAnimation(transaction);
Fragment fragment = getSupportFragmentManager().findFragmentByTag(showTag);
if (fragment == null) {
// 沒有找到表示沒有被創(chuàng)建過
fragment = new FragmentA();
// 直接add
transaction.add(R.id.fragment_content, fragment, showTag);
} else {
// 找到了,表示已經(jīng)被add了兽泄,所以直接show
transaction.show(fragment);
}
fragment = getSupportFragmentManager().findFragmentByTag(hideTag);
if (fragment != null) {
// 找到了漓概,直接hide
transaction.hide(fragment);
}
transaction.commit();
}
這樣就可以setCustomAnimations使用自己自定義的Animation了。
其他
這里主要是介紹一種思路病梢,setCustomAniamtions不能set自定義Animation的時候怎么辦胃珍?看注釋,Google蜓陌,都不能解決的時候觅彰,如果通過分析猜測快速定位解決問題。當然中間還有很多Fragment相關(guān)的一些東西并沒有直接分析到钮热。比如Fragment填抬,F(xiàn)ragmentManageer,F(xiàn)ragmentTransaction之間的關(guān)系等隧期。主要是Fragment本身相對還是比較復雜的飒责,什么時候有空了,會把Fragment的源碼寫個文章分析下仆潮,會解釋清楚读拆,F(xiàn)ragment到底是什么,F(xiàn)ragment最后是如何顯示的鸵闪,DialogFragment明明沒有指定ContainerId檐晕,為什么它還是能顯示等等。
Rotate3dAnimator
最后再加一個Rotate3dAnimator蚌讼。為什么有個Animator辟灰?前面Google給的是Animation,但是假如你的項目使用的是android.app.Fragment篡石。那么你在Fragment需要復寫的就是
@Override
public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) {
Animator animator = AnimationHelper.onCreateAnimator(transit, enter, nextAnim);
if (animator == null) {
return super.onCreateAnimator(transit, enter, nextAnim);
} else {
return animator;
}
}
這里就是3.0之后的屬性動畫了芥喇。所以前面Google給的Rotate3dAnimation就不能用了。那怎么辦凰萨?這里就需要寫一個3d變換的Animator實現(xiàn)了继控。下面給出實現(xiàn)代碼。
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.support.annotation.Nullable;
import android.view.View;
public class Rotate3dAnimator extends ValueAnimator implements ValueAnimator.AnimatorUpdateListener {
private static double K = Math.sqrt(2.0f);
private static final int TYPE_SCALE = 0;
private static final int TYPE_PX = 1;
private View mTargetView;
private final float mFromDegrees;
private final float mToDegrees;
private float mCenterX;
private float mCenterY;
private float mDepthZ;
private int mType = TYPE_PX;
private boolean mNeedInit = true;
private final boolean mReverse;
private boolean mException = true;
private boolean mVisibleBeforeStart = false;
public Rotate3dAnimator(float fromDegrees, float toDegrees, boolean reverse) {
this(fromDegrees, toDegrees, 0.5f, 0.5f, 0.5f, reverse, TYPE_SCALE);
}
public Rotate3dAnimator(float fromDegrees, float toDegrees,
float centerX, float centerY, float depthZ,
boolean reverse, int type) {
this.mFromDegrees = fromDegrees;
this.mToDegrees = toDegrees;
this.mReverse = reverse;
this.mCenterX = centerX;
this.mCenterY = centerY;
this.mDepthZ = depthZ;
this.mType = type;
addUpdateListener(this);
addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
if (!mVisibleBeforeStart) {
mTargetView.setVisibility(View.VISIBLE);
}
if (mNeedInit) {
if (mType == TYPE_SCALE) {
mCenterX = mCenterX * mTargetView.getWidth();
mCenterY = mCenterY * mTargetView.getHeight();
}
mTargetView.setPivotX(mCenterX);
mTargetView.setPivotY(mCenterY);
mNeedInit = false;
}
removeListener(this);
}
});
setFloatValuesSafe(0f, 1f);
}
private void setFloatValuesSafe(float... values) {
mException = false;
setFloatValues(values);
mException = true;
}
@Override
public void setFloatValues(float... values) {
if (mException) {
throw new IllegalAccessError("Disable call. ");
}
super.setFloatValues(values);
}
public void setStartDelay(long startDelay, boolean visibleBeforeStart) {
super.setStartDelay(startDelay);
mVisibleBeforeStart = visibleBeforeStart;
}
View getTargetView() {
return mTargetView;
}
@Override
public void setTarget(@Nullable Object target) {
super.setTarget(target);
if (target == null) {
throw new NullPointerException("Target can't be null.");
}
mTargetView = (View) target;
if (!mVisibleBeforeStart) {
mTargetView.setVisibility(View.INVISIBLE);
}
}
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float progress = (float) animation.getAnimatedValue();
final float fromDegrees = mFromDegrees;
float degrees = fromDegrees + ((mToDegrees - fromDegrees) * progress);
double value = mDepthZ * K;
if (mReverse) {
// progress 0 - 1
// exit 1 -> value
mTargetView.setScaleX((float) (1 - (1 - value) * progress));
mTargetView.setScaleY((float) (1 - (1 - value) * progress));
} else {
// progress 0 - 1
// enter value -> 1
mTargetView.setScaleX((float) (value + (1 - value) * progress));
mTargetView.setScaleY((float) (value + (1 - value) * progress));
}
mTargetView.setRotationY(degrees);
}
}
注意有個setStartDelay方法有兩個參數(shù)胖眷,第二個參數(shù)是讓View在start前不顯示武通。為什么要這樣?因為翻轉(zhuǎn)的時候珊搀,背面的View需要在第一個View動畫處理完了才開始顯示冶忱,如果這個參數(shù)不指定false,那么第一次翻轉(zhuǎn)時候會有問題境析。具體可以自行嘗試下囚枪。
另外就是為什么這里翻轉(zhuǎn)時候不是移動Z軸派诬,而是對XY軸做Scale變換?這個链沼。默赂。。怎么解釋呢括勺?首先translationZ是5.0之后的Api缆八。其次translationZ是不能實現(xiàn)3d效果的翻轉(zhuǎn)的。整個翻轉(zhuǎn)的深度效果其實就是盡量保證翻轉(zhuǎn)時候有一條邊的高度搞好一直與容器高度相同朝刊。所以這里通過Scale來實現(xiàn)耀里。具體大家可自行嘗試translationZ看看實際是什么效果就明白了。