您的位置:首页 > 其它

ViewPager动画

2016-06-13 22:41 507 查看
     

                                          动画系列 - ViewPager动画

转载                 http://www.lightskystreet.com/2014/12/15/viewpager-anim/    

前言

前两天看到鲍永章分享的Great animations with PageTransformer以及农民伯伯分享的Create
ViewPager transitions 文章,都是通过ViewPager来实现酷炫的动画,而现在的App中ViewPager的动画使用也非常的广泛。正好最近一直研究动画,那么就趁热打铁,分析一下相关的开源库吧。

本篇文章介绍的ViewPager动画,可以分为两类,第一类是针对于ViewPager的界面滑动动画(这个是PageTransformer的真正用途),分析并比较了AndroidImageSlider和JazzyViewPager两种实现,第二类是对ViewPager中的内容进行动画处理,这个是这个是PageTransformer的巧妙应用,处理好了可以达到很棒的交互效果,示例是Yahoo天气的视差效果。

ViewPager动画的实现原理

从3.0开始,ViewPager开始支持自定义切换动画,暴露的接口为PageTransformer,因此只要实现PageTransformer接口和其唯一的方法transformPage(View view, float position)即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

/**
* A PageTransformer is invoked whenever a visible/attached page is scrolled.
* This offers an opportunity for the application to apply a custom transformation
* to the page views using animation properties.
*
* <p>As property animation is only supported as of Android 3.0 and forward,
* setting a PageTransformer on a ViewPager on earlier platform versions will
* be ignored.</p>
*/
public interface PageTransformer {
/**
* Apply a property transformation to the given page.
*
* @param page Apply the transformation to this page
* @param position Position of page relative to the current front-and-center
*                 position of the pager. 0 is front and center. 1 is one full
*                 page position to the right, and -1 is one page position to the left.
*/
public void transformPage(View page, float position);
}

参数position

给定界面的位置相对于屏幕中心的偏移量。在用户滑动界面的时候,是动态变化的。那么我们可以将position的值应用于setAlpha(), setTranslationX(), or setScaleY()方法,从而实现自定义的动画效果。

另外在ViewPager滑动时,内存中存活的Page都会执行transformPage方法,在滑动过程中涉及到两个Page,当前页和下一页,而它们的position值是相反的(因为是相对运动,一个滑入一个滑出),比如,页面A向右滑动到屏幕一半,页面B也正好处于一半的位置,那么A和B的position为:0.5 和 -0.5

position == 0 :当前界面位于屏幕中心的时候

position == 1 :当前Page刚好滑出屏幕右侧

position == -1 :当前Page刚好滑出屏幕左侧

AndroidImageSlider动画库解析

要说到动画库,肯定会想到代码家,没错,代码家也开源了一个ViewPager效果的库:AndroidImageSlider ,我们就来分析下这个库的实现。AndroidImageSlider除了基本的page动画外,也支持用户为Page内容添加自定义的动画,比如下面描述框的动画。





AndroidImageSlider兼容性的实现原理:

因为API 11才开始支持PagerTransformer. 这里面修改了Android系统的ViewPager,名为ViewPagerEx,ViewPager里面有一段if逻辑判断是否在3.0以上使用PagerTransformer。因为PagerTransformer动画效果的实现依赖了PropertyViewAnim。但代码家的这个库使用NineOldAndroids实现了向3.0之前的兼容。因此就把这个if条件去掉了,其它部分都没变。

1
2
3
4
5
6

/**
* @author daimajia : I just remove the if condition in setPageTransformer() to make it compatiable with Android 2.0+
* of course, with the help of the NineOldDroid.
* Thanks to JakeWharton.
* http://github.com/JakeWharton/NineOldAndroids */

AndroidImageSlider的总体设计

BaseTransformer

所有Transformer的基类,实现了ViewPagerEx.PageTransformer接口以及transformPage方法,并提供了onPreTransform(View view, float position)、onPostTransform(View view, float position)、onTransform(View view, float position);

分别在transformPage前后调用,用来处理为每一次的执行动画前的准备和结束动作,比如还原所有的动画状态。

