实现带3D弹性效果的ViewPager(非自定义控件)

一、前言

最近有朋友问我,这种效果是怎么做出来的……

csdn只能上传小于2M的图片,只好划的快一点了。
效果就是,在第一页和最后一页的时候,继续滑动会有个3D的旋转效果。

第一反应就是jazzViewpager,叫朋友去试试看。结果他说不行,我也没去试,估计jazzViewPager是每个页面都会有动画……
然后我用了一个很笨的方法,就是监听touch事件,用属性动画搞定。

二、实现思路

  1. 判断当前的pager是否是第一个或者最后一个。
  2. 监听滑动事件,根据手指的位移一直改变View的角度。其实重点就在这里了=。=
  3. 当手指松开的时候,用属性动画将View旋转回原来的角度。

三、代码实现

没有自定义一个ViewPager,直接监听onTouch了。以后有时间可能会抽一个自定义出来……

package com.aitsuki.viewpagedemo;

import android.animation.ObjectAnimator;
import android.app.Activity;
import android.os.Bundle;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;

import java.util.ArrayList;

/** * Created by AItsuki on 2016/1/11. */
public class PagerActivity extends Activity {

    private ViewPager mViewPager;
    private ArrayList<View> mViews;

    // 手指按下的位置
    private int startX;
    // ViewPager当前显示的position
    private int mCurrentPage;
    // 是否正在进行动画
    private boolean onAnimator;
    // View的最小滑动距离。
    private int mSlop;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_pager);

        // 获取View的最小滑动距离。(当滑动超过这个距离,我们才判断这是滑动事件)
        mSlop = ViewConfiguration.get(this).getScaledTouchSlop();

        // 将需要显示View填充出来, 用集合保存
        LayoutInflater inflater = getLayoutInflater();
        View page1 = inflater.inflate(R.layout.page1, null);
        View page2 = inflater.inflate(R.layout.page2, null);
        View page3 = inflater.inflate(R.layout.page3, null);
        mViews = new ArrayList<>();
        mViews.add(page1);
        mViews.add(page2);
        mViews.add(page3);

        mViewPager = (ViewPager) findViewById(R.id.vp);
        MyAdapter adapter = new MyAdapter();
        mViewPager.setAdapter(adapter);


        // 监听页面切换,获取ViewPager当前显示的position
        mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

            }

            @Override
            public void onPageSelected(int position) {
                mCurrentPage = position;

            }

            @Override
            public void onPageScrollStateChanged(int state) {

            }
        });

        // 监听事件,这里就是整个功能的核心了……
        mViewPager.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                int action = event.getAction();
                switch (action) {
                    case MotionEvent.ACTION_DOWN:
                        // 不能在这里获取手指按下的事件,因为如果有子View消费了此事件,那么这里是不执行的。
// startX = (int) event.getRawX();
                        break;
                    case MotionEvent.ACTION_MOVE:
                        // 获得手指按下的位置。
                        if(startX == 0) {
                            startX = (int) event.getRawX();
                        }

                        // 获得当前的位置。
                        int endX = (int) event.getRawX();

                        // 如果是第一个页面
                        if(mCurrentPage == 0 ) {
                            // 判断手指移动的距离是否大于最小滑动距离
                            // 是的话标记当前的状态正在进行动画。
                            if(endX - startX > mSlop) {
                                onAnimator = true;
                            }

                            // 如果正在进行动画
                            // startX < endX 这个是为了防止向反方向的位置做动画。比如按下手指,往右滑动一丁点,
                            // 然后往左滑动。如果不做判断,那么动画的方向就反了。
                            if(startX < endX && onAnimator) {
                                // 获取当前显示的View
                                // 不能直接mViewPager.getCharAt(mCurrentPager),因为ViewPager缓存懒加载机制
                                // 的原因,这样获取的View并不一定是当前正在显示的View。
                                // 在Adapter的时候就要给View设置一个tag和position关联起来,然后通过
                                // mViewPager.findViewWithTag(position)找到该View。
                                View view = mViewPager.findViewWithTag(mCurrentPage);

                                // 使用属性动画实时改变View的形状, 滑动距离乘以一个系数,不然旋转角度会很大。
                                // 你也可以通过设置其他方式计算,比如限制一个最大角度,这里只是图省事。
                                // 如果觉得用属性动画太挫,你也可以使用nineoldandroids这个库的ViewHelper,实际上
                                // 并没有什么不同。
                                ObjectAnimator.ofFloat(view, "rotationY", (endX - startX) * 0.025f).setDuration(0).start();
                            }
                            // 如果是最后一页。这里的代码和上面类似,就不注释了。
                        } else if(mCurrentPage == mViews.size() -1) {
                            if(startX - endX > mSlop) {
                                onAnimator = true;
                            }
                            if(startX > endX && onAnimator) {
                                View view = mViewPager.findViewWithTag(mCurrentPage);
                                ObjectAnimator.ofFloat(view , "rotationY", (endX - startX) * 0.025f).setDuration(0).start();
                            }
                        }
                        break;
                    case MotionEvent.ACTION_UP:
                    case MotionEvent.ACTION_CANCEL:
                        // 当手指抬起或事件取消的时候,我们要将View归位,至于动画的时间,你们也可以根据角度的大小设置时间=。=
                        View view = mViewPager.findViewWithTag(mCurrentPage);
                        ObjectAnimator.ofFloat(view, "rotationY", 0).setDuration(250).start();

                        // 按下的位置归0,不然可能影响下一次手势判断。
                        startX = 0;
                        // 退出动画状态
                        onAnimator= false;
                        break;
                }
                // 如果是动画状态,那么就直接return true,用这里的代码处理事件。
                // 否则将事件交给ViewPager处理
                // 简单来说: true自己处理,false交给ViewPager处理。没看懂的话建议去看看我的另一篇博客=。=
                // "Android的事件分发源码分析,告别事件冲突"
                return onAnimator;
            }
        });
    }


    class MyAdapter extends PagerAdapter {
        @Override
        public int getCount() {
            return mViews.size();
        }

        @Override
        public boolean isViewFromObject(View view, Object object) {
            return view == object;
        }

        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            container.removeView((View) object);
        }

        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            View view = mViews.get(position);
            // 这里也是关键,将position设置为view的tag。否则找不到这个View
            view.setTag(position);
            container.addView(view);
            return view;
        }
    }
}

