Fragment-自定义过度动画

本文主要内容翻译 http://trickyandroid.com/fragments-translate-animation/

Fragment 目前有v4包和API11以上才能用的两种。Fragment之间的过度动画主要有两个方法, 这两个方法是属于FragmentTransaction的

  • FragmentTransaction setCustomAnimations(int enter, int exit, int popEnter, int popExit)
  • FragmentTransaction setCustomAnimations(int enter, int exit)

这两个方法的参数是动画id, v4包的Fragment要求是R.animation类型, 而v11以上的Fragment要求的是R.animator类型。

然而在xml上animator 与animation上在移动动画上是有区别的:

1
2
3
4
5
6
7
8
9
10
11
12
13
animation的动画fromYDelta,toYDelta 支持百分比
<translate android:fromYDelta="0%"
android:toYDelta="8%"
android:fillEnabled="true"
android:duration="250"/>

animator的不支持百分比,只能1280px - 0px移动,不同手机的px值不一样没方法适配不同手机
<objectAnimator
android:propertyName="translationY"
android:valueType="floatType"
android:valueFrom="1280"
android:valueTo="0"
/>

为了解决animator在xml支持百分比这个问题,首先可以想到animator的propertyName参数是可以随意定义的,比如定义”yFraction”属性名,只要objectAnimator的动画target类有setYFraction(float fraction) 和getFragction()就可以了。

在Fragment中objectAnimator是作用在oncreateView()返回的View, 所以需要自定义的View解决了。

以RelativeLayout为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
进场动画:slide_up.xml
<objectAnimator
android:propertyName="yFraction"
android:valueType="floatType"
android:valueFrom="0"
android:valueTo="1.0"
/>

退场动画:slide_down.xml
<objectAnimator
android:propertyName="yFraction"
android:valueType="floatType"
android:valueFrom="1.0"
android:valueTo="0"
/>

使用setCustomAnimations()需要注意,必须在add之前,否则没效果
getFragmentManager().beginTransaction()
.setCustomAnimations(R.animator.slide_up,
R.animator.slide_down,
R.animator.slide_up,
R.animator.slide_down)
.add(R.id.list_fragment_container, SlidingListFragment())
.addToBackStack(null).commit();

//自定义SlidingRelativeLayout

public class SlidingRelativeLayout extends RelativeLayout {

public SlidingRelativeLayout(Context context) {
super(context);
}

public SlidingRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}

public SlidingRelativeLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}

public void setYFraction(final float fraction) {
float translationY = getHeight() * fraction;
setTranslationY(translationY);
}

public float getYFraction() {
if (getHeight() == 0) {
return 0;
}
return getTranslationY() / getHeight();
}
}

看效果:

从上图看来动画刚开始会一丝闪烁,这闪烁的问题出在:fragment的动画在layout测量之前就开始了。这就说明刚开始之前getHeight()为0,导致translationY为0,然而进场动画我们想要translationY为整个view高度到0。为了验证这推测,在原来的基础上加你log跟踪验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class SlidingRelativeLayout extends RelativeLayout {

private static final String TAG = "SlidingRelativeLayout";

private float yFraction = 0;
private ViewTreeObserver.OnPreDrawListener preDrawListener = null;
private float translationY;

public SlidingRelativeLayout(Context context) {
super(context);
}

public SlidingRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}

public SlidingRelativeLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}

public void setYFraction(final float fraction) {
this.yFraction = fraction;
Log.d(TAG, "setYFraction: height * fraction = translationY ->" + getHeight() + " * " + fraction + " = " + getHeight() * fraction);
translationY = getHeight() * fraction;
setTranslationY(translationY);

if(preDrawListener == null){
preDrawListener = new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
Log.d(TAG, "onPreDraw: translationY " + translationY);
return true;
}
};
getViewTreeObserver().addOnPreDrawListener(preDrawListener);
}

}

public float getYFraction() {
return yFraction;
}
}

最终呈现的log:

so,明显只要过滤掉还没有测量时getHeight为0就可以了。
最终代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class SlidingRelativeLayout extends RelativeLayout {

private float yFraction = 0;

public SlidingRelativeLayout(Context context) {
super(context);
}

public SlidingRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}

public SlidingRelativeLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}

private ViewTreeObserver.OnPreDrawListener preDrawListener = null;

public void setYFraction(float fraction) {

this.yFraction = fraction;

if (getHeight() == 0) {
if (preDrawListener == null) {
preDrawListener = new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
setYFraction(yFraction);
return true;
}
};
getViewTreeObserver().addOnPreDrawListener(preDrawListener);
}
return;
}

float translationY = getHeight() * fraction;
setTranslationY(translationY);
}

public float getYFraction() {
return this.yFraction;
}
}

Loading comments box needs to over the wall