BaseAnimationInterface:

ViewPagerEx执行Transformer动画的时候注入一些自己的动画。你需要实现该接口,然后实现以下4个方法,获取SlideView中的View,实现自己的动画。比如底部的DescriptionText动画DescriptionAnimation
出现时的动画就是onNextItemAppear中添加的,你可以点入,看下源码

onPrepareCurrentItemLeaveScreen(View current)

onPrepareNextItemShowInScreen(View next)

onCurrentItemDisappear(View view)

onNextItemAppear(View view)

而这4个方法的调用是在BaseTransformer中。BaseTransformer统一管理了Page滑动时的所有动画。为了获取这4个方法的调用时机,也是煞费苦心啊,先说下思路:

因为ViewPager滑动的时候transformPage方法是实时调用的,这里获取最初两次调用时传入的position进行比较。通过一个HashMap<view, arraylist>来维护不同pageView的多个position。但这里为了优化,只取前两个position做比较。

先判断起始滑动的方向,然后再判断下一次滑动的方向,两次结果作差来判断到底哪个page要离开界面还是进入界面。

确定2个界面的4种临界状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2021
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
54
55
56
57
58

if(mCustomAnimationInterface != null){
if(h.containsKey(view) == false || h.get(view).size() == 1){
if(position > -1 && position < 1){
if(h.get(view) == null){
h.put(view,new ArrayList<Float>());//为每个View创建一个List,来存储偏移分数position
}
h.get(view).add(position);//向指定的View中添加偏移分数
if(h.get(view).size() == 2){
float zero = h.get(view).get(0);
float cha = h.get(view).get(1) - h.get(view).get(0);//当前滑动的位移偏移量分数差: newfraction - oldfraction
if(zero > 0){//起始时,向左滑动,当前Page中心位于屏幕中心左侧
//判断滑动趋势,如果继续向左侧滑动,position从0到1越来越大 newfraction > oldfraction
if(cha > -1 && cha < 0){//向右侧滑动
//in
mCustomAnimationInterface.onPrepareNextItemShowInScreen(view);//下一个Page将要进入屏幕
}else{//cha > 0 继续向左侧滑动
//out
mCustomAnimationInterface.onPrepareCurrentItemLeaveScreen(view);//当前Page将要离开屏幕
}
}else{//起始时,向右滑动,当前Page中心位于屏幕中心右侧
//判断滑动趋势,如果继续向右侧滑动,position从0到-1越来越小 newfraction < oldfraction
if(cha > -1 && cha < 0){//负值,继续向右滑动,因此当前page将要滑出屏幕
//out
mCustomAnimationInterface.onPrepareCurrentItemLeaveScreen(view);//当前Page将要离开屏幕
}else{//向左滑动
//in
mCustomAnimationInterface.onPrepareNextItemShowInScreen(view);//下一个Page将要离开屏幕
}
}
}
}
}
}

boolean isApp,isDis;
/**
* Called each {@link #transformPage(View, float)} call after {@link #onTransform(View, float)} is finished.
*
* @param view
* @param position
*/
protected void onPostTransform(View view, float position) {
if(mCustomAnimationInterface != null){
if(position == -1 || position == 1){//当前界面刚好完全移除界面
mCustomAnimationInterface.onCurrentItemDisappear(view);
isApp = true;
}else if(position == 0){
mCustomAnimationInterface.onNextItemAppear(view);//下一个Page刚好完全显示
isDis = true;
}
if(isApp && isDis){
h.clear();
isApp = false;
isDis = false;
}
}
}

BaseSliderView:

该类就是SlideView的基类,持有SlideView的一些公共方法,比如设置SliderView的image资源的方法image(File file),图片加载异常和失败的处理,empty(URL)用于展示,errorDisappear(boolean disappear)。这种思想和一个EmptyView有些类似,在我公司的项目也是使用了这种方式,比如通常一个ListView的界面,会有一个包装后的progressbar(包含各种情况的处理)来显示进度,当加载失败的时候,或者无数据的时候,可以调用该progressbar身上的方法,去决定显示何种布局。使用起来很方便。当然如果你的SliderView更复杂,你可以通过实现BaseSliderView,然后实现自己的SliderView。
下面两个类就是切换的View:

