阅读器开发:实现书签功能的详细步骤
阅读器开发:实现书签功能的详细步骤
在阅读器开发中,书签功能是一个重要的交互特性。本文将详细介绍如何在Vue.js框架下实现书签功能,包括手势识别、书签组件开发、书签添加和删除的逻辑以及书签列表的展示。
书签手势实现
书签功能涉及复杂的手势交互,在阅读器中需要隐藏标题和菜单后才能操作。我们将整个功能拆分为以下几个部分:
- 在阅读器部分监听手势或鼠标的移动事件。通过监听
touchmove
事件,获取手势移动的高度。 - 监听到事件后,需要对外层的
index.vue
进行移动。需要将index.vue
设置为绝对定位,当向下移动时,通过改变top
值来移动界面。 - 在顶部位置增加一个书签组件。当向下移动时书签组件出现。在书签组件中需要进行一些判断,例如当达到临界值时,书签组件要能够自动固定在上面不再移动,同时能做各种逻辑判断。最后在下拉到某值后,在书签组件中,最右侧有一个书签图标,可以通过CSS实现让它固定在右上角。
实现步骤
绑定
touchmove
事件。我们回到EbookReader
组件中,之前定义了一个initGesture
方法,如下:initGesture() { this.rendition.on('touchstart', this.touchStart); this.rendition.on('touchmove', this.touchMove); this.rendition.on('touchend', this.touchEnd); }
但是我们在阅读器上触摸移动却不打印出event,因为rendition对象没有提供touchmove事件的绑定,所以我们这里不能采用下面这个方法实现,也就意味着如果要实现书签功能,这个initGesture就不可行了。
通过蒙版方式实现书签手势功能。我们之前在做左中右点击翻页展示标题菜单栏时做过,现在这个手势下拉仍然需要蒙版实现,所以如下我们在EbookReader中增加一个蒙版,然后把背景颜色改为透明:
.mask { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: transparent; }
然后我们给蒙版添加点击翻页展示菜单栏事件:
this.mask.addEventListener('click', this.showMenu);
打印出来的event中有
offsetX
,就和我们第一次做的那个初始版一样,只不过之前是分左中右三个DOM来实现,这个只是不要了上面的initGesture方法改成了蒙版来实现翻页和展示菜单栏。实现移动事件即下拉。你触摸移动过程中会不断触发move事件,所以打印很多move的event,然后最后松手的时候才会触发end事件,所以end事件的event只打印了一次。然后我们来看看move的event中那个changedTouch:
[0]
表示用一个手指,[1]
表示两个手指,这个我们就没做这么复杂。[0]
中会有clientY
,它是表示当前move这个位置的Y值位置,所以其实第一个move的clientY
是刚开始的Y轴位置,最后那个move的clientY
是最后的Y轴位置,那你移动的位置就是用最后那个clientY-最开始那个clientY
即可得到你move过程中的Y轴偏移量啦。原理:这里的偏移量主要是Y轴的偏移量,就是最开始的Y轴位置和最后的Y轴位置,我们需要拿到这个偏移量,我们将它存入到Vuex中,然后由最外层即
index.vue
去接收这个偏移量,然后对整个index.vue
界面进行下拉。我们不是直接拿的最后move的
clientY
,因为你也不知道哪次的move是最后一个move呀,所以是每次move都去计算这个Y轴偏移量。第一次的move时就是我们要的刚开始的Y轴位置,如下第一次move就把刚开始的Y轴位置给了this.firstOffsetY
;然后第二次往后的move,有this.firstOffsetY
了,所以计算当前move的Y轴位置距离this.firstOffsetY
的距离即offsetY
。因为this.firstOffsetY
是不变的了,而你每次move的Y轴位置都在变,到了最后一次move,即可算出了最终位置与初始位置的偏移量了。moveEnd时应该让偏移量还原,因为屏幕下拉是跟着偏移量一起下拉的,下拉结束后屏幕应该还原了,而是否屏幕是跟着偏移量变的,所以偏移量得还原,屏幕才能还原不再下拉,并且
this.firstOffsetY
也要还原。
在
index.vue
中实现整个屏幕跟着偏移量下移。在index.vue
中首先要把其变成绝对定位,因为下拉的时候是通过绝对定位改变top
值来将整个页面下拉的,然后给它增加个ref
,以便来获取它的DOM来改变它的top
值。那
index.vue
中如何监听到offsetY
的值呢?offsetY
一变化它也跟着变化嘛,通过watch
来监听,如下监听offsetY
的值,传入v
是新的offsetY
,如果v>0
则去调用move()
,move()
中通过去获取整个DOM,然后改变DOM的样式中的top
值即可实现整个屏幕下拉的值等于offsetY
的值。然后我们发现我们这个下拉后松开手了它不会还原,松开手后即end了此时
offsetY
已经被置为0了,但是我们只监听了v>0
没有监听到=0
的情况,所以我们增加如下:watch: { offsetY(v) { if (v > 0) { this.move(); } else if (v === 0) { this.reset(); } } }
回去的时候我们加个过渡动画:
.transition { transition: top 0.3s; }
实现书签组件以及手势动画
这个灰色的背景颜色在App.vue
中设置全局的背景色就能实现:
创建EbookBookmark.vue
,这个书签组件应该引入到index.vue
中并且以绝对定位的形式放在屏幕画面的上方。书签组件中分为左右两侧来做,左侧是下拉图标和文字,右侧是书签图标,为什么不合成一个整体来做,因为右侧那个下拉图标它在书签组件回到顶部上面的时候这个图标是要固定在右上角这个位置,如果三个作为一个整体布局,左侧文字箭头位置不好计算。
左侧箭头是通过旋转180度来实现的,所以我们给了它一个过渡动画:
.arrow {
transition: transform 0.3s;
}
我们创建书签组件叫Bookmark.vue
:
我们来做书签图标那个三角形,通过CSS来做,我们让宽高都为0,然后通过调节边距大小颜色来实现,如下:
.bookmark {
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-bottom: 10px solid #ccc;
}
我们border-width
其中第一个即上的px2rem(10)
变成px2rem(35)
即可拉高如下:
我们把书签图标单独做成一个组件叫Bookmark.vue
,然后引入到书签组件EbookBookmark.vue
中:
我们让Bookmark
组件接收这三个参数,然后对样式进行改变:
我们先来搞color,如下:
下面来实现宽高,下面第二个第四个忘记加rem单位了,记得加上去:
接下来我们设计书签的算法:
监听offsetY
,因为我们下拉书签的移动完全依赖与offsetY
来判断,所以需要监听offsetY
。
要实现这个算法需要更深入的分析整个书签添加的过程,整个书签添加的过程分为3个阶段:
- 第一个阶段,自然下拉阶段,下拉过程中,上面书签组件跟随屏幕移动而移动
- 第二个阶段,是吸顶阶段,这个书签组件我们高是35px,所以我们定它下拉刚好到35px的时候,书签就被吸顶,此时还没有到临界值
- 第三个阶段,可添加状态,此时下拉的长度已经超过临界值,此时箭头翻转为向上,文字内容改变,书签会变色,此时书签已添加,此时松手书签会被固定在右上角
第一个阶段已经实现,我们下拉的时候之前就已经使整个index
跟着offsetY
下拉了。
接下来我们实现第二个阶段,如下定义两个计算属性定义那两个临界值,然后去watch
监听offsetY
,划分出两个阶段:
怎么实现让书签吸在顶部:
其实是整个书签组件被吸在顶部,我们向下拖动的时候,是改变的最外层的top
即index
的top
(index.vue
是包含这个阅读器和书签组件的),但是书签组件本身没有改变top
值,书签组件是跟着index
移动而移动的,那么我们可以单独给书签组件一个反方向的相对位移,到了第二阶段的临界值后,整个index
是向下移动的top
值是正值越来越大的,此时我们让书签组件向上移动负值即反方向越来越大,这个值是等于offset
的值的,只不过是取反。其实就是你继续下拉,它跟着反方向同速度向上,这样子看起来它就像不动一样即被吸在顶部一样。如下:
接下来我们实现下拉文字和图标颜色的变化:
接下来我们实现下拉图标箭头的旋转:
我们在Vuex中定义了一个变量isBookmark
表示当前页是否为书签页。如果当前页为书签页,那我们进入这页时,这页会有一个书签挂在右上角。
监听offsetY
的时候,我们要判断当前页是否为书签页,如果为书签页那下拉的话文字就应该展示下拉删除书签这些。所以如下:
我们封装以下简洁一些:
还有状态1自然下拉的过程,我们主要是做一些归位操作:
还有一个是达到状态3时书签会添加到当前页面中,我们可以用fixed
固定定位。
我们就给书签图标组件绑定一个样式,一个变量isFixed
(表示是否可固定书签图标到右上角),这个变量如果是true
则书签图标组件应该以固定定位固定在右上角,为false
则保持原样:
然后我们在状态1,2中isFixed
都应该为false
,状态3的时候isFixed
应该变成true
,状态4即归位的时候要判断isFixed
即是否有书签图标挂在右上角,如果书签图标挂在右上角即为true
的话应该去修改Vuex中的isBookmark
(判断是否为书签页)为true
,如下书签手势动画就实现了:
先介绍一下EpubCFI:
我们添加一个书签后,到书签列表中是可以看到这个书签当前页内容,就是我们需要把当前内容截取,截取下来之后进行显示,然后我们切换别的页后,点击书签中的某个依然能回到那一页并且判断是书签页即有书签图标在右上角。
这里就有一个很重要的概念,就是去获取当前页的内容,我们要获取这个当前页的内容就需要通过这个Epubcfi,epubcfi可以对应到电子书中每一个文字:
如上,最后的4/10/4[Par5]/3:307
就表示在文本中那个位置。
接下来我们做添加书签的操作,就是获取当前页文本内容以及此页最开始的位置cfi保存到一个变量bookmark
中:
currentLocation
是当前页的信息,有start
的cfi表示当前页第一个字母的位置,有end
的cfi表示当前页的最后一个字母的位置。
我们前面知道cfi感叹号后面的就是指位置的东西:
end部分也是同样的,然后做拼接,通过epubjs中的range方法来获取这个范围内的内容:
去掉空格:
定义一个变量叫bookmark
,主要用来放书签页的信息(有哪些书签页,这些书签页的位置即cfi,这些书签页的文本内容,所以bookmark
是[{xx},{xx}]
这种形式),然后如果bookmark
还没有就去初始化给一个空数组,如果本地缓存中有就从本地缓存中获取,然后把获取到的文本和cfi做成一个对象追加到bookmark
中。这样bookmark
中就存着这些书签页的信息,然后把bookmark
存到本地存储中。这就是添加书签,就是获取书签文本内容以及此页最开始的位置即cfi保存到bookmark
中。
然后我们书签列表的渲染就可以直接获取这个bookmark
的text,点击书签列表中某一页可以通过往display中传cfi即可完成渲染。
接下来我们做删除书签的操作,就是获取当前页开始位置cfi,然后到bookmark
中找到这个cfi把它过滤掉即可:
每一页都应该判断是否是书签页:
此时我们发现我们给某页添加书签后,然后翻页发现每一页都是书签页,这是因为我们缺失对当前页判断是否为书签页的判断,这个我们在刷新的时候加,如下,利用some()方法,如果当前页存在于bookmark
中那么就把Vuex中isbookmark
置为true:
然后还应该在EbookBookmark
组件即书签组件中watch监听isBookmark
的变化,因为上面你只是改变了isBookmark
的值还没做什么操作:
下面我们实现书签的列表功能:
我们建一个书签展示组件叫EbookSlideBookmark
,到EbookSlide
中导入:
到本地存储中获取bookmark
赋值给这里的bookmark
;
书签列表也是用的滚动组件Scroll,遍历bookmark
,把bookmark
里面的每页截取的文本展示出来,然后绑定个点击事件,点击就把当前页的cfi传入display()中渲染处页面: