高仿蘑菇街歡迎頁

蘑菇街歡迎頁

蘑菇街歡迎頁.gif

高仿效果

高仿版本.gif

這裏這裏…Demo下載地址

前言

本文將介紹如何對蘑菇街歡迎頁效果進行分析,拆分,並一步步實現1個高仿版本,最重要的設計思路包括以下2點:
1.ViewPager切換時,通過offset偏移量動態修改View元素屬性
2.canvas上精細化的控制旋,移,縮,透明等view屬性變化,進行動態繪製

效果拆解

首先可以把整體效果拆分爲靜態,動態2部分。

整體佈局設計.png

  • 靜態:1個支持4個頁面的ViewPager,每個頁面的展示相對固定,不會根據offset進行改變。

    • 第1-4頁的頂部文案
    • 第4頁的開始按鈕
  • 動態:擺放在viewPager上會變形的自定義View,根據offset動態調整需要繪製的元素的寬高,left,top,透明度等。

    • 第1頁->第2頁
      • 0%->50%,矩形背景高度增加,先上移,再下移
      • 0%->50%,模特圖,文案,下移,漸變消失
      • 50%-100%,左右裂變出2張背景圖,並左右移開
      • 50%->100%,第2頁,頂部,底部圖,漸變顯示
      • 50%->100%,第2頁,3張模特圖逐步放大顯示
      • 0%->100%,底部背景圖跟隨向左偏移,並消失
    • 第2頁->第3頁
      • 0%->50%,矩形背景寬度減少,上移
      • 0%->50%,頂部,底部圖,3張模特圖漸變消失
      • 0%->50%,2張裂變背景圖跟隨向左偏移,並消失
      • 50%->100%,第3頁,6張模特圖逐步放大,漸變顯示
    • 第3頁->第4頁
      • 0%->50%,矩形背景寬度,高度減少,並逆時針進行旋轉
      • 0%->50%,6張模特圖縮小,漸變消失
      • 50%->100%,左右裂變出2張背景圖,並左右移開
      • 50%->100%,頂部模特,文案,漸變顯示
      • 50%->100%,底部3長模特圖逐步放大,漸變顯示

以上是對部分實現細節的分析,抽取;本文demo會全部實現以上變化效果。

實現步驟

1.實現靜態的ViewPager
2.根據offset實現矩形背景變化
3.根據offset實現第1頁底部背景,第2,4頁裂變背景圖變化
4.根據offset實現頁面切換時,每個頁面圖片元素的隱藏,顯示,變形等效果

  • 實現靜態的ViewPager

自定義ViewPager,每個頁面是一個獨立layout,可以自由實現每個頁面的頂部文案,和第4個頁面的Button

public class MoguViewPager extends RelativeLayout {

    private MoguViewPagerAdapter mAdapter;
    private ViewPager mViewPager;
    private List<View> mViewList = new ArrayList<>();
    /** 每個頁面都是一個layout */
    private int[] mLayouts = new int[] {R.layout.guide_view_one, R.layout.guide_view_two, R.layout.guide_view_three,
        R.layout.guide_view_four};

    public MoguViewPager(Context context) {
        super(context);
        init();
    }

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

    private void init() {
        inflate(getContext(), R.layout.layout_mogu_viewpager, this);

        mViewPager = (ViewPager) this.findViewById(R.id.viewpager);

        {
            /** 初始化4個頁面 */
            for (int i = 0; i < mLayouts.length; i++) {
                View view = View.inflate(getContext(), mLayouts[i], null);
                mViewList.add(view);
            }
        }

        mAdapter = new MoguViewPagerAdapter(mViewList, getContext());
        mViewPager.setAdapter(mAdapter);
    }

}
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent">

    <android.support.v4.view.ViewPager
        android:id="@+id/viewpager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_centerHorizontal="true"
        android:clipChildren="false"/>

    <!--這裏準備放個自定義View-->
</RelativeLayout>

第一步完成,實現代碼還是比較簡單的,直接看效果:
第1版.gif

  • 根據offset實現矩形背景變化

自定義會變形的TransforView,在xml佈局中擺放在ViewPager之上

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent">

    <android.support.v4.view.ViewPager
        android:id="@+id/viewpager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_centerHorizontal="true"
        android:clipChildren="false"/>

    <com.listen.test_mogu_viewpager.viewpager.TransforView
        android:id="@+id/transfor_view" android:layout_width="match_parent"
        android:layout_height="450dp"
        android:layout_centerInParent="true"/>
</RelativeLayout>

給ViewPager添加addOnPageChangeListener()監聽,在onPageScrolled()的時候將position,positionOffset,positionOffsetPixels傳遞給TransforView。

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

在TransforView中,首先定義頁面切換時變化的參數,比如第1頁->第2頁切換時,第1頁的矩形背景高度放大40%,上移30dp,下移60dp,則只需要定義FIRST_HEIGHT=0.4,FIRST_TOP1=-30dp,FIRST_TOP2 =60dp三個參數即可。


/**
 * 第1頁->第2頁
 * 0%->50%,矩形背景高度增加40%,先上移30dp,再下移60dp
 */
public static final float FIRST_HEIGHT = 0.4f;// 第1個頁面高度縮放比例,正:放大,負:縮小
public final int FIRST_TOP1 = -dp2px(30);// 第1個頁面top移動距離,正:下移,負:上移
public final int FIRST_TOP2 = dp2px(60);// 第1個頁面top移動距離,正:下移,負:上移
public static final float FIRST_RATE = 0.5f;// 在偏移50%處,進行下一頁的顯示
/**
 * 第2頁->第3頁
 * 0%->50%,矩形背景寬度減少15%,上移20dp
 */