DefaultSliderView:实现了BaseSliderView,该View默认就是一张图片

TextSliderView:带有图片和描述性文字的View

Sliderlayout:

一个控制中心,将InfiniteViewPager,PageIndicator,粘合在一起,控制动画的样式,ViewPager轮循的播放。
addSlider:向ViewPager中添加一个SlideView

startAutoCycle:启动轮播

pauseAutoCycle:停止轮播

setDuration: 轮播间隔

setPagerTransformer 为ViewPager设置自定义的PageTransformer

onInterceptTouchEvent 在用户手势按下的时候,就停止轮循

Note:

这里在onInterceptTouchEvent中处理而不能再onTouchEvent中处理。因为SlideView会消费掉点击事件,事件被消费了,没法返回给SlideLayout的onTouchEvent中。

OK,总体设计弄清楚后,剩下来就是动画中最核心的部分了,就是动画效果的实现,这里只简单的介绍Accordion动画的实现,其它的大家可以自己分析。关于PropertyView动画的使用,可以参考我的另一篇文章:PropertyAnim实际应用

Accordion

1
2
3
4
5
67
8
9

public class AccordionTransformer extends BaseTransformer {

@Override
protected void onTransform(View view, float position) {
ViewHelper.setPivotX(view,position < 0 ? 0 : view.getWidth());
ViewHelper.setScaleX(view,position < 0 ? 1f + position : 1f - position);
}

}

代码家在README中注明了Thanks JazzyViewPager,开始没注意,后来看JazzyViewPager的源码时候偶然发现AndroidImageSlider的一些实现的灵感是来自JazzyViewPager项目。不过JazzyViewPager的实现方式略显复杂,没有使用PageTransformer接口,而是使用了OnPageChangeListener接口的onPageScrolled方法。

下面简单的看下JazzyViewPager库的动画实现,它将positionOffset作为参数控制。由于positionOffset的值为[0,1),所以就需要分别处理正负的情况。另外JazzyViewPager是通过维护一个LinkedHashMap来持有Page的引用。在Adapter添加界面的时候,会调用JazzyViewPager的 setObjectForPosition(Object obj, int position) 方法存入到集合中去。然后在onPageScrolled方法中再根据position获取当前Page的前一个mLeft和后一个mRight界面,分别对前后两个界面添加动画。

两种方式的关键参数区别

onPageScrolled(int position, float positionOffset, int positionOffsetPixels)方法的参数:

1
2
3
4
5
67
8
9
10
11

/**
* This method will be invoked when the current page is scrolled, either as part
* of a programmatically initiated smooth scroll or a user initiated touch scroll.
*
* @param position Position index of the first page currently being displayed.
*                 Page position+1 will be visible if positionOffset is nonzero.
* @param positionOffset Value from [0, 1) indicating the offset from the page at position.
* @param positionOffsetPixels Value in pixels indicating the offset from position.
*/
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);

下面是打印的PageTransformer接口的transformPage时position的值:相邻两界面的绝对值的和为1.而正负则表示了滑入和滑出状态

1
2
3
4
5
67
8
9
10
11
12
13
14
15
16
17
18

12-15 21:03:13.025    I/System.out﹕ transformPage----------------------    -0.43611112
12-15 21:03:13.025    I/System.out﹕ transformPage----------------------    0.5638889

12-15 21:03:13.045    I/System.out﹕ transformPage----------------------    -0.38055557
12-15 21:03:13.045    I/System.out﹕ transformPage----------------------    0.61944443

12-15 21:03:13.045    I/System.out﹕ transformPage----------------------    -0.37777779
12-15 21:03:13.045    I/System.out﹕ transformPage----------------------    0.62222224

12-15 21:03:13.055    I/System.out﹕ transformPage----------------------    -0.32916668
12-15 21:03:13.055    I/System.out﹕ transformPage----------------------    0.67083335

