實現帶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

發佈了42 篇原創文章 · 獲贊 152 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章