仿掌閱實現(xiàn)書籍打開動畫

本文已授權(quán)微信公眾號:鴻洋(hongyangAndroid)在微信公眾號平臺原創(chuàng)首發(fā)

一. 前言

上次打開掌閱的時候看到書籍打開動畫的效果還不錯,正好最近也在做閱讀器的項目变屁,所以想在項目中實現(xiàn)一下意狠。

二. 思路

講思路之前敞贡,先看一下實現(xiàn)效果吧:
書籍打開關(guān)閉動畫.gif

看完實現(xiàn)效果,我們再來講一下實現(xiàn)思路:
書籍打開動畫的思路.png
  1. 獲取RecyclerView(或GridView)中的子View里面的ImageView在屏幕的位置摄职,因為獲取的是Window下的位置,所以Y軸位置取出來還要減去狀態(tài)欄的高度蛔垢。
  2. 圖書的封面和內(nèi)容頁(其實是兩個ImageView)設(shè)置成剛剛?cè)〕龅?code>子View里面的ImageView的位置和大小迫悠。
  3. 設(shè)置動畫,這邊縮放動畫的軸心點的計算方式需要注意一下创泄,等下文講解代碼的時候再具體解釋,還有就是利用Camera類(非平常的相機(jī)類)實現(xiàn)的打開和關(guān)閉動畫(如果你對Camera不熟悉饭聚,建議先看GcsSloop大佬的這篇Matrix Camera)搁拙。

三. 具體實現(xiàn)

我會在這個過程中一步一步教你如何實現(xiàn)這個效果:
1. 布局
activity_open_book.xml:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.activity.OpenBookActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycle"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <ImageView
        android:id="@+id/img_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="gone"
        android:contentDescription="@string/app_name" />

    <ImageView
        android:id="@+id/img_first"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:scaleType="centerCrop"
        android:visibility="gone"
        android:contentDescription="@string/app_name" />

</RelativeLayout>

recycler_item_book.xml:
RecylerVIew中的子布局箕速,其實也就是ImageViewTextView,這里就不貼放了兴垦。

2. 動畫
我們只講解旋轉(zhuǎn)動畫,因為旋轉(zhuǎn)動畫中也會涉及縮放動畫探越。想一下扶关,如果想要在界面中實現(xiàn)縮放動畫数冬,我們得找好軸心點,那么拐纱,軸心點的x,y坐標(biāo)如何計算呢揍庄?為了更好的求出坐標(biāo)东抹,我們先來看一張圖:

縮放講解圖.png

我們可以得出這樣的公式:x / pl = vr / pr,而對于pl食茎、vrpr馏谨,則有pl = ml + xvr = w - xpr = pw -pl哎媚,綜合以上的公式喊儡,最終我們可以得出的x = ml * pw / (pw - w)拨与,y的坐標(biāo)可以用同樣的方式求得截珍。下面我們來看代碼:

public class Rotate3DAnimation extends Animation {
    private static final String TAG = "Rotate3DAnimation";

    private final float mFromDegrees;
    private final float mToDegrees;
    private final float mMarginLeft;
    private final float mMarginTop;
    // private final float mDepthZ;
    private final float mAnimationScale;
    private boolean reverse;
    private Camera mCamera;

    // 旋轉(zhuǎn)中心
    private float mPivotX;
    private float mPivotY;

    private float scale = 1;    // <------- 像素密度

    public Rotate3DAnimation(Context context, float mFromDegrees, float mToDegrees, float mMarginLeft, float mMarginTop,
                             float animationScale, boolean reverse) {
        this.mFromDegrees = mFromDegrees;
        this.mToDegrees = mToDegrees;
        this.mMarginLeft = mMarginLeft;
        this.mMarginTop = mMarginTop;
        this.mAnimationScale = animationScale;
        this.reverse = reverse;

        // 獲取手機(jī)像素密度 (即dp與px的比例)
        scale = context.getResources().getDisplayMetrics().density;
    }

    @Override
    public void initialize(int width, int height, int parentWidth, int parentHeight) {
        super.initialize(width, height, parentWidth, parentHeight);

        mCamera = new Camera();
        mPivotX = calculatePivotX(mMarginLeft, parentWidth, width);
        mPivotY = calculatePivotY(mMarginTop, parentHeight, height);
        Log.i(TAG,"width:"+width+",height:"+height+",pw:"+parentWidth+",ph:"+parentHeight);
        Log.i(TAG,"中心點x:"+mPivotX+",中心點y:"+mPivotY);
    }

    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        super.applyTransformation(interpolatedTime, t);

        float degrees = reverse ? mToDegrees + (mFromDegrees - mToDegrees) * interpolatedTime : mFromDegrees + (mToDegrees - mFromDegrees) * interpolatedTime;
        Matrix matrix = t.getMatrix();

        Camera camera = mCamera;
        camera.save();
        camera.rotateY(degrees);
        camera.getMatrix(matrix);
        camera.restore();

