前端用图片实现翻书效果

2,028 阅读4分钟

前言

之前有个项目是用vue做的英语早教的小程序,英语采取的是绘本,其中有一模块是英语朗读跟读,要实现类似翻书读语句的效果,特在此记录下实现思路。

整体要实现类似以下的页面:

image_2423d9e2-fea3-48c8-a99e-7b9cd8c81902.jpeg

左右滑动时图片会慢慢翻转到另一侧,然后下一页或上一页的图片页会同步展示出来。

image_07b85106-9a6a-4b68-bb78-7cb4aaaa4630.jpeg

页面里可以听英语,并可以录自己读的语句,页面视图区展示的绘本的内容,是多张图片组成的,这些图片可以翻页,每一页是不同的图片和语句。左右翻页有翻书的动画效果,并会读英语语句。

下面主要讲下翻页的实现,翻页是自己实现,没有用第三方能力。

总体思路

在后台上传绘本的图片时会把图片分成左右两部分,在小程序把第一页左边的图片和最后一页的右边图片固定,然后把中间的图片两张合成一张用以翻页。

翻页组件实现:

<template>
    <view :class="$style['ture-page']">
        <view :class="$style['page-show-container']">
            <!--第一页左边图片-->
            <view :class="[$style['first-last-img'], $style['move-right']]">
                <image :src="firstLeftImage" :mode="mode" alt="" />
            </view>
            <!--左右滑动翻页图片-->
            <view :class="$style['page-right-zone']">
                <view
                    v-for="(item, index) in pageList"
                    ref="turnItem"
                    :key="index"
                    :class="[isTarget ? '' : $style['set-transition'] ,$style['page-item'], $style['common-right-img']]"
                >
                    <view
                        :class="[$style['turn-image-container'],]"
                    >
                        <view :class="[$style['item-img'], $style['turn-common']]">
                            <image :src="item.srcFront" :mode="mode" alt="" />
                        </view>
                        <view :class="[$style['item-img'], $style['turn-common'], $style['item-right-back-img'],]">
                            <image :src="item.srcBack" :mode="mode" alt="" />
                        </view>
                    </view>
                </view>
                <!--最后一页右边图片-->
                <view :class="[$style['common-right-img'], $style['first-last-img']]">
                    <image :src="lastRightImage" :mode="mode" alt="" />
                </view>
            </view>
        </view>
    </view>
</template>

前端拿到后端数据会先处理,把第一页左边图片和最后页面右边图片抽离出来,中间合成一个数组来展示翻页图片,翻页样式如下所示:

<style lang="scss" module>
.ture-page {
    width: 100%;
    height: 100%;
    position: relative;
    z-index: 1;

    .page-show-container {
        display: flex;
        width: 100%;
        height: 100%;
        position: relative;
        z-index: 1;

        .first-last-img {
            height: 100%;
            position: relative;

            > image {
                /* width: 100%; */
                height: 100%;
            }
        }

        .move-right {
            width: 50%;
            text-align: right;
        }

        .page-right-zone {
            text-align: left;
            position: relative;
            width: 50%;
            height: 100%;
            transform-style: preserve-3d;
            transform: translateX(px2vmin(-1));

            .common-right-img {
                width: 100%;
                height: 100%;
                position: absolute;
                top: 0;
                left: 0;
            }

            .set-transition {
                transition: all .9s ease;
            }

            .page-item {
                position: absolute;
                z-index: 1;
                transform-style: preserve-3d;
                transform-origin: left center;
                transform: perspective(800px) rotateY(0deg);

                .turn-image-container {
                    width: 100%;
                    height: 100%;
                    position: relative;
                    transform-style: preserve-3d;
                }

                .turn-image-container .item-img {
                    width: 100%;
                    height: 100%;
                    backface-visibility: hidden;
                    position: relative;

                    & > image {
                        /* width: 100%; */
                        height: 100%;
                    }
                }

                .turn-common {
                    position: absolute;
                    top: 0;
                    left: 0;
                }

                .item-right-back-img {
                    transform: perspective(800px) rotateY(180deg);
                }
            }
        }
    }

    .turn-last-page {
        transform: perspective(800px) rotateY(180deg);
    }

    .turn-next-page {
        transform: perspective(800px) rotateY(-180deg);
    }
}
</style>