12-15 21:03:13.065    I/System.out﹕ transformPage----------------------    -0.32222223
12-15 21:03:13.065    I/System.out﹕ transformPage----------------------    0.67777777

12-15 21:03:13.075    I/System.out﹕ transformPage----------------------    -0.27916667
12-15 21:03:13.075    I/System.out﹕ transformPage----------------------    0.72083336

onPageScrolled方法的positionOffset的值为 [0, 1) ,而PageTransformer接口的transformPage(View page, float position) 已经标好了正负值(滑入和滑出),如果你的动画正好是相对的,那么用transfromPage就简单的多。

我们取同一种动画效果Cube来比较一下JazzyViewPager和AndroidImageSlider两者的实现:

JazzyViewPager部分源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2021
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
54
55
56
57
58
59
60
61

@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

...

float effectOffset = isSmall(positionOffset) ? 0 : positionOffset;
mLeft = findViewFromObject(position);
mRight = findViewFromObject(position+1);

switch (mEffect) {
case Standard:
break;
...
case animateCube:
animateCube(mLeft, mRight, effectOffset);
break;
}
}
...
private void animateCube(View left, View right, float positionOffset, boolean in) {
if (mState != State.IDLE) {
if (left != null) {
manageLayer(left, true);
mRot = (in ? 90.0f : -90.0f) * positionOffset;
ViewHelper.setPivotX(left, left.getMeasuredWidth());
ViewHelper.setPivotY(left, left.getMeasuredHeight()*0.5f);
ViewHelper.setRotationY(left, mRot);
}
if (right != null) {
manageLayer(right, true);
mRot = -(in ? 90.0f : -90.0f) * (1-positionOffset);
ViewHelper.setPivotX(right, 0);
ViewHelper.setPivotY(right, right.getMeasuredHeight()*0.5f);
ViewHelper.setRotationY(right, mRot);
}
}
}

public void setObjectForPosition(Object obj, int position) {//每次Adapter实例化View的时候,调用该方法,将实例化的View存入集合
mObjs.put(Integer.valueOf(position), obj);
}

public View findViewFromObject(int position) {//根据position从集合中获取到对应的View
Object o = mObjs.get(Integer.valueOf(position));
if (o == null) {
return null;
}
PagerAdapter a = getAdapter();
View v;
for (int i = 0; i < getChildCount(); i++) {
v = getChildAt(i);
if (a.isViewFromObject(v, o))
return v;
}
return null;
}

....

}

AndroidIamgeSlider中的CubeIn:

1
2
3
4
5
67
8
9
10

public class CubeInTransformer extends BaseTransformer {

@Override
protected void onTransform(View view, float position) {
// Rotate the fragment on the left or right edge
ViewHelper.setPivotX(view,position > 0 ? 0 : view.getWidth());
ViewHelper.setPivotY(view,0);
ViewHelper.setRotation(view,-90f * position);
}
}

JazzyViewPager:CubeIn上面代码中已经贴出:

1
2
3
4
5
67
8
9

if (left != null) {
...
mRot = (in ? 90.0f : -90.0f) * positionOffset;
...
}

if (right != null) {
mRot = -(in ? 90.0f : -90.0f) * (1-positionOffset);
}

可以精简为:

1
2

positionOffset * value
-(1-positionOffset) * value

与从上面的transformPage打印的position的值再比较下

1
2

12-15 21:03:13.075    I/System.out﹕ transformPage----------------------    -0.27916667
12-15 21:03:13.075    I/System.out﹕ transformPage----------------------    0.72083336

结论:

发现transformPage已经帮我们处理好了一切,直接用。我们可以根据正负符号来判断滑入和滑出的View(不用像JazzyViewPager那样去维护一个集合了),从而针对滑入滑出做出不同或相对的的动画。最简单的,两个界面如果是简单的相对动画(滑入对滑出),则什么都不用处理,直接用就行了,就像下面介绍的Yahoo视差的实现一样,具体可以看下面的讲解。