四、如果是FragmentAdapter该怎么使用

将上面那个Demo发给朋友之后,他一直说牛逼,让后有点小小的成就感。结果第二天他跟我说不会用,因为他需要用FragmentAdapter,我一口老血喷涌而出。
核心思想其实就是获取到第一个页面和最后一个页面的根View进行动画。那么Fragment该怎么获取到它的View呢?
我们再初始化Fragment的时候会填充一个View作为Fragment的显示内容,这个View就是我们要操作的对象了,所以我们要将这个View提供出去。



抽一个Fragment的基类,写一个抽象方法让子类实现,得到它们的根View。

/** * Created by AItsuki on 2016/1/11. */
public abstract class BaseFragment extends Fragment {

    public View mRootView;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        mRootView = initView(inflater);
        return mRootView;
    }

    public abstract View initView(LayoutInflater inflater);
}


子类继承就可以了Basefragment就可以了。我这里写了三个Fragment,分别是A、B、C

/** * Created by AItsuki on 2016/1/11. */
public class AFragment extends BaseFragment {

    @Override
    public View initView(LayoutInflater inflater) {
        return inflater.inflate(R.layout.page1, null);
    }
}
/** * Created by AItsuki on 2016/1/11. */
public class BFragment extends BaseFragment {

    @Override
    public View initView(LayoutInflater inflater) {
        return inflater.inflate(R.layout.page2, null);
    }
}
/** * Created by AItsuki on 2016/1/11. */
public class CFragment extends BaseFragment {

    @Override
    public View initView(LayoutInflater inflater) {
        return inflater.inflate(R.layout.page3, null);
    }
}