因为翻页的图片要用前一页的右边图片和后一页的左边图片合成一张图片,这就涉及要把图片背部给隐藏,后一张图片要翻转180度,隐藏涉及到样式如下:

transform-style: preserve-3d;
backface-visibility: hidden;

后一张图片翻转样式如下:

transform: perspective(800px) rotateY(180deg);

这样翻页的dom和样式就创建好了,下面就是处理翻页的逻辑,主要翻下一页和上一页的逻辑处理,如下:

resetDomsStyleCommon(currentIndex: number): void {
    const queryDoms = this.$refs.turnItem;
    queryDoms.forEach((q, i) => {
        // 用于上一页翻转显示
        if (i === currentIndex + 1) {
            q.style.zIndex = 2;
            q.style.display = 'none';
            return;
        }
        if (i !== currentIndex) {
            q.style.zIndex = 1;
        }
    });
},
// 获取要翻转的dom
getCurrentDom(currentIndex: number): void {
    const queryDoms = this.$refs.turnItem;
    const currentDom = queryDoms[currentIndex];
    return currentDom;
},
turnNextPage(currentIndex: number): void {
    const currentDom = this.getCurrentDom(currentIndex);
    currentDom.style.zIndex = 3;
    // translateX(0)兼容苹果手机翻页闪烁问题
    if (isIOS()) {
        currentDom.style.transform = 'perspective(800px) rotateY(-180deg) translateX(0)';
    }
    else {
        currentDom.style.transform = 'perspective(800px) rotateY(-180deg)';
    }
    const alignNextTimer = setTimeout(() => {
        currentDom.style.textAlign = 'right';
        clearTimeout(alignNextTimer);
    // eslint-disable-next-line no-magic-numbers
    }, 50);
    const turnPageTimer = setTimeout(() => {
        this.resetDomsStyleCommon(currentIndex);
        clearTimeout(turnPageTimer);
    // eslint-disable-next-line no-magic-numbers
    }, 400);
},
turnLastPage(currentIndex: number): void {
    const currentDom = this.getCurrentDom(currentIndex);
    currentDom.style.zIndex = 3;
    currentDom.style.display = 'block';
    this.resetDomsStyleCommon(currentIndex);
    if (currentIndex !== this.pageList.length - 1) {
        const currentLastDom = this.getCurrentDom(currentIndex + 1);
        currentLastDom.style.display = 'block';
    }
    const timer2 = setTimeout(() => {
        currentDom.style.transform = 'perspective(800px) rotateY(0deg)';
        clearTimeout(timer2);
    // eslint-disable-next-line no-magic-numbers
    }, 50);
    const alignLastTimer = setTimeout(() => {
        currentDom.style.textAlign = 'left';
        clearTimeout(alignLastTimer);
    // eslint-disable-next-line no-magic-numbers
    }, 80);
},

turnNextPage方法是下一页的处理逻辑,主要处理有:

  1. 把翻转的当前页z-index层级提高,用以翻转过程展示出来
  2. 给翻转页添加动画
  3. 翻转过来再把翻转页位置右对齐
  4. 重置翻转页样式

turnLastPage翻转上一页逻辑和下一页差不多,就是翻转的方向区别。

所遇问题

翻转在iOS上图片穿透

当我们使用3D transform变换的时候,如果祖先元素没有overflow:hidden/scroll/auto等限制,则会直接忽略自身和其他元素的z-index层叠顺序设置,而直接使用真实世界的3D视角进行渲染。可使用translateZ来处理该问题,具体可参考该文档:www.zhangxinxu.com/wordpress/2…