大家仔细看下相同的动画效果的处理方式,CubeInTransformer不用单独处理上一个和下一个界面,而只管根据position的正负判断当前Page和下一个Page去自定义不同的动画即可。

所以可以把AndroidImageSlider中的一些动画效果看做是JazzyViewPager的精简版本。

对于ViewPager如何帮我们处理View的呢?可以看下源码:

1
2
3
4
5
67
8
9
10
11
12
13
14
15
16
17
18
19

protected void onPageScrolled(int position, float offset, int offsetPixels) {
...
if (mPageTransformer != null) {
final int scrollX = getScrollX();
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();

if (lp.isDecor) continue;

final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth();
mPageTransformer.transformPage(child, transformPos);
}
}
...

}

小结

上面主要介绍了ViewPager的Page的滑动动画,两种实现方式:

PageTransformer.transformPage方式:在执行onPageScrolled方法的时候,会遍历ViewPager的所有View,并执行其transformPage方法。position是已经处理好的(方向和值)

onPageScrolled方式:略显复杂,因为没法拿到View,还要自己去维护一个View集合,并且positionOffset的限制,需要自己去处理不同View的position : PageA:position , PageB:-(1-position)。

为ViewPager的Page内容添加动画,实现炫酷的交互效果

Tholotis





实现原理:

为Page内部的View处理不同的平移速度,达到视差的效果,作者的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2021
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

public void transformPage(View view, float position) {
int pageWidth = view.getWidth();

if (position < -1) { // [-Infinity,-1)
// This page is way off-screen to the left.
view.setAlpha(0);

} else if (position <= 1) { // [-1,1]

mBlur.setTranslationX((float) (-(1 - position) * 0.5 * pageWidth));
mBlurLabel.setTranslationX((float) (-(1 - position) * 0.5 * pageWidth));

mDim.setTranslationX((float) (-(1 - position) * pageWidth));
mDimLabel.setTranslationX((float) (-(1 - position) * pageWidth));

mCheck.setTranslationX((float) (-(1 - position) * 1.5 * pageWidth));
mDoneButton.setTranslationX((float) (-(1 - position) * 1.7 * pageWidth));
// The 0.5, 1.5, 1.7 values you see here are what makes the view move in a different speed.
// The bigger the number, the faster the view will translate.
// The result float is preceded by a minus because the views travel in the opposite direction of the movement.

mFirstColor.setTranslationX((position) * (pageWidth / 4));

mSecondColor.setTranslationX((position) * (pageWidth / 1));

mTint.setTranslationX((position) * (pageWidth / 2));

mDesaturate.setTranslationX((position) * (pageWidth / 1));
// This is another way to do it

} else { // (1,+Infinity]
// This page is way off-screen to the right.
view.setAlpha(0);
}
}

GitHub上类似的效果还有:
https://github.com/prolificinteractive/ParallaxPager





https://github.com/flavienlaurent/discrollview





这两个项目都封装成了库,使用的时候也简单,但一般我们可以通过ViewPager.PageTransformer来实现这样的效果,从上面AndroidImageSlider与JazzyViewPager的对比中也能看到,PageTransformer的实现方式更简单。而且ViewPager的动画库也很多,比如上面的JazzyViewPager。你只需把某一个动画效果的transformPage方法的逻辑拿来就可用,当然如果你的需求更复杂,或者ViewPager实现起来较麻烦,你可以考虑上面的两个项目,扩展自己的思维。

JazzyViewPager:
https://github.com/jfeinstein10/JazzyViewPager/blob/master/lib/src/com/jfeinstein/jazzyviewpager/JazzyViewPager.java
https://github.com/prolificinteractive/ParallaxPager
https://github.com/flavienlaurent/discrollview

Yahoo天气





原理很简单,只是这次处理的动画对象是背景图片,减慢其平移的速度,而ViewPager的内容正常移动,从而达到视差的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2021
22
23