public static final float SECOND_WIDTH = -0.15f;// 第2個頁面寬度縮放比例,正:放大,負:縮小
public final int SECOND_TOP = -dp2px(20);// 第2個頁面top移動距離比例,正:下移,負:上移
public static final float SECOND_RATE = 0.5f;// 在偏移50%處,進行下一頁的顯示
/**
 * 第3頁->第4頁
 * 0%->50%,矩形背景寬度,高度減少10%,並逆時針進行旋轉10度
 */
public static final float THIRD_WIDTH = -0.1f;// 第3個頁面寬度縮放比例,正:放大,負:縮小
public static final float THIRD_HEIGHT = -0.1f;// 第3個頁面高度縮放比例,正:放大,負:縮小
public static final int THIRD_DEGREE = -10;// 第3個頁面角度調整,正:順時針,負:逆時針
public static final float THIRD_RATE = 0.5f;// 在偏移50%處,進行下一頁的顯示

/**
 * 第1頁初始化矩形背景的寬,高,left,top
 */
private float mPage1RectBgDefaultWidth = dp2px(260);
private float mPage1RectBgDefaultHeight = dp2px(230);
private float mPage1RectBgDefaultLeft = getScreenWidth() / 2 - mPage1RectBgDefaultWidth / 2;//left=屏幕寬度/2-矩形寬度/2
private float mPage1RectBgDefaultTop = dp2px(80);

/**
 * 第1頁->第2頁
 * 在第1頁的基礎上進行變化
 * 1.height放大
 * 2.top先上移n,在下移n*2
 */
private float mPage2RectBgDefaultWidth = mPage1RectBgDefaultWidth;
private float mPage2RectBgDefaultHeight = mPage1RectBgDefaultHeight * (1 + FIRST_HEIGHT);// 第2頁的高度=第一頁高度*1.4
private float mPage2RectBgDefaultLeft = mPage1RectBgDefaultLeft;
private float mPage2RectBgDefaultTop = mPage1RectBgDefaultTop + FIRST_TOP1 + FIRST_TOP2;//第2頁的top=第一頁的top-30dp+60dp

/**
 * 第2頁->第3頁
 * 在第2頁的基礎上進行變化
 * 1.寬度縮小
 * 2.top上移
 */
private float mPage3RectBgDefaultWidth = mPage2RectBgDefaultWidth * (1 + SECOND_WIDTH);
private float mPage3RectBgDefaultHeight = mPage2RectBgDefaultHeight;
private float mPage3RectBgDefaultLeft = getScreenWidth() / 2 - mPage3RectBgDefaultWidth / 2;//第3頁的left=屏幕的寬度/2-矩形背景寬度/2
private float mPage3RectBgDefaultTop = mPage2RectBgDefaultTop + SECOND_TOP;

/**
 * 第3頁->第4頁
 * 在第3頁的基礎上進行變化
 * 1.寬度縮小
 * 2.高度縮小
 * 2.逆時針旋轉
 */
private float mPage4RectBgDefaultWidth = mPage3RectBgDefaultWidth * (1 + THIRD_WIDTH);
private float mPage4RectBgDefaultHeight = mPage3RectBgDefaultHeight * (1 + THIRD_HEIGHT);
private float mPage4RectBgDefaultLeft = getScreenWidth() / 2 - mPage4RectBgDefaultWidth / 2;
private float mPage4RectBgDefaultTop = mPage3RectBgDefaultTop;
private float mPage4ModelDefaultWidth = (mPage4RectBgDefaultWidth - padding() * 4) / 3;

TransforView的transfor()方法負責接收position,positionOffset,
positionOffsetPixels,並根據position判斷當前第幾頁,從而決定要實現哪些效果。比如在第1頁->第2頁的0%-50區間時,需要將高度放大40%:mRectBgCurrentHeight =(int) (mPage1RectBgDefaultHeight * (1 + FIRST_HEIGHT * positionOffset * (1 / FIRST_RATE)))。mRectBgCurrentHeight是矩形背景當前的高度,是個動態值,mPage1RectBgDefaultHeight是屏幕處於第1頁時矩形背景的初始值,只要基於這個初始值,根據positionOffset計算偏移的比例,就可以知道當前動態的高度值應該是多少。

public void transfor(int position, float positionOffset, int positionOffsetPixels) {
    mCurrentPageIndex = position;
    if (fromPage1ToPage2(position)) {
        if (positionOffset < FIRST_RATE) {
            /** 第1頁,在0->50%區間偏移 */
            /** 矩形背景,高度放大40% */
            /**
             * 偏移到50%的時候height需要放大40%,defaultHeight=400,targetHeight=400*1.4=560
             *
             * offset=0
             * 400 * (1 + 0.4 * 0 * (1 / 0.5)) = 400
             *
             * offset=0.25
             * 400 * (1 + 0.4 * 0.25 * (1 / 0.5)) = 400 * 1.2 = 480
             *
             * offset=0.5
             * 400 * (1 + 0.4 * 0.5 * (1 / 0.5)) = 400 * 1.4 = 560
             *
             */
            mRectBgCurrentHeight =
                    (int) (mPage1RectBgDefaultHeight * (1 + FIRST_HEIGHT * positionOffset * (1 / FIRST_RATE)));
            /** 矩形背景,向上移動30dp */
            mRectBgCurrentTop = (int) (mPage1RectBgDefaultTop + (FIRST_TOP1 * positionOffset * (1 / FIRST_RATE)));

        } else {
            /** 第1頁,在50%->100%區間偏移 */

            /** 矩形背景,上移30dp後,向下偏移60dp */
            mRectBgCurrentTop =
                    (int) (mPage1RectBgDefaultTop + FIRST_TOP1 + (FIRST_TOP2 * (positionOffset - FIRST_RATE) * 1.0 / (1 - FIRST_RATE)));
        }
    } else if (fromPage2ToPage3(position)) {
        /** 矩形背景,寬度縮小15% */
        mRectBgCurrentWidth = (int) (mPage2RectBgDefaultWidth * (1 + SECOND_WIDTH * positionOffset));
        mRectBgCurrentLeft = getScreenWidth() / 2 - mRectBgCurrentWidth / 2;

        /** 矩形背景,上移20dp */
        mRectBgCurrentTop = (int) (mPage2RectBgDefaultTop + (SECOND_TOP * positionOffset));

    } else if (fromPage3ToPage4(position)) {

        /** 背景矩形的寬度,減少10% */
        mRectBgCurrentWidth = mPage3RectBgDefaultWidth * (1 + THIRD_WIDTH * positionOffset);
        mRectBgCurrentLeft = getScreenWidth() / 2 - mRectBgCurrentWidth / 2;

        /** 背景矩形的高度,減少10% */
        mRectBgCurrentHeight = mPage3RectBgDefaultHeight * (1 + THIRD_HEIGHT * positionOffset);

        /** 逆時針旋轉10度 */
        mRectBgCurrentDegree = THIRD_DEGREE * positionOffset;
    }
     /** 請求重新繪製 */
    postInvalidate();
}

