仿掌阅实现书籍打开动画

本文已授权微信公众号:鸿洋(hongyangAndroid)在微信公众号平台原创首发

一. 前言

上次打开掌阅的时候看到书籍打开动画的效果还不错,正好最近也在做阅读器的项目,所以想在项目中实现一下。

二. 思路

讲思路之前,先看一下实现效果吧:
书籍打开关闭动画.gif

看完实现效果,我们再来讲一下实现思路:
书籍打开动画的思路.png
  1. 获取RecyclerView(或GridView)中的子View里面的ImageView在屏幕的位置,因为获取的是Window下的位置,所以Y轴位置取出来还要减去状态栏的高度
  2. 图书的封面和内容页(其实是两个ImageView)设置成刚刚取出的子View里面的ImageView的位置和大小。
  3. 设置动画,这边缩放动画的轴心点的计算方式需要注意一下,等下文讲解代码的时候再具体解释,还有就是利用Camera类(非平常的相机类)实现的打开和关闭动画(如果你对Camera不熟悉,建议先看GcsSloop大佬的这篇Matrix Camera)。

三. 具体实现

我会在这个过程中一步一步教你如何实现这个效果:
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. 动画
我们只讲解旋转动画,因为旋转动画中也会涉及缩放动画。想一下,如果想要在界面中实现缩放动画,我们得找好轴心点,那么,轴心点的x,y坐标如何计算呢?为了更好的求出坐标,我们先来看一张图:

缩放讲解图.png

我们可以得出这样的公式:x / pl = vr / pr,而对于plvrpr,则有pl = ml + xvr = w - xpr = pw -pl,综合以上的公式,最终我们可以得出的x = ml * pw / (pw - w),y的坐标可以用同样的方式求得。下面我们来看代码:

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;

    // 旋转中心
    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;

        // 获取手机像素密度 (即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);                //获取数值
        mValues[6] = mValues[6] / scale;            //数值修正
        mValues[7] = mValues[7] / scale;            //数值修正
        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);
        }
    }

    /**
     * 计算缩放的中心点的横坐标
     *
     * @param marginLeft  该View距离父布局左边的距离
     * @param parentWidth 父布局的宽度
     * @param width       View的宽度
     * @return 缩放中心点的横坐标
     */
    public float calculatePivotX(float marginLeft, float parentWidth, float width) {
        return parentWidth * marginLeft / (parentWidth - width);
    }


    /**
     * 计算缩放的中心点的纵坐标
     *
     * @param marginTop    该View顶部距离父布局顶部的距离
     * @param parentHeight 父布局的高度
     * @param height       子布局的高度
     * @return 缩放的中心点的纵坐标
     */
    public float calculatePivotY(float marginTop, float parentHeight, float height) {
        return parentHeight * marginTop / (parentHeight - height);
    }

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

计算缩放点我们在上面已经讨论过,这里我们就只看函数applyTransformation(float interpolatedTime, Transformation t),我们先判断我们当前是打开书还是合上书的状态(这两个状态使得动画正好相反),计算好当前旋转度数再取得Camera,利用camera.rotateY(degrees)实现书本围绕Y轴旋转,之后拿到我们的矩阵,围绕计算出的中心点进行缩放。
3. 使用
这一步我们需要将动画运用到我们的界面上去,当点击我们的RecyclerView的时候,我们需要取出RecyclerView中的子View中的ImageView,在适配器中利用监听器传出:

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

接着,我们在OpenBookActivity中实现OnBookClickListener接口,省略了一些代码:

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

    //  一系列变量 此处省略
    ... 
    // 记录View的位置
    private int[] location = new int[2];
    // 内容页
    private ImageView mContent;
    // 封面
    private ImageView mFirst;
    // 缩放动画
    private ContentScaleAnimation scaleAnimation;
    // 3D旋转动画
    private Rotate3DAnimation threeDAnimation;

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

        initWidget();
    }

    private void initWidget() {
        ...

        // 获取状态栏高度
        statusHeight = -1;
        //获取status_bar_height资源的ID
        int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            //根据资源ID获取响应的尺寸值
            statusHeight = getResources().getDimensionPixelSize(resourceId);
        }

        initData();
        ...
    }

    // 重复添加数据
    private void initData() {
        for(int i = 0;i<10;i++){
            values.add(R.drawable.preview);
        }
    }

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

        // 当界面重新进入的时候进行合书的动画
        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()) {
            // 两个动画都结束的时候再处理后续操作
            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);

        // 计算当前的位置坐标
        view.getLocationInWindow(location);
        int width = view.getWidth();
        int height = view.getHeight();

        // 两个ImageView设置大小和位置
        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);
        //  设置内容
        Bitmap contentBitmap = Bitmap.createBitmap(width,height, Bitmap.Config.ARGB_8888);
        contentBitmap.eraseColor(getResources().getColor(R.color.read_theme_yellow));
        mContent.setImageBitmap(contentBitmap);
        // 设置封面
        Bitmap coverBitmap = BitmapFactory.decodeResource(getResources(),values.get(pos));
        mFirst.setImageBitmap(coverBitmap);
        // 设置封面
        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());  //设置插值器
        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);                         //设置动画时长
        threeDAnimation.setFillAfter(true);                        //保持旋转后效果
        threeDAnimation.setInterpolator(new DecelerateInterpolator());
    }
}

第一个重点是复写的OnBookClickListener中的onItemClick方法,在该方法中:

  • 我们根据取得的view(实际上是子View中的ImageView),计算出当前界面的两个ImageView的位置和大小。
  • 计算缩放参数和播放动画的顺序,展开动画,和处理动画结束后的事件。

第二个重点是中心回到当前界面的时候,合上书的动画,就是刚刚的动画倒过来执行,在onRestart()方法中执行,执行完成之后隐藏两个ImageVIew

四. 总结

总的来说就是CameraAnimation的简单使用,本人水平有限,难免不足,欢迎提出。
项目地址:Test
Over~

引用:
Matrix Camera

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容