public class ParallaxPageTransformer implements ViewPager.PageTransformer {

public void transformPage(View view, float position) {

int pageWidth = view.getWidth();

if (position < -1) { // [-Infinity,-1)
// This page is way off-screen to the left.
view.setAlpha(1);

} else if (position <= 1) { // [-1,1]

dummyImageView.setTranslationX(-position * (pageWidth / 2)); //Half the normal speed

} else { // (1,+Infinity]
// This page is way off-screen to the right.
view.setAlpha(1);
}

}
}

OK,本来想自己写,但刚巧在GitHub上发现了一个:ParallaxPagerTransformer ,并提供了APK
,实现了Yahoo天气的效果,但多了一些缩放的效果,将缩放的代码注释掉就和Yahoo天气的效果完全一样了。另外想clone到本地的朋友注意了,由于作者的AndroidStudio版本太老,导入的时候你需要做一些处理。

作者把该Transformer抽取出来,做为了一个lib,就一个类,核心代码很简单,说明ViewPager.PageTransformer接口很强大啊:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

@Override
public void transformPage(View view, float position) {

View parallaxView = view.findViewById(id);

if (parallaxView != null) {
if (position > -1 && position < 1) {
float width = parallaxView.getWidth();
parallaxView.setTranslationX(-(position * width * speed));
float sc = ((float)view.getWidth() - border)/ view.getWidth();
if (position == 0) {//这里处理了缩放的效果,去掉即和Yahoo天气的效果一样
view.setScaleX(1);
view.setScaleY(1);
} else {
view.setScaleX(sc);
view.setScaleY(sc);
}
}
}
}

看了上面的实现,大家应该知道如何去实现类似的交互动画了,而且现在的App导航页中,也越来越多的使用到了这样的交互,作者在文章还列举了News Digest的导航页,有了思路,实现起来应该就不难了,第一、三页的动画上面已经介绍过,对于第二页,其实就是一个旋转动画。实时的旋转可以通过实时变化的position来处理。




类似的项目:
https://github.com/andraskindler/parallaxviewpager




总结:

整篇文章介绍的东西其实很简单,只是以前没有研究过ViewPager.PageTransformer,使用简单,但功能强大,position参数在动画处理中相当重要,因为是实时的百分比,所以省去了自己不少计算,如果你需要为ViewPager自定义动画,那么选择PageTransformer,对于本文的第二类巧妙动画效果的介绍,看具体情况决定是否使用PageTransformer,如果position能很方便帮助动画的计算,那是最好的,如果不是那么你完全可以使用onPageScrolled来处理,源码中也可看到,PageTransformer就是在onPageScroll中调用的。

使用时要注意,它只支持3.0以上的系统。如果要兼容,可以参考上面AndroidImageSlider的兼容实现。在文章中也插入了不少ViewPager动画项目的链接,也许不是使用PageTransformer,但也是一种思路的扩展,供大家参考。下一篇文章,我会介绍Fragment的动画,大家敬请期待。

参考文献:
官方文档
Great animations with PageTransformer
Create ViewPager transitions

GitHub上相关的ViewPager动画的项目

https://github.com/daimajia/AndroidImageSlider
https://github.com/inovex/ViewPager3D

轮循的ViewPager
https://github.com/antonyt/InfiniteViewPager
https://github.com/JakeWharton/salvage
https://github.com/Trinea/android-auto-scroll-view-pager

VerticalViewPager
https://github.com/JakeWharton/Android-DirectionalViewPager
https://github.com/LambergaR/VerticalViewPager
https://github.com/VenomVendor/AutoNotifyViewPager

https://github.com/Dreddik/AndroidTouchGallery

特效的ViewPager
https://github.com/kmshack/Android-ParallaxHeaderViewPager
https://github.com/andraskindler/parallaxviewpager
https://github.com/MoshDev/BackgroundViewPager

与ViewPager一起使用的导航:
https://github.com/astuetz/PagerSlidingTabStrip (不支持TextView颜色的变化)
https://github.com/jpardogo/PagerSlidingTabStrip (支持TextView颜色变化)
https://github.com/DSofter/SmoothTabIndicator

FragmentAnim
https://github.com/DesarrolloAntonio/FragmentTransactionExtended

1


                                            
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息