最後在onDraw方法中,調用canvas.drawRoundRect()將計算好寬,高,left,top的圓角矩形在繪製在canvas上即可。

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    RectF rect = new RectF();
    rect.left = mRectBgCurrentLeft;
    rect.top = mRectBgCurrentTop;
    rect.right = rect.left + mRectBgCurrentWidth;
    rect.bottom = rect.top + mRectBgCurrentHeight;

    canvas.rotate(mRectBgCurrentDegree, rect.left + mRectBgCurrentWidth / 2, rect.top + mRectBgCurrentHeight / 2);
    canvas.drawRoundRect(rect, mRectBgDefaultCorner, mRectBgDefaultCorner, mRectBgPaint);
}

第2步:通過ViewPager的偏移offset,實現了矩形背景在頁面間切換時的變化效果,如下:
第2版.gif

  • 根據offset實現第1頁底部背景,第2,4頁裂變圖背景圖變化

在TransforView的init()初始化方法中,獲取並設置圖片的默認寬,高,left,top。這裏封裝了1個ViewModel,裏面記錄了在canvas上繪製圖形需要的bitmap,paint,matrix,width,height,left,top等屬性。在調用ViewModel.create()的時候,通過matrix.postScale()將Bitmap縮放一定比例,以便在矩形背景上進行精確的繪製,比如:矩形背景的200,要在1排展示3張圖,則每張圖的寬度=(200-矩形左邊距-矩形右邊距-中間2張圖的左右邊距)/3。

public ViewModel create() {
    /** 縮放圖片尺寸到合適的比例 */
    matrix.postScale(currentWidth / bitmap.getWidth(), currentHeight / bitmap.getHeight());
    bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
    return this;
}

private void init() {
    /** 第1頁,底部背景圖 */
    mPage1BottomBg =
        new ViewModel(getContext(), R.drawable.one_bottom_bg).alpha(255)
            .width(mPage1RectBgDefaultWidth - padding() * 2)
            .left(mPage1RectBgDefaultLeft + padding())
            // top距離=矩形背景top+height+5dp邊距
            .top(mPage1RectBgDefaultTop + mPage1RectBgDefaultHeight + padding())
            .create();

    /** 第2頁,裂變背景圖 */
    for (int i = 0; i < 2; i++) {
        mPage2Split[i] =
            new ViewModel(getContext(), R.drawable.two_bg).width(mPage2RectBgDefaultWidth)
                .height(mPage2RectBgDefaultHeight)
                .left(mPage2RectBgDefaultLeft)
                .top(mPage2RectBgDefaultTop)
                .create();
    }
    /** 第4頁,2張裂變背景圖 */
    for (int i = 0; i < mPage4Split.length; i++) {
        mPage4Split[i] =
            new ViewModel(getContext(), R.drawable.four_bg)
                    .width(mPage4RectBgDefaultWidth)
                .height(mPage4RectBgDefaultHeight)
                .left(mPage4RectBgDefaultLeft)
                .top(mPage4RectBgDefaultTop);

    }
}

在transfor()中修改圖片left,top,實現移動;第1頁的底部背景圖,根據viewPager向左滑動的距離,跟隨左移,直到消失不可見。在第1頁滑動到50%時,顯示第2頁裂變背景圖,根據offset分別左右平移,第4頁裂變圖原理一致,只是繪製前需要通過Matrix.postRotate()將圖進行旋轉。