最后就是Activity中的代码了, 就不注释了,和Pager并没有多大区别。
通过adapter.getItem就可以很准确的获取到当前显示的Fragment,不用设置tag那么麻烦。

package com.aitsuki.viewpagedemo;

import android.animation.ObjectAnimator;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;

import com.aitsuki.viewpagedemo.fragment.AFragment;
import com.aitsuki.viewpagedemo.fragment.BFragment;
import com.aitsuki.viewpagedemo.fragment.BaseFragment;
import com.aitsuki.viewpagedemo.fragment.CFragment;

import java.util.ArrayList;

/** * Created by AItsuki on 2016/1/11. */
public class FragmentPagerActivity extends FragmentActivity {
    private ViewPager mViewPager;
    private int startX;
    private int mCurrentPage;
    private boolean onAnimator;
    private int mSlop;
    private ArrayList<BaseFragment> mFragments;
    private MyFragmentAdapter fragmentAdapter;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_pager);

        mSlop = ViewConfiguration.get(this).getScaledTouchSlop();

        AFragment aFragment = new AFragment();
        BFragment bFragment = new BFragment();
        CFragment cFragment = new CFragment();
        mFragments = new ArrayList<>();
        mFragments.add(aFragment);
        mFragments.add(bFragment);
        mFragments.add(cFragment);

        FragmentManager manager = getSupportFragmentManager();
        fragmentAdapter = new MyFragmentAdapter(manager);

        mViewPager = (ViewPager) findViewById(R.id.vp);
        mViewPager.setAdapter(fragmentAdapter);

        mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

            }

            @Override
            public void onPageSelected(int position) {
                mCurrentPage = position;

            }

            @Override
            public void onPageScrollStateChanged(int state) {

            }
        });

        mViewPager.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                int action = event.getAction();
                switch (action) {
                    case MotionEvent.ACTION_DOWN:

                        break;
                    case MotionEvent.ACTION_MOVE:
                        if(startX == 0) {
                            startX = (int) event.getRawX();
                        }
                        int endX = (int) event.getRawX();

                        if(mCurrentPage == 0 ) {
                            if(endX - startX > mSlop) {
                                onAnimator = true;
                            }
                            if(startX < endX && onAnimator) {
                                BaseFragment item = fragmentAdapter.getItem(mCurrentPage);
                                ObjectAnimator.ofFloat(item.mRootView, "rotationY", (endX - startX) * 0.025f).setDuration(0).start();
                            }
                        } else if(mCurrentPage == mFragments.size() -1) {
                            if(startX - endX > mSlop) {
                                onAnimator = true;
                            }
                            if(startX > endX && onAnimator) {
                                BaseFragment item = fragmentAdapter.getItem(mCurrentPage);
                                ObjectAnimator.ofFloat(item.mRootView , "rotationY", (endX - startX) * 0.025f).setDuration(0).start();
                            }
                        }
                        break;
                    case MotionEvent.ACTION_UP:
                    case MotionEvent.ACTION_CANCEL:
                        BaseFragment item = fragmentAdapter.getItem(mCurrentPage);
                        ObjectAnimator.ofFloat(item.mRootView, "rotationY", 0).setDuration(250).start();
                        startX = 0;
                        onAnimator= false;
                        break;
                }
                return onAnimator;
            }
        });
    }


    class MyFragmentAdapter extends FragmentPagerAdapter {

        public MyFragmentAdapter(FragmentManager fm) {
            super(fm);
        }

        @Override
        public BaseFragment getItem(int position) {
            return mFragments.get(position);
        }

        @Override
        public int getCount() {
            return mFragments.size();
        }
    }
}

五、写在后面

这篇博客到底算是什么呢,我也不知道,不过在网上找了好久还真找不到类似的……
等什么时候说不定会抽成一个自定义控件,像jazzViewPager一样可以设置各种动画。
如果有什么好的建议可以直接留言~

Demo已经上传完毕,PagerAdapter和FragmentPagerAdapter的都有,欢迎下载学习交流。http://download.csdn.net/detail/u010386612/9399487

你可能感兴趣的:(动画,viewpager,3D,边界,弹性)