android 桌面3d 水平翻转动画_canvas

       关于阅读器翻页动画的实现,有大把的开源项目可以借鉴,但基本都是基于canvas 2D动画,利用缓存bitmap来处理。绘制需要完全手动绘制。有很多优秀的ios 阅读产品,都可以在阅读页插入翻页视频,以及几大ADDSP的广告sdk顺滑接入,炫酷之余很有产品价值。反观android,目前还没找到同样优秀的产品,苦于没有借鉴思路。随不知活路,却已知死路。bitmap+canvas的手动绘制 局限性太大,展现形态未能解耦,内容无法对外扩充。这对于接入pdf,epub等格式的拓展非常困难。

       反过来想想,使用缓存bitmap+canvas最主要的就是目的就是解决缓存翻页与动画的实现,而并非是要处理页面展现的。基于单个页面解耦的目的,缓存机制可以使用 RecyclerView,动画部分就借助 LayoutManager。

恶补了一波RecyclerView与LayoutManager机制与原理,这里不赘述了。

核心思路:利用LayoutManager将itemView 摆放成Z轴的ABC三层,而不是左,中,右水平摆放,再根据滑动手势处理view状态产生动画。

提前需要考虑的问题:事件穿透,滑动冲突,无缝切换动画,引入openGL实现curl仿真翻页动画的坑。

      在下手之前,感觉比较费劲,但是一步步探索实现后,回头再看并不复杂,只是要注意的细节比较多。

目前效果

  • 垂直翻页

android 桌面3d 水平翻转动画_canvas_02

android 桌面3d 水平翻转动画_svg_03

  • 覆盖滑动

android 桌面3d 水平翻转动画_svg_04

  • 横向滚动

android 桌面3d 水平翻转动画_android 桌面3d 水平翻转动画_05

  • 仿真卷曲动画 :取消翻页时,页面会错乱

android 桌面3d 水平翻转动画_android_06

        垂直翻页 和 覆盖滑动 动画实现思路:自定义LayoutManger,处理滑动及itemView显示位置,根据滑动方向将下一个或上一个itemView提前固定作为准备展示的页,利用scale 控制将进入屏幕的itemView显示状态,利用Rotation 实现翻转效果,利用viewZ轴层级准确提供页面层级,利用阴影增加上下页的立体视觉。

核心代码:

if (bookFilpMode == MODE_HORIZATIONAL_SLIDE || bookFilpMode == MODE_FILP) {
                //view 复用时 需要重制属性
                if (itemIndex + 1 <= fastVisiPos) {
                    //计算下一个view
                    View nextView = recycler.getViewForPosition(itemIndex + 1);
                    nextView.setScaleX(1f);
                    nextView.setScaleY(1f);
                    nextView.setTranslationX(0);
                    nextView.setRotationY(0f);
                    if (itemIndex <= focusPosition) {
                        addView(nextView);
                    } else {
                        addView(nextView, 0);
                    }
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                        nextView.setElevation(-1);
                    }
                    measureChildWithMargins(nextView, 0, 0);
                    int nl, nr;
                    nl = 0;
                    nr = nl + getDecoratedMeasurementHorizontal(nextView);
                    layoutDecoratedWithMargins(nextView, nl, 0, nr, bottom);
                }
            }
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                itemView.setElevation(0);
            }
            layoutDecoratedWithMargins(itemView, left, top, right, bottom);


            if (bookFilpMode == MODE_HORIZATIONAL_SLIDE) {
                itemView.setTranslationX(dx);
                final int childCenterX = (right + left) / 2;
                final int parentCenterX = getWidth() / 2;
                boolean isChildLayoutLeft = childCenterX <= parentCenterX;
                if (isChildLayoutLeft) {
                    itemView.setScaleX(1f);
                    itemView.setScaleY(1f);
                } else {
                    itemView.setTranslationX(0);
                    itemView.setScaleX(0);
                    itemView.setScaleY(0);
                }
            } else if (bookFilpMode == MODE_FILP) {
                final int childCenterX = (right + left) / 2;
                final int parentCenterX = getWidth() / 2;
                boolean isChildLayoutLeft = childCenterX <= parentCenterX;
                if (!isChildLayoutLeft) {//隐藏屏幕外的
                    itemView.setTranslationX(0);
                    itemView.setScaleX(0);
                    itemView.setScaleY(0);
                }else {
                    itemView.setScaleX(1F);
                    itemView.setScaleY(1F);
                }
                float offset = (horizontalOffset * 1.0f) % (childWidth * 1.0f);
                float rataitonY = Math.abs(offset / childWidth * 90);
                itemView.setTranslationX(offset);//一边沿着Y轴旋转一边调整位置
                itemView.setCameraDistance(-50000);//无限拉近相机焦点
                itemView.setPivotX(0);
                itemView.setRotationY(-rataitonY);//沿着Y 反转
            }else {
                itemView.setTranslationX(dx);
                itemView.setScaleX(1f);
                itemView.setScaleY(1f);
                itemView.setRotationY(0f);
            }

  仿真卷曲动画实现思路:使用开源库pagecurl的卷曲动画,在Layoutmanger中提前准备好Itemview的缓存bitmap,或者直接使用RecyclerView的缓存bitmap,并提供给curlView。

核心代码:

@Override
        public int getPageCount() {
            return adapter.getItemCount();
        }


        @Override
        public void updatePage(CurlPage page, int width, int height, int index) {
            int lastIndex = curlLayoutManger.getSelectIndex();
            RBLog.log("updatePage index=%s lastIndex=%s", index, lastIndex);
            Message message = handler.obtainMessage();
            message.arg1 = index;
            handler.sendMessage(message);
            recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);
            recyclerView.setDrawingCacheEnabled(true);
            recyclerView.buildDrawingCache();
            Bitmap bitmap = recyclerView.getDrawingCache().copy(Bitmap.Config.RGB_565, false);
            recyclerView.setDrawingCacheEnabled(false);
            recyclerView.destroyDrawingCache();
//                Bitmap front = curlLayoutManger.getItemBitmap().copy(Bitmap.Config.RGB_565,false);
            page.setTexture(bitmap, CurlPage.SIDE_BOTH);
            page.setColor(Color.rgb(0xFF, 0xFF, 0xFF), CurlPage.SIDE_BACK);
        }
    }

       pagecur的引入,十分尴尬。这是我个人能找到最好接入的curl opengl绘制代码。但有一个非常核心的问题,GLSuacefaceView 它不是来自ViewGroup而是View。1.不能与RecyclerView共享同一个事件传递树,2.只能播放View的bitmap 而不是view本身,导致需要提前准备好bitmap。也就是会出现上面动画中,翻页显示错乱的情况,实践了一些思路 都不理想。