        // 修正失真岗喉,主要修改 MPERSP_0 和 MPERSP_1
        float[] mValues = new float[9];
        matrix.getValues(mValues);                //獲取數(shù)值
        mValues[6] = mValues[6] / scale;            //數(shù)值修正
        mValues[7] = mValues[7] / scale;            //數(shù)值修正
        matrix.setValues(mValues);                //重新賦值

        if (reverse) {
            matrix.postScale(1 + (mAnimationScale - 1) * interpolatedTime, 1 + (mAnimationScale - 1) * interpolatedTime,
                    mPivotX - mMarginLeft, mPivotY - mMarginTop);
        } else {
            matrix.postScale(1 + (mAnimationScale - 1) * (1 - interpolatedTime), 1 + (mAnimationScale - 1) * (1 - interpolatedTime),
                    mPivotX - mMarginLeft, mPivotY - mMarginTop);
        }
    }

    /**
     * 計算縮放的中心點的橫坐標(biāo)
     *
     * @param marginLeft  該View距離父布局左邊的距離
     * @param parentWidth 父布局的寬度
     * @param width       View的寬度
     * @return 縮放中心點的橫坐標(biāo)
     */
    public float calculatePivotX(float marginLeft, float parentWidth, float width) {
        return parentWidth * marginLeft / (parentWidth - width);
    }


    /**
     * 計算縮放的中心點的縱坐標(biāo)
     *
     * @param marginTop    該View頂部距離父布局頂部的距離
     * @param parentHeight 父布局的高度
     * @param height       子布局的高度
     * @return 縮放的中心點的縱坐標(biāo)
     */
    public float calculatePivotY(float marginTop, float parentHeight, float height) {
        return parentHeight * marginTop / (parentHeight - height);
    }

    public void reverse() {
        reverse = !reverse;
    }
}

計算縮放點我們在上面已經(jīng)討論過钱床,這里我們就只看函數(shù)applyTransformation(float interpolatedTime, Transformation t)埠居,我們先判斷我們當(dāng)前是打開書還是合上書的狀態(tài)(這兩個狀態(tài)使得動畫正好相反)事期,計算好當(dāng)前旋轉(zhuǎn)度數(shù)再取得Camera纸颜,利用camera.rotateY(degrees)實現(xiàn)書本圍繞Y軸旋轉(zhuǎn),之后拿到我們的矩陣唠倦,圍繞計算出的中心點進(jìn)行縮放涮较。
3. 使用
這一步我們需要將動畫運(yùn)用到我們的界面上去,當(dāng)點擊我們的RecyclerView的時候候齿,我們需要取出RecyclerView中的子View中的ImageView闺属,在適配器中利用監(jiān)聽器傳出:

public interface OnBookClickListener{
    void onItemClick(int pos,View view);
}

接著,我們在OpenBookActivity中實現(xiàn)OnBookClickListener接口润匙,省略了一些代碼:

public class OpenBookActivity extends AppCompatActivity implements Animation.AnimationListener,BookAdapter.OnBookClickListener {
    private static final String TAG = "OpenBookActivity";

    //  一系列變量 此處省略
    ... 
    // 記錄View的位置
    private int[] location = new int[2];
    // 內(nèi)容頁
    private ImageView mContent;
    // 封面
    private ImageView mFirst;
    // 縮放動畫
    private ContentScaleAnimation scaleAnimation;
    // 3D旋轉(zhuǎn)動畫
    private Rotate3DAnimation threeDAnimation;

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