private void transfor(int position, float positionOffset, int positionOffsetPixels) {
        if (fromPage1ToPage2(position)) {
            /** 第1頁,底部背景圖,根據頁面pian yi偏移offset向左偏移 */
            mPage1BottomBg.currentLeft = mPage1BottomBg.defaultLeft - positionOffsetPixels;

            if (positionOffset < FIRST_RATE) {

            } else {
                /** 第2頁,計算裂變背景圖的偏移px,並修改透明度漸變顯示 */
                float offset = (mPage1RectBgDefaultWidth + dp2px(15)) * ((positionOffset - FIRST_RATE) * (1 / (1 - FIRST_RATE)));
                mPage2Split[0].currentLeft = mPage2Split[0].defaultLeft - offset;
                mPage2Split[1].currentLeft = mPage2Split[0].defaultLeft + offset;
                /**
                 * 偏移到50%的時候alpha需要爲0,偏移到100%,alpha需要爲255,不過此時positionOffset的取值=0.5~1
                 *
                 * offset=0.5
                 * 255 * (0.5 - 0.5) * (1 / (1 - 0.5)))=255 * 0 = 0
                 *
                 * offset=0.75
                 * 255 * (0.75 - 0.5) * (1 / (1 - 0.5)))=255 * 0.5 = 127.5
                 *
                 * offset=1
                 * 255 * (1 - 0.5) * (1 / (1 - 0.5)))=255 * 1 = 255
                 */
                mPage2Split[0].alpha((int) (255 * (positionOffset - FIRST_RATE) * (1 / (1 - FIRST_RATE))));
                mPage2Split[1].alpha((int) (255 * (positionOffset - FIRST_RATE) * (1 / (1 - FIRST_RATE))));
            }
        } else if (fromPage2ToPage3(position)) {
            if (positionOffset < SECOND_RATE) {

            }
        } else if (fromPage3ToPage4(position)) {

            if (positionOffset < THIRD_RATE) {

            } else {
                /** 顯示第4頁,裂變背景圖,並向左右平移 */
                float offset = (mPage4RectBgDefaultWidth + dp2px(40)) * ((positionOffset - THIRD_RATE) * (1 / (1 - THIRD_RATE)));
                for (int i = 0; i < mPage4Split.length; i++) {
                    mPage4Split[i].matrix.reset();
                    mPage4Split[i].matrix.postScale(mPage4RectBgDefaultWidth / mPage4Split[i].bitmap.getWidth(), mPage4RectBgDefaultHeight / mPage4Split[i].bitmap.getHeight());

                    float currentLeft = 0;
                    if (i == 0) {
                        // 左移
                        currentLeft = mPage4RectBgDefaultLeft - offset;
                    } else if (i == 1) {
                        // 右移
                        currentLeft = mPage4RectBgDefaultLeft + offset;
                    }

                    // 平移
                    mPage4Split[i].matrix.postTranslate(currentLeft, mPage4RectBgDefaultTop);
                    // 旋轉角度
                    mPage4Split[i].matrix.postRotate(THIRD_DEGREE, currentLeft + mPage4RectBgDefaultWidth/2,
                            mPage4RectBgDefaultTop + mPage4RectBgDefaultHeight/2);

                    mPage4Split[i].alpha((int) (255 * ((positionOffset - THIRD_RATE) * (1 / (1 - THIRD_RATE)))));
                }
            }
        }
    }

效果如下:
第3版.gif

  • 4個頁面切換時,實現每個頁面圖片元素的隱藏,顯示,變形等效果

在transfor()中,根據position判斷當前頁數,才知道當前是從第幾頁滑動到第幾頁,該隱藏,或顯示哪些view。

private void transfor(int position, float positionOffset, int positionOffsetPixels) {
    if (fromPage1ToPage2(position)) {
        /** 第1頁,底部背景圖,根據頁面向左偏移 */
        mPage1BottomBg.currentLeft = mPage1BottomBg.defaultLeft - positionOffsetPixels;

        if (positionOffset < FIRST_RATE) {

            /** 第1頁,在0->50%區間偏移 */
            /** 矩形背景,高度放大40%,向上移動30dp */
            transformRectBgFrom1To2Before(positionOffset);

            /** 第1頁,漸漸隱頂部圖,底部圖;透明度漸變消失,偏移到50%時完全消失 */
            stepByHidePage1Views(positionOffset);

        } else {

            /** 第1頁,在50%->100%區間偏移 */
            /** 矩形背景,上移30dp後,向下偏移60dp */
            transformRectBgFrom1To2After(positionOffset);

            /** 第2頁,漸漸顯示頂部,3張模特圖,底部圖 */
            stepByShowPage2Views(positionOffset);

        }
    } else if (fromPage2ToPage3(position)) {
            /** 矩形背景,寬度縮小15%,上移20dp */
           transformRectBgFrom2To3(positionOffset);

           if (positionOffset < SECOND_RATE) {
               /** 第2頁,在0->50%區間偏移,漸漸隱藏頂部,中間,底部,裂變背景圖 */
               stepByHidePage2Views(positionOffset, positionOffsetPixels);
           } else {
               /** 第2頁,在50->100%區間偏移,漸漸顯示第3頁,6張模特圖 */
               stepByShowPage3Views(positionOffset);
           }
    } else if (fromPage3ToPage4(position)) {
            /** 背景矩形的寬度,高度減少10%,逆時針旋轉10度 */
            transformRectBgFrom3To4(positionOffset);

            if (positionOffset < THIRD_RATE) {
                /** 漸漸縮放,隱藏第3頁,6張模特圖 */
                stepByHidePage3Views(positionOffset);

            } else {
                /** 漸漸顯示第4頁,頂部圖,底部3張模特圖,分裂背景圖 */
                stepByShowPage4Views(positionOffset);
            }
    }
}

第1頁->第2頁,偏移區間0%-50%時
* 矩形背景,高度放大40%,向上移動30dp
* 漸漸隱藏第1頁頂部,底部圖;透明度漸變消失,偏移到50%時完全消失

private void transformRectBgFrom1To2Before(float positionOffset) {
        /** 矩形背景,高度放大40% */
        /**
         * 偏移到50%的時候height需要放大40%,defaultHeight=400,targetHeight=400*1.4=560
         *
         * offset=0
         * 400 * (1 + 0.4 * 0 * (1 / 0.5)) = 400
         *
         * offset=0.25
         * 400 * (1 + 0.4 * 0.25 * (1 / 0.5)) = 400 * 1.2 = 480
         *
         * offset=0.5
         * 400 * (1 + 0.4 * 0.5 * (1 / 0.5)) = 400 * 1.4 = 560
         *
         */
        mRectBgCurrentHeight =
            (int) (mPage1RectBgDefaultHeight * (1 + FIRST_HEIGHT * positionOffset * (1 / FIRST_RATE)));
        /** 矩形背景,向上移動30dp */
        mRectBgCurrentTop = (int) (mPage1RectBgDefaultTop + (FIRST_TOP1 * positionOffset * (1 / FIRST_RATE)));
private void stepByHidePage1Views(float positionOffset) {
        /**
         * 偏移到50%的時候alpha需要爲0,view不可見
         *
         * offset=0
         * 255-(255*0.0*(1/0.5)) = 0
         *
         * offset=0.25
         * 255-(255*0.25*(1/0.5)) = 127
         *
         * offset=0.5
         * 255-(255*0.5*(1/0.5)) = 255
         */
        mPage1Top.alpha((int) (255 - (255 * positionOffset * (1 / FIRST_RATE))));
        mPage1Bottom.alpha((int) (255 - (255 * positionOffset * (1 / FIRST_RATE))));

        /** 第1頁,頂部圖向下移動 */
        mPage1Top.currentTop = mPage1Top.defaultTop + (FIRST_TOP2 + FIRST_TOP1) * positionOffset * (1 / FIRST_RATE);

        /** 第1頁,底部圖跟隨頂部圖向下移動 */
        mPage1Bottom.currentTop = mPage1Top.currentTop + mPage1Top.defaultHeight + padding();
    }

第1頁->第2頁,偏移區間50%-100%時
* 矩形背景,向下移動60dp
* 顯示第2頁裂變背景圖,並左右平移
* 逐漸顯示第2頁,頂部,底部圖,3張模特圖

private void transformRectBgFrom1To2After(float positionOffset) {
        /** 快速滑動的時候,可能丟失最後一次繪製,所以需要在這裏調重新設置一次,保證變化完成 */
        mRectBgCurrentHeight = mPage2RectBgDefaultHeight;
        mRectBgCurrentTop = mPage1RectBgDefaultTop + FIRST_TOP1;
        /** 第1頁,在50%->100%區間偏移 */
        /** 矩形背景,在上上偏移30dp後,向下偏移60dp */
        mRectBgCurrentTop =
            (int) (mPage1RectBgDefaultTop + FIRST_TOP1 + (FIRST_TOP2 * (positionOffset - FIRST_RATE) * 1.0 / (1 - FIRST_RATE)));
    }
private void stepByShowPage2Views(float positionOffset) {
        /** 第2頁,頂部圖,跟隨矩形背景下移 */
        mPage2Top.currentTop = mRectBgCurrentTop + padding();

        /** 第2頁,底部圖,跟隨矩形背景下移 */
        mPage2Bottom.currentTop = mPage2Center[0].currentTop + mPage2Center[0].defaultHeight + padding();

        /** 第2頁,計算裂變背景圖的偏移px,並修改透明度漸變顯示 */
        float offset =
            (mPage1RectBgDefaultWidth + dp2px(15)) * ((positionOffset - FIRST_RATE) * (1 / (1 - FIRST_RATE)));
        mPage2Split[0].currentLeft = mPage2Split[0].defaultLeft - offset;
        mPage2Split[1].currentLeft = mPage2Split[0].defaultLeft + offset;
        /**
         * 偏移到50%的時候alpha需要爲0,偏移到100%,alpha需要爲255,不過此時positionOffset的取值=0.5~1
         *
         * offset=0.5
         * 255 * (0.5 - 0.5) * (1 / (1 - 0.5)))=255 * 0 = 0
         *
         * offset=0.75
         * 255 * (0.75 - 0.5) * (1 / (1 - 0.5)))=255 * 0.5 = 127.5
         *
         * offset=1
         * 255 * (1 - 0.5) * (1 / (1 - 0.5)))=255 * 1 = 255
         */
        mPage2Split[0].alpha((int) (255 * (positionOffset - FIRST_RATE) * (1 / (1 - FIRST_RATE))));
        mPage2Split[1].alpha((int) (255 * (positionOffset - FIRST_RATE) * (1 / (1 - FIRST_RATE))));

        /** 第2頁,頂部,底部圖,透明度漸變顯示,偏移量達到100%,完成顯示 */
        mPage2Top.alpha((int) (255 * (positionOffset - FIRST_RATE) * (1 / (1 - FIRST_RATE))));
        mPage2Bottom.alpha((int) (255 * (positionOffset - FIRST_RATE) * (1 / (1 - FIRST_RATE))));

        /** 第2頁,顯示中間3張模特圖 */
        for (int i = 0; i < mPage2Center.length; i++) {
            if (i == 0) {
                /** 第2頁,顯示第1張模特圖 */
                mPage2Center[i].currentWidth =
                    mPage2Center[i].defaultWidth * (positionOffset - FIRST_RATE) * 1 / (1 - FIRST_RATE);
                mPage2Center[i].currentHeight =
                    mPage2Center[i].defaultHeight * (positionOffset - FIRST_RATE) * 1 / (1 - FIRST_RATE);
                mPage2Center[i].alpha((int) (255 * (positionOffset - FIRST_RATE) * (1 / (1 - FIRST_RATE))));
                mPage2Center[i].currentTop = mPage2Top.currentTop + mPage2Top.currentHeight + padding();
            } else {
                /** 第2,3張模特圖,在前1張顯示到一半時才顯示 */
                if (mPage2Center[i - 1].currentWidth >= mPage2Center[i - 1].defaultWidth / 2) {
                    float rate = mPage2Center[i - 1].widthRate() - 0.5f;
                    mPage2Center[i].currentWidth = mPage2Center[i].defaultWidth * (rate * 2);
                    mPage2Center[i].currentHeight = mPage2Center[i].defaultHeight * (rate * 2);
                    /** 第2,3張模特圖,需要根據第1張圖計算left */
                    mPage2Center[i].currentLeft =
                        mPage2Center[0].currentLeft + mPage2Center[0].currentWidth + padding();
                    mPage2Center[i].currentTop = mPage2Top.currentTop + mPage2Top.currentHeight + padding();
                    if (i == 2) {
                        /** 第3張模特圖,根據第2張圖計算top */
                        mPage2Center[i].currentTop =
                            mPage2Center[1].currentTop + mPage2Center[1].currentHeight + padding();
                    }
                    mPage2Center[i].alpha((int) (255 * (positionOffset * rate * 2)));
                } else {
                    mPage2Center[i].alpha(0);
                }
            }
        }
    }

第2頁->第3頁,偏移區間0%-100%時
* 矩形背景,寬度縮小15%
* 矩形背景,上移20dp

private void transformRectBgFrom2To3(float positionOffset) {
        /** 快速滑動的時候,可能丟失最後一次繪製,所以需要在這裏調重新設置一次,保證變化完成 */
        mRectBgCurrentHeight = mPage2RectBgDefaultHeight;
        mRectBgCurrentTop = mPage2RectBgDefaultTop;

        /** 矩形背景,寬度縮小15% */
        mRectBgCurrentWidth = (int) (mPage2RectBgDefaultWidth * (1 + SECOND_WIDTH * positionOffset));
        mRectBgCurrentLeft = getScreenWidth() / 2 - mRectBgCurrentWidth / 2;

        /** 矩形背景,上移20dp */
        mRectBgCurrentTop = (int) (mPage2RectBgDefaultTop + (SECOND_TOP * positionOffset));
    }

第2頁->第3頁,偏移區間0%-50%時
* 裂變背景圖跟隨滑動,向左偏移至消失
* 漸漸減少透明度,隱藏第2頁的頂部圖,3張模特圖,底部圖

private void stepByHidePage2Views(float positionOffset, int positionOffsetPixels) {
        /** 裂變背景圖,跟隨滑動,向左偏移至消失 */
        mPage2Split[0].currentLeft =
            (mPage2Split[0].defaultLeft - mPage1RectBgDefaultWidth - dp2px(15)) - positionOffsetPixels
                * (1 / SECOND_RATE);
        mPage2Split[1].currentLeft =
            (mPage2Split[1].defaultLeft + mPage1RectBgDefaultWidth + dp2px(15)) - positionOffsetPixels
                * (1 / SECOND_RATE);
        mPage2Split[0].alpha((int) (255 - (255 * positionOffset * (1 / SECOND_RATE))));
        mPage2Split[1].alpha((int) (255 - (255 * positionOffset * (1 / SECOND_RATE))));

        /** 頂部圖,3張模特圖,底部圖,跟隨矩形背景上移 */
        mPage2Top.currentTop = mRectBgCurrentTop + padding();
        mPage2Center[0].currentTop = mPage2Top.currentTop + mPage2Top.currentHeight + padding();
        mPage2Center[1].currentTop = mPage2Center[0].currentTop;
        mPage2Center[2].currentTop = mPage2Center[1].currentTop + mPage2Center[1].currentHeight;
        mPage2Bottom.currentTop = mPage2Center[0].currentTop + mPage2Center[0].currentHeight + padding();

        /** 漸漸減少透明度,隱藏第2頁的頂部圖,3張模特圖,底部圖 */
        mPage2Top.alpha((int) (255 - (255 * positionOffset * (1 / SECOND_RATE))));
        mPage2Bottom.alpha((int) (255 - (255 * positionOffset * (1 / SECOND_RATE))));
        for (ViewModel viewModel : mPage2Center) {
            viewModel.alpha((int) (255 - (255 * positionOffset * (1 / SECOND_RATE))));
        }

        /** 因爲矩形背景變窄了,所以漸漸減少第2頁頂部圖,底部圖的寬度,實現跟隨矩形背景寬度變化 */
        mPage2Top.currentWidth = mRectBgCurrentWidth - padding() * 2;
        mPage2Top.currentLeft = mRectBgCurrentLeft + padding();
        mPage2Bottom.currentWidth = mRectBgCurrentWidth - padding() * 2;
        mPage2Bottom.currentLeft = mRectBgCurrentLeft + padding();
        mPage2Bottom.currentLeft = mRectBgCurrentLeft + padding();

        /** 因爲矩形背景變窄了,所以漸漸減少第2,3張模特圖的寬高,left和top,實現跟隨矩形背景寬度變化 */
        mPage2Center[0].currentLeft = mRectBgCurrentLeft + padding();
        mPage2Center[1].currentWidth = mRectBgCurrentWidth - padding() * 3 - mPage2Center[0].defaultWidth;
        mPage2Center[1].currentHeight = mPage2Center[1].currentWidth;
        mPage2Center[1].currentLeft = mPage2Center[0].currentLeft + mPage2Center[0].defaultWidth + padding();

        mPage2Center[2].currentWidth = mRectBgCurrentWidth - padding() * 3 - mPage2Center[0].defaultWidth;
        mPage2Center[2].currentHeight = mPage2Center[2].currentWidth;
        mPage2Center[2].currentLeft = mPage2Center[0].currentLeft + mPage2Center[0].defaultWidth + padding();
        mPage2Center[2].currentTop = mPage2Center[1].currentTop + mPage2Center[1].currentHeight + padding();
    }

第2頁->第3頁,偏移區間50%-100%時
* 依次顯示第3頁,6張模特圖

private void stepByShowPage3Views(float positionOffset) {
        /** 第2頁,在50->100%區間偏移,顯示第3頁,6張模特圖 */
        for (int i = 0; i < mPage3Model.length; i++) {
            if (i == 0) {
                /** 第1張模特圖先顯示 */
                if (mPage3Model[i].paint.getAlpha() < 255) {
                    mPage3Model[i].alpha((int) (255 * (positionOffset - SECOND_RATE) * (1 / (1 - SECOND_RATE))));
                }
            } else {
                /** 其他模特圖在前1張顯示50%透明度的時候再依次展示 */
                if (mPage3Model[i - 1].paint.getAlpha() >= 255 / 2) {
                    float rate = mPage3Model[i - 1].paint.getAlpha() / 255.0f - 0.5f;
                    mPage3Model[i].alpha((int) (255 * (rate * 2)));
                } else {
                    mPage3Model[i].alpha(0);
                }
            }

            /** 6張模特圖,跟隨矩形背景上移 */
            if (i < mPage3ModelResources.length / 2) {
                /** 第1排,3張模特圖的top計算 */
                mPage3Model[i].currentTop = mRectBgCurrentTop + padding();
            } else {
                /** 第1排,3張模特圖的top,需要加上第一排的height */
                mPage3Model[i].currentTop = mRectBgCurrentTop + mPage3ModelDefaultHeight + padding() * 2;
            }
        }
    }

第3頁->第4頁,偏移區間0%-100%時
* 矩形背景,寬度縮小10%
* 矩形背景,高度縮小10%
* 矩形背景,逆時針旋轉10度

private void transformRectBgFrom3To4(float positionOffset) {
        /** 快速滑動的時候,可能丟失最後一次繪製,所以需要在這裏調重新設置一次,保證變化完成 */
        mRectBgCurrentWidth = mPage3RectBgDefaultWidth;
        mRectBgCurrentTop = mPage3RectBgDefaultTop;

        /** 調整第4頁,背景矩形的寬高和角度 */
        /** 背景矩形的寬度,在第3頁調整寬度的基礎上進行縮小 */
        mRectBgCurrentWidth = mPage3RectBgDefaultWidth * (1 + THIRD_WIDTH * positionOffset);
        mRectBgCurrentLeft = getScreenWidth() / 2 - mRectBgCurrentWidth / 2;
        /** 背景矩形的高度,在第2頁調整高度的基礎上進行縮小 */
        mRectBgCurrentHeight = mPage3RectBgDefaultHeight * (1 + THIRD_HEIGHT * positionOffset);
        /** 背景矩形逆時針旋轉 */
        mRectBgCurrentDegree = THIRD_DEGREE * positionOffset;
    }

第3頁->第4頁,偏移區間0%-50%時
* 漸漸縮放,隱藏6張模特圖

private void stepByHidePage3Views(float positionOffset) {
        /** 隱藏第3頁6張模特圖 */
        /** 從第1排,第3-1張開始,依次縮放 */
        for (int i = mPage3ModelResources.length / 2 - 1; i >= 0; i--) {
            if (i == mPage3ModelResources.length / 2 - 1) {
                /** 如果是第1排,第3張,則開始縮放 */
                mPage3Model[i].currentHeight =
                    mPage3Model[i].defaultHeight * (1 - positionOffset * (1 / (1 - THIRD_RATE)));
                mPage3Model[i].currentWidth =
                    mPage3Model[i].defaultWidth * (1 - positionOffset * (1 / (1 - THIRD_RATE)));
            } else {
                /** 如果是第1排,第1/2張,則判斷後1張縮放到一半的時候開始自己的縮放 */
                if (mPage3Model[i + 1].currentHeight <= mPage3Model[i + 1].defaultHeight / 2) {
                    mPage3Model[i].currentHeight = mPage3Model[i].defaultHeight * mPage3Model[i + 1].heightRate() * 2;
                    mPage3Model[i].currentWidth = mPage3Model[i].defaultWidth * mPage3Model[i + 1].heightRate() * 2;
                } else {
                    mPage3Model[i].currentHeight = mPage3Model[i].defaultHeight;
                    mPage3Model[i].currentWidth = mPage3Model[i].defaultWidth;
                }
            }

            /** 跳轉left,top,實現居中縮放 */
            mPage3Model[i].currentLeft =
                mPage3Model[i].defaultLeft + mPage3Model[i].defaultWidth / 2 - mPage3Model[i].currentWidth / 2;
            mPage3Model[i].currentTop =
                mPage3Model[i].defaultTop + mPage3Model[i].defaultHeight / 2 - mPage3Model[i].currentHeight / 2;
        }

        /** 從第1排,第4-6張開始,依次縮放 */
        for (int i = mPage3ModelResources.length / 2; i < mPage3ModelResources.length; i++) {
            if (i == mPage3ModelResources.length / 2) {
                /** 如果是第2排,第1張,則開始縮放 */
                mPage3Model[i].currentHeight =
                    mPage3Model[i].defaultHeight * (1 - positionOffset * (1 / (1 - THIRD_RATE)));
                mPage3Model[i].currentWidth =
                    mPage3Model[i].defaultWidth * (1 - positionOffset * (1 / (1 - THIRD_RATE)));
            } else {
                /** 如果是第2排,第5/6張,則判斷前1張縮放到一半的時候開始自己的縮放 */
                if (mPage3Model[i - 1].currentHeight <= mPage3Model[i - 1].defaultHeight / 2) {
                    mPage3Model[i].currentHeight = mPage3Model[i].defaultHeight * mPage3Model[i - 1].heightRate() * 2;
                    mPage3Model[i].currentWidth = mPage3Model[i].defaultWidth * mPage3Model[i - 1].heightRate() * 2;
                } else {
                    mPage3Model[i].currentHeight = mPage3Model[i].defaultHeight;
                    mPage3Model[i].currentWidth = mPage3Model[i].defaultWidth;
                }
            }

            /** 跳轉left,top,實現居中縮放 */
            mPage3Model[i].currentLeft =
                mPage3Model[i].defaultLeft + mPage3Model[i].defaultWidth / 2 - mPage3Model[i].currentWidth / 2;
            mPage3Model[i].currentTop =
                mPage3Model[i].defaultTop + mPage3Model[i].defaultHeight / 2 - mPage3Model[i].currentHeight / 2;
        }
    }

第3頁->第4頁,偏移區間50%-100%時
* 漸漸顯示頂部圖,底部3張模特圖
* 顯示第4頁裂變背景圖,並左右平移

private void stepByShowPage4Views(float positionOffset) {
        /** 顯示第4頁,裂變背景圖,並向左右平移 */
        float offset = (mPage4RectBgDefaultWidth + dp2px(40)) * ((positionOffset - THIRD_RATE) * (1 / (1 - THIRD_RATE)));
        for (int i = 0; i < mPage4Split.length; i++) {
            mPage4Split[i].matrix.reset();
            mPage4Split[i].matrix.postScale(mPage4RectBgDefaultWidth / mPage4Split[i].bitmap.getWidth(), mPage4RectBgDefaultHeight / mPage4Split[i].bitmap.getHeight());

            float currentLeft = 0;
            if (i == 0) {
                // 左移
                currentLeft = mPage4RectBgDefaultLeft - offset;
            } else if (i == 1) {
                // 右移
                currentLeft = mPage4RectBgDefaultLeft + offset;
            }

            // 平移
            mPage4Split[i].matrix.postTranslate(currentLeft, mPage4RectBgDefaultTop);
            // 旋轉角度
            mPage4Split[i].matrix.postRotate(THIRD_DEGREE, currentLeft + mPage4RectBgDefaultWidth/2,
                    mPage4RectBgDefaultTop + mPage4RectBgDefaultHeight/2);

            mPage4Split[i].alpha((int) (255 * ((positionOffset - THIRD_RATE) * (1 / (1 - THIRD_RATE)))));
        }

        /** 顯示第4頁,頂部模特圖 */
        mPage4Top.alpha((int) (255 * ((positionOffset - THIRD_RATE) * (1 / (1 - THIRD_RATE)))));

        /** 顯示第4頁,底部3張模特圖 */
        for (int i = 0; i < mPage4Model.length; i++) {
            if (i == 0) {
                mPage4Model[i].currentWidth =
                    mPage4Model[i].defaultWidth * ((positionOffset - THIRD_RATE) * (1 / (1 - THIRD_RATE)));
                mPage4Model[i].currentHeight =
                    mPage4Model[i].defaultHeight * ((positionOffset - THIRD_RATE) * (1 / (1 - THIRD_RATE)));
                mPage4Model[i].alpha((int) (255 * (positionOffset - THIRD_RATE) * (1 / (1 - THIRD_RATE))));
            } else {
                if (mPage4Model[i - 1].currentWidth >= mPage4ModelDefaultWidth / 2) {
                    mPage4Model[i].currentWidth =
                        mPage4Model[i].defaultWidth * ((mPage4Model[i - 1].widthRate() - 0.5f) * 2);
                    mPage4Model[i].currentHeight =
                        mPage4Model[i].defaultHeight * ((mPage4Model[i - 1].widthRate() - 0.5f) * 2);
                    mPage4Model[i].currentLeft =
                        mPage4Model[i - 1].currentLeft + mPage4Model[i - 1].currentWidth + padding();
                    mPage4Model[i].alpha((int) (255 * (mPage4Model[i - 1].widthRate() - 0.5f) * 2));
                }
            }
        }
    }

最後在onDraw(),將計算好偏移值的view都繪製出來。

protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        /** 按重疊順序繪製 */
        if (fromPage1ToPage2(mCurrentPageIndex)) {

            /** 繪製第1頁,底部背景圖 */
            drawBitmap(canvas, mPage1BottomBg);

            /** 繪製第2頁,裂變背景圖 */
            drawBitmap(canvas, mPage2Split[0]);
            drawBitmap(canvas, mPage2Split[1]);

            /** 繪製白色矩形背景 */
            drawWhiteRectBackgroud(canvas);

            drawPage1InCanvas(canvas);
            drawPage2InCanvas(canvas);

        } else if (fromPage2ToPage3(mCurrentPageIndex)) {

            /** 繪製第2頁,裂變背景圖 */
            drawBitmap(canvas, mPage2Split[0]);
            drawBitmap(canvas, mPage2Split[1]);

            /** 繪製矩形背景 */
            drawWhiteRectBackgroud(canvas);

            drawPage2InCanvas(canvas);
            drawPage3InCanvas(canvas);

        } else if (fromPage3ToPage4(mCurrentPageIndex)) {
            /** 繪製第4頁,裂變背景圖 */
            drawBitmapMatrix(canvas, mPage4Split[0]);
            drawBitmapMatrix(canvas, mPage4Split[1]);

            /** 繪製矩形背景 */
            drawWhiteRectBackgroud(canvas);

            drawPage3InCanvas(canvas);
            drawPage4InCanvas(canvas);

        } else if (isPage4(mCurrentPageIndex)) {

            /** 繪製第4頁,裂變背景圖 */
            drawBitmapMatrix(canvas, mPage4Split[0]);
            drawBitmapMatrix(canvas, mPage4Split[1]);

            /** 繪製矩形背景 */
            drawWhiteRectBackgroud(canvas);

            drawPage4InCanvas(canvas);
        }
    }

最終效果:
高仿版本.gif

目前還有一些細節的效果,以及適配,性能調優還沒實現。雖然原理不難,不過要真正完整的實現以上效果,也算嘔心瀝血吧!難點就在於如何精細化的控制每個view的屬性,因爲頁面中每個圖片的位置,大小都是在參照其他view的基礎上進行計算後得出的。現在市場上很多APP的歡迎頁都有類似比較動態的效果,原理就是ViewPager+Canvas繪製,掌握了本文的demo,其他實現原理應該是一樣樣的。感興趣的朋友可以Github上下載源碼查看, 註釋還算清晰,有什麼問題頁歡迎提出。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章