        initWidget();
    }

    private void initWidget() {
        ...

        // 獲取狀態(tài)欄高度
        statusHeight = -1;
        //獲取status_bar_height資源的ID
        int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            //根據(jù)資源ID獲取響應(yīng)的尺寸值
            statusHeight = getResources().getDimensionPixelSize(resourceId);
        }

        initData();
        ...
    }

    // 重復(fù)添加數(shù)據(jù)
    private void initData() {
        for(int i = 0;i<10;i++){
            values.add(R.drawable.preview);
        }
    }

    @Override
    protected void onRestart() {
        super.onRestart();

        // 當(dāng)界面重新進(jìn)入的時候進(jìn)行合書的動畫
        if(isOpenBook) {
            scaleAnimation.reverse();
            threeDAnimation.reverse();
            mFirst.clearAnimation();
            mFirst.startAnimation(threeDAnimation);
            mContent.clearAnimation();
            mContent.startAnimation(scaleAnimation);
        }
    }

    @Override
    public void onAnimationEnd(Animation animation) {
        if(scaleAnimation.hasEnded() && threeDAnimation.hasEnded()) {
            // 兩個動畫都結(jié)束的時候再處理后續(xù)操作
            if (!isOpenBook) {
                isOpenBook = true;
                BookSampleActivity.show(this);
            } else {
                isOpenBook = false;
                mFirst.clearAnimation();
                mContent.clearAnimation();
                mFirst.setVisibility(View.GONE);
                mContent.setVisibility(View.GONE);
            }
        }
    }

    @Override
    public void onItemClick(int pos,View view) {
        mFirst.setVisibility(View.VISIBLE);
        mContent.setVisibility(View.VISIBLE);

        // 計算當(dāng)前的位置坐標(biāo)
        view.getLocationInWindow(location);
        int width = view.getWidth();
        int height = view.getHeight();

        // 兩個ImageView設(shè)置大小和位置
        RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) mFirst.getLayoutParams();
        params.leftMargin = location[0];
        params.topMargin = location[1] - statusHeight;
        params.width = width;
        params.height = height;
        mFirst.setLayoutParams(params);
        mContent.setLayoutParams(params);
        //  設(shè)置內(nèi)容
        Bitmap contentBitmap = Bitmap.createBitmap(width,height, Bitmap.Config.ARGB_8888);
        contentBitmap.eraseColor(getResources().getColor(R.color.read_theme_yellow));
        mContent.setImageBitmap(contentBitmap);
        // 設(shè)置封面
        Bitmap coverBitmap = BitmapFactory.decodeResource(getResources(),values.get(pos));
        mFirst.setImageBitmap(coverBitmap);
        // 設(shè)置封面
        initAnimation(view);
        Log.i(TAG,"left:"+mFirst.getLeft()+"top:"+mFirst.getTop());

        mContent.clearAnimation();
        mContent.startAnimation(scaleAnimation);
        mFirst.clearAnimation();
        mFirst.startAnimation(threeDAnimation);
    }

    // 初始化動畫
    private void initAnimation(View view) {
        float viewWidth = view.getWidth();
        float viewHeight = view.getHeight();

        DisplayMetrics displayMetrics = new DisplayMetrics();
        getWindow().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
        float maxWidth = displayMetrics.widthPixels;
        float maxHeight = displayMetrics.heightPixels;
        float horScale = maxWidth / viewWidth;
        float verScale = maxHeight / viewHeight;
        float scale = horScale > verScale ? horScale : verScale;

        scaleAnimation = new ContentScaleAnimation(location[0], location[1], scale, false);
        scaleAnimation.setInterpolator(new DecelerateInterpolator());  //設(shè)置插值器
        scaleAnimation.setDuration(1000);
        scaleAnimation.setFillAfter(true);  //動畫停留在最后一幀
        scaleAnimation.setAnimationListener(OpenBookActivity.this);

        threeDAnimation = new Rotate3DAnimation(OpenBookActivity.this, -180, 0
                , location[0], location[1], scale, true);
        threeDAnimation.setDuration(1000);                         //設(shè)置動畫時長
        threeDAnimation.setFillAfter(true);                        //保持旋轉(zhuǎn)后效果
        threeDAnimation.setInterpolator(new DecelerateInterpolator());
    }
}

第一個重點是復(fù)寫的OnBookClickListener中的onItemClick方法巍膘,在該方法中:

  • 我們根據(jù)取得的view(實際上是子View中的ImageView),計算出當(dāng)前界面的兩個ImageView的位置和大小璃饱。
  • 計算縮放參數(shù)和播放動畫的順序肪康,展開動畫,和處理動畫結(jié)束后的事件磷支。

第二個重點是中心回到當(dāng)前界面的時候,合上書的動畫廓潜,就是剛剛的動畫倒過來執(zhí)行,在onRestart()方法中執(zhí)行呻畸,執(zhí)行完成之后隱藏兩個ImageVIew悼院。

四. 總結(jié)

總的來說就是CameraAnimation的簡單使用,本人水平有限据途,難免不足昨凡,歡迎提出爽醋。
項目地址:Test
Over~

引用:
Matrix Camera

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市哪痰,隨后出現(xiàn)的幾起案子久妆,更是在濱河造成了極大的恐慌,老刑警劉巖筷弦,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件烂琴,死亡現(xiàn)場離奇詭異,居然都是意外死亡奸绷,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門反症,熙熙樓的掌柜王于貴愁眉苦臉地迎上來畔派,“玉大人线椰,你說我怎么就攤上這事。” “怎么了悔叽?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵爵嗅,是天一觀的道長。 經(jīng)常有香客問我趟庄,道長伪很,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任猫十,我火速辦了婚禮呆盖,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘应又。我一直安慰自己,他們只是感情好株扛,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布洞就。 她就那樣靜靜地躺著,像睡著了一般奖磁。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上秕狰,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天躁染,我揣著相機(jī)與錄音,去河邊找鬼我衬。 笑死,一個胖子當(dāng)著我的面吹牛挠羔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播俱恶,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼范舀,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了聪全?” 一聲冷哼從身側(cè)響起辅辩,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎鹤竭,沒想到半個月后景醇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吝岭,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年散劫,在試婚紗的時候發(fā)現(xiàn)自己被綠了幕帆。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡现恼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出姐呐,到底是詐尸還是另有隱情坠韩,我是刑警寧澤茧泪,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布聋袋,位于F島的核電站,受9級特大地震影響缰泡,放射性物質(zhì)發(fā)生泄漏代嗤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一宜猜、第九天 我趴在偏房一處隱蔽的房頂上張望硝逢。 院中可真熱鬧,春花似錦渠鸽、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至板甘,卻和暖如春详炬,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背呛谜。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工呻率, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人礼仗。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像韭脊,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子饥伊,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容