腾讯视频是一个让我们都喜爱的视频观看平台,用户群体也相当庞大。小编也非常喜欢使用腾讯视频播放软件,在娱乐的时间之中,也给本人来许多快乐。
前言
在学习了小程序之后,为了巩固自身的学习知识和提高实战能力。小编也非常的喜欢写一个属于自己的小程序,而且也发现有些人写的视频类小程序不是很细节,所以小编选了‘腾讯视频’小程序,也开始走上一条“踩坑”的不归路。写下这边文章也是为了纪念自己的痛苦之路,同时也希望给学习小程序的你带来丁点帮助。
项目部分gif演示
1. 前期准备
2. tabBar设计
在设计小程序的tabBar时,直接使用微信小程序官方提供给我们的tabBar。那如何使用微信小程序提供的tabBar来设计腾讯视频小程序的tabBar呢?
a.首先,使用微信开发者工具(或者VScode)打开你新建的小程序项目,找到你的小程序中的app.json文件。在app.json文件中的pages项,新增如下配置:
b.接着,按(ctrl+s)进行保存。此时,微信开发者工具会帮你新建四个页面文件夹,你在pages文件夹打开即可看到这四个文件夹。
c.然后,在pages同级目录下,新建images用来放置程序图片资源。紧接着我们去阿里巴巴矢量图标库搜索自己需要的tabBar对应的图标,把它们下载放置到imgages中去。
d.开始配置tabBar,找到你的小程序中的app.json文件。在app.json文件中的tabBar项,新增如下配置:
"tabBar": { "color": "#000000", "selectedColor": "#FF4500", "list": [ { "pagePath": "pages/main/main", "text": "首页", "iconPath": "images/shouye.png", "selectedIconPath": "images/shouye-action.png" }, { "pagePath": "pages/shortVideo/index", "text": "短视频", "iconPath": "images/duanshiping.png", "selectedIconPath": "images/duanshiping-action.png" }, { "pagePath": "pages/brush/brush", "text": "刷一刷", "iconPath": "images/shuayishua.png", "selectedIconPath": "images/shuayishua-action.png" }, { "pagePath": "pages/mine/mine", "text": "我的", "iconPath": "images/mine.png", "selectedIconPath": "images/mine-action.png" } ] }
e.效果图如下:
3. 数据请求
日常小程序开发过程中基本时通过微信小程序开发工具提供的wx.request来做数据请求,那么怎么可以让自己定义的数据库呢?我们这里采用云开发的微信提供的免费云数据库,做数据请求。在项目的cloudfunctions文件夹下新建几个自己需要的云函数请求响应的数据请求。
以获取搜索建议为例:
1. 云函数部分:
// 云函数入口文件const cloud = require('wx-server-sdk')const env = 'dhyuan'cloud.init()// 获取数据库句柄suggestVideoconst db = cloud.database({ env })// 云函数入口函数exports.main = async (event, context) => { // cloud.getWXContext()直接拿到用户信息 console.log(event.key) // 查询建议的 模糊查询 const _ = db.command let suggestVideo = await db.collection('suggestVideo').where(_.or([{ keyword: db.RegExp({ regexp: '.*' + event.key, options: 'i', }) } ])).get({ success: res => { console.log(res) }, fail: err => { console.log(err) } }) let returnResult = []; for (let i = 0; i < suggestVideo.data.length; i++) { returnResult.push(suggestVideo.data[i]) } return returnResult.sort((a,b) => a.createTime < b.createTime ? 1 : -1)}
2. 搜索页中的 数据请求调用 与函数部分:
// 搜索建议 searchSuggest() { const self = this; //展示标题栏的loding wx.showNavigationBarLoading(); //调用云函数 wx.cloud.callFunction({ name: 'search', data:{ key: self.data.searchKey }, success(res){ // console.log(res); self.setData({ showvideoresult: true, searchsuggest: res.result }) }, fail(err){ // console.log(err); self.setData({ showvideoresult: false }) }, complete(){ //让 loding消失 wx.hideNavigationBarLoading(); } }) },
4. 视频搜索
在小程序开发中,搜索功能是一个比较难实现的功能,尤其涉及数据求以及动态化的实时搜索。下面小编进行一步一步搜索功能的解析搜索的样式设计
以头部查询为例:(其他样式请见github: 传送门)
在设计搜索栏的头部时,我们采用原生的样式渲染方式,这样大家也可以理解其实现的原理,所以就不采用UI框架了,当然如果你想使用UI小编也可以推荐你使用WeUI(微信原生视觉体验一致的基础样式库)。
不多说废话啦,开始动手了。
1. 实现样式设计思路:使用view包裹search的icon和input,让包裹的view边框角变成圆角,在使用行内块级元素使其在一行显示,并使用vertical-align: middle;居中对齐;
2. 搜索头部基本结构
<!-- 搜索框 -->
<view class="page__bd">
<view class="searchsearch-bar">
<view class="search-bar__box">
<icon class="icon-search_in-box" type="search" size="14" color='#EDA8A3'></icon>
<input focus='true' type="text" class="search-bar__input" placeholder="请输入片名、主演或导演" placeholder-style="font-size: 14px;color:#EDA8A3;padding-left:4px" value='{{inputValue}}' bindinput="getsearchKey" bindblur="routeSearchResPage" bindconfirm="searchover" />
<!-- 点击×可以清空正在输入 -->
<view class="icon-clear" wx:if="{{share}}">
<icon type="clear" size="14" color='#EDA8A3' bindtap="clearInput"></icon>
</view>
</view>
<view class="search-bar__cancel-btn {{isCancel?'':'hide'}}" bindtap="cancel">取消</view>
</view>
</view>
3. 样式渲染
/* 搜索bar */
.page__bd {
position: fixed;
top:0;
width:100%;
background-color:#FF4500;
z-index:1;
}
.search-bar {
width:100%;
display: flex;
background-color:#FF4500;
border: 1px solid #DC4238;
}
.search-bar__box{
vertical-align: middle;
height: 65.52rpx;
margin: 20rpx 10rpx 20rpx 25rpx;
background-color:#DE655C;
border-radius: 20rpx;
width: 75%;
padding-left: 30rpx;
padding-right: 30rpx;
display: inline-block;
align-items: center;
}
.icon-search_in-box{
width: 32.76rpx;
height: 32.76rpx;
vertical-align: middle;
display: inline-block;
}
.icon-clear{
width: 32.76rpx;
height: 32px 0.76rpx;
vertical-align: middle;
display: inline-block;
margin-left: 80rpx;
}
.search-bar__input{
vertical-align: middle;
display: inline-block;
}
.search-bar__cancel-btn {
color:#ffffff;
display: inline-block;
font-size:32rpx;
}
搜索功能部分
1. 实现思路:a. 关键字搜索建议:绑定input输入框使其每输入一个值触发关键字搜索建议操作,并展示给用户观看,此时展示你的搜索建议view设置z-index;b. 关键字搜索结果:当你输完关键字回车时,触发搜索结果操作,云函数去查询云数据库并放回相关数据;c.取消:当你点击取消时,此时小程序会放回到首页;d.搜索历史:当你每次输完关键字点击回车时,使用wx.setStorageSync保存数据到本地,当回到搜索主页时,从本次内存取出你查询过的关键字即可。2. 实现关键字搜索建议
页面js求
searchResult() { // console.log(this.data.searchKey) const self = this; //展示标题栏的loding wx.showNavigationBarLoading(); //调用云函数 wx.cloud.callFunction({ name: 'searchResult', data:{ key: self.data.searchKey }, success(res){ // console.log(res); self.setData({ showvideoresult: false, searchresult: res.result }) }, fail(err){ // console.log(err); self.setData({ showvideoresult: false }) }, complete(){ //让 loding消失 wx.hideNavigationBarLoading(); } }) }
云函数:
// 云函数入口文件const cloud = require('wx-server-sdk')const env = 'dhyuan'cloud.init()// 获取数据库句柄suggestVideoconst db = cloud.database({ env })// 云函数入口函数exports.main = async (event, context) => { // cloud.getWXContext()直接拿到用户信息 console.log(event.key) // 查询建议的 模糊查询 const _ = db.command let suggestVideo = await db.collection('suggestVideo').where(_.or([{ keyword: db.RegExp({ regexp: '.*' + event.key, options: 'i', }) } ])).get({ success: res => { console.log(res) }, fail: err => { console.log(err) } }) let returnResult = []; for (let i = 0; i < suggestVideo.data.length; i++) { returnResult.push(suggestVideo.data[i]) } return returnResult.sort((a,b) => a.createTime < b.createTime ? 1 : -1)}
3. 关键字搜索结果
js请求
// 搜索结果 searchResult() { // console.log(this.data.searchKey) const self = this; //展示标题栏的loding wx.showNavigationBarLoading(); //调用云函数 wx.cloud.callFunction({ name: 'searchResult', data:{ key: self.data.searchKey }, success(res){ // console.log(res); self.setData({ showvideoresult: false, searchresult: res.result }) }, fail(err){ // console.log(err); self.setData({ showvideoresult: false }) }, complete(){ //让 loding消失 wx.hideNavigationBarLoading(); } }) }
云函数
// 云函数入口文件const cloud = require('wx-server-sdk')const env = 'dhyuan'cloud.init()// 获取数据库句柄suggestVideoconst db = cloud.database({ env })// 云函数入口函数exports.main = async (event, context) => { // cloud.getWXContext()直接拿到用户信息 console.log(event.key) // 查询建议的 模糊查询 const _ = db.command let serultVideo = await db.collection('searchResult').where(_.or([{ title: db.RegExp({ regexp: '.*' + event.key, options: 'i', }) },{ artists: db.RegExp({ regexp: '.*' + event.key, options: 'i', }) } ])).get({ success: res => { console.log(res) }, fail: err => { console.log(err) } }) let returnResult = []; for (let i = 0; i < serultVideo.data.length; i++) { returnResult.push(serultVideo.data[i]) } return returnResult.sort((a,b) => a.createTime < b.createTime ? 1 : -1)}
特别注意:
搜索中有可能出现“抖动现象”,那么如何解决该现象呢?此时,你需要采用debounce来解决,防止用户多次输入抖动触发搜索,从而导致多次不必要的数据请求。
具体解决如下:
//获取input文本并且实时搜索,动态隐藏组件 getsearchKey: function (e) { // console.log(e.detail.value) //打印出输入框的值 let that = this; if (e.detail.cursor != that.data.cursor) { //实时获取输入框的值 that.setData({ searchKey: e.detail.value }) } if (e.value != "") { //组件的显示与隐藏 that.setData({ showView: false, share: true }) } else { that.setData({ showView: "" }) } if (e.detail.value != "") { //解决 如果输入框的值为空时,传值给搜索建议,会报错的bug that.debounce(that.searchSuggest(), 300) } }, // 去除输入抖动 debounce (func, delay) { let timer let self = this return function (...args) { if (timer) { clearTimeout(timer) } timer = setTimeout(() => { func.apply(self, args) }, delay) } },
4. 取消
js操作
//实现取消功能,停止搜索,返回首页 cancel: function () { wx.switchTab({ url: '/pages/main/main' }) },
5. 搜索历史
js操作
routeSearchResPage: function (e) { // console.log(e.detail.value) // 将数据存入本地 if (this.data.searchKey) { let history = wx.getStorageSync("history") || []; history.push(this.data.searchKey) wx.setStorageSync("history", history); } },
//每次显示变动就去获取缓存,给history,并for出来。 onShow: function () { this.setData({ history: wx.getStorageSync("history") || [] }) }
wxml对应部分
<!-- 搜索历史 --> <view class="{{showView?'hot_keys':'header_view_hide'}}"> <view class="title history">搜索历史</view> <icon type='clear' bindtap="clearHistory" class="title record" color="#DE655C"></icon> <view class='hot_key_box'> <view wx:for="{{history}}" wx:key="{{index}}"> <view class='hot_keys_box_item' data-value='{{item}}' data-index="{{index}}" bindtap="fill_value"> {{item}} </view> </view> </view> </view>
5. 首页部分
首页基本是结构的设计,以及轮播和菜单切换,主要时考验我们wxss的功底和js交互功底。
1.样式结构设计
结构设计基本没什么大的难度,小编就不多废话了,详情见github项目(传送门)。结果如下图:
2.滑动菜单切换&轮播
a. 对于菜单的滑动切换,其实实现非常简单。
在实现之前,你需要了解的几个标签:swiper,swiper-item,scroll-view;滑块视图容器。swiper:其中只可放置swiper-item组件,否则会导致未定义的行为;scroll-view可滚动视图区域。使用竖向滚动时,需要给scroll-view一个固定高度,通过 WXSS 设置 height。组件属性的长度单位默认为px,2.4.0起支持传入单位(rpx/px)。
b. 菜单滑动切换实现思路:给swiper绑定一个bindchange='swiperChange'事件,每当用户滑动页面时,触发'swiperChange'事件,并且在js中定义一个数据变量为curentIndex,通过监听if(e.detail.source == 'touch')其中的touch事件,从而让curentIndex用来记录每次滑动切换的swiper-item,并通过wx:if="{{curentIndex == 0}}来判断当前的swiper-item是否显示,从而达到滑动切换菜单的效果。并且,菜单栏的index也与curentIndex进行判断,从而让指定的菜单高亮显示。
c. 实现过程
1. wxml部分:
<!-- 视频菜单类型 --> <scroll-view class="shouye-menu {{scrollTop > 40 ? 'menu-fixed' : ''}}" scroll-x="{{true}}" scroll-y="{{false}}"> <block wx:for="{{moviesCategory}}" wx:key="{{item.id}}"> <text class="{{curentIndex === index ? 'active' : ''}}" data-id="{{item.id}}" data-index="{{index}}" bind:tap="switchTab" >{{item.name}}</text> </block> </scroll-view><!-- 切换主体部分 -->
<view class="content" style="height:{{ch}}rpx;"> <swiper class="swiper" bindchange='swiperChange' current='{{curentIndex}}'> <swiper-item> <scroll-view scroll-y class="scroll" wx:if="{{curentIndex == 0}}"> </scroll-view> </swiper-item>
</swiper>
<view>
2. js 部分
//改变swiper swiperChange: function(e) {//切换 if(e.detail.source == 'touch') { let curentIndex = e.detail.current; this.setData({ curentIndex }) } }, switchTab(e){ this.setData({ curentIndex:e.currentTarget.dataset.index, toView: e.currentTarget.dataset.id }) }
d. 你可能会踩的“坑”
在你使用 swiper 和scroll-view时,会出现swiper-item中的内容超出可视范围时,无法上下滑动问题。这是你要第一时间想到“swiper高度自适应”这个关键词。小编在这提供几种解决方式。
方案一:
swiper
高度固定,swiper-item
默认绝对定位且宽高100%,每个swiper-item
中内容由固定高度的child组成,然后根据child数量动态计算swiper
高度,初始方案(由于rpx针对屏幕宽度进行自适应,child_height
使用rpx
方便child正方形情况下自适应):
swiper_height = child_height * child_num
屏幕效果仅在宽度375的设备(ip6、ipⅩ)完美契合,其他设备都底部会出现多余空隙,并且在上拉加载过程中,随着内容增加,底部空隙也逐渐变大。
方案二:
swiper_height = child_height * child_num * ( window_width / 750 )
然后并无变化,我们可以看到
child_height
在不同宽度屏幕下,显示的宽高尺寸是不一样的(px
单位),那就尝试使用box在各个屏幕的实际高度进行计算swiper
高度,box的高度可以单独在页面中增加一个固定标签,该标签样式和box宽高保持一致并且隐藏起来,然后在page
的onload
中通过wx.createSelectorQuery()获取标签实际高度baseItemHeight
(px
单位):swiper_height = baseItemHeight * child_num
结果显示原本的ip6、ipⅩ没有问题,另外宽带小于375的ip5上也ok,但是在大于375的设备上还是出现空隙,比如ip的plus系列
方案三:
swiper
底部有一个load标签显示“加载更多”,该标签紧贴box其后,通过wx.createSelectorQuery()
来获取bottom
,然而你会发现bottom
是标签的height
加top
的和。计算底部空隙(暂时忽略“加载更多”标签高度)space_height = swiper_height - load_top刚计算完可以看到在静止状态下,计算出space_height
拿去修改swiper_height
显示空隙刚好被清掉了,但是接着就发现在动过程中获取到的bottom
是不固定的,也就是说数值可能不准确导致space_height
计算错误,显示效果达不到要求
小编采用的是方案一
思路:给swiper一个外部view装载swiper,并给它设置style="height:{{ch}}rpx;",这里的ch为js中的数据变量方便动态修改view的高度。并且在js中的钩子函数中的onLoad函数中编写如下代码:
onLoad: function (options) { wx.getSystemInfo({ success: res => { //转为rpx let ch = (750 / res.screenWidth) * res.windowHeight - 180; this.setData({ ch }) }, })
}
式子中减 180 ,是小编自己调试的数据,具体减多少,可以根据具体情况而定。其实说白了,该方案的设计,基本是与better-scoll的滑动策略基本雷同。
6. 视频播放
1. 主体设计
a. 主体结构设计
<!-- pages/videoDetail/index.wxml --><view class="detailContainer"> <!-- <view class="detail_movice">{{showModalStatus == true ? 'stopScroll' : ''}} <video src="{{entitie.video}}" id="{{entitie.id}}" hidden="{{currentVid != entitie.id}}"></video> <image src="{{entitie.image}}" bind:tap="play" data-vid="{{entitie.id}}" hidden="{{currentVid == entitie.id}}"> <view class="label">{{entitie.duration}}</view> </image> </view> --> <view hidden="{{tvphide}}"> <txv-video vid="{{vid}}" class="{{detailOn ? '' : 'on'}}" width="{{width}}" height="{{height}}" playerid="txv0" autoplay="{{autoplay}}" controls="{{controls}}" title="{{title}}" defn="{{defn}}" vslideGesture="{{true}}" enablePlayGesture="{{true}}" playBtnPosition="center" bindstatechange="onStateChange" bindtimeupdate="onTimeUpdate" showProgress="{{showProgress1}}" show-progress="{{false}}" bindfullscreenchange="onFullScreenChange"></txv-video> </view> <!-- 介绍信息 --> <view class="introduce"> <view class="top"> <text class="introduce-top-title">{{entitie.header}}</text> <text class="introduce-top-text" bind:tap="showModal">简介</text> </view> <view class="center"> <text class="introduce-center-text">8.6分·VIP·视频·全36集·8.8亿</text> </view> <view class="bottom"> <image class="ping" src="../../images/ping.png" lazy-load="{{true}}" bind:tap="" /> <image class="like" src="../../images/like.png" lazy-load="{{true}}" bind:tap="" /> <image class="share" src="../../images/share.png" lazy-load="{{true}}" /> <button data-item='1' plain='true' open-type='share' class='share'></button> </view> </view> <!-- 剧集 --> <view class="episode"> <view class="top"> <text class="episode-top-title">剧集</text> <text class="episode-top-text" bind:tap="showModal">每周一二三20点更新2集,会员多看6集</text> </view> <view class="center"> <block wx:for="{{episodes}}" wx:key="{{index}}"> <view class="gather" data-vid="{{item.vid}}" data-num="{{item.num}}" bind:tap="select"> <view class="{{vid == item.vid ? 'episode-num' : '' }}" data-vid="{{item.vid}}" data-num="{{item.num}}"> {{item.num}} </view> </view> </block> </view> </view> <!-- 精彩片花 --> <view class="clips"> <view class="clips-title"> <text class="clips-title-text">精彩片花</text> </view> <view class="mod_box mod_feeds_list"> <view class="mod_bd"> <view class="figure_box" wx:for="{{clips}}" wx:for-item="video" wx:for-index="index" wx:key="{{video.vid}}"> <view class="mod_poster" data-vid="{{video.vid}}" data-index="{{index}}" bindtap="onPicClick"> <image class="poster" src="{{video.img}}"></image> <!-- 标题、时间和播放按钮 --> <view> <image class="play_icon" src="https://puui.qpic.cn/vupload/0/20181023_1540283106706_mem4262nz4.png/0"></image> <view class="time">{{video.time}}</view> <view class="toptitle two_row">{{video.title}}</view> </view> </view> </view> </view> <view wx:if="{{currVideo.vid}}" style="top:{{top+'px'}};" class="videoContainer"> <txv-video vid="{{currVideo.vid}}" playerid="tvp" autoplay="{{true}}" danmu-btn="{{true}}" width="{{'100%'}}" height="{{'194px'}}" bindcontentchange="onTvpContentChange" bindplay="onTvpPlay" bindended="onTvpEnd" bindpause="onTvpPause" bindstatechange="onTvpStateChanage" bindtimeupdate="onTvpTimeupdate" bindfullscreenchange="onFullScreenChange"></txv-video> <view class='pinList'> <footer></footer> </view> </view> </view> </view> <!-- 弹出框 --> <view animation="{{animationData}}" class="commodity_attr_box" wx:if="{{showModalStatus}}"> <view class="commodity_hide"> <text class="title">{{entitie.header}}</text> <view class="commodity_hide__on" bind:tap="hideModal"></view> </view> <view class="hight" catchtouchmove="stopScroll"> <scroll-view scroll-y class='hightDataView' style="height:{{ch}}rpx;"> <view class="top"> <view class="top-text">每周一二三20点更新2集,会员多看6集</view> <view class="top-descrese"> {{entitie.score}}分·VIP·{{entitie.type}}·全{{entitie.universe}}集·8.8亿 </view> </view> <view class="center"> <scroll-view class="star-list" scroll-x="{{true}}" scroll-y="{{false}}"> <block wx:for="{{entitie.stars}}" wx:key="{{index}}"> <view class="item"> <image class="starImg" src="{{item.starImg}}" lazy-load="ture" /> <view class="name">{{item.name}}</view> </view> </block> </scroll-view> </view> <view class="bottom"> <view class="title">简介</view> <view class="text">{{entitie.original_description}}</view> </view> </scroll-view> </view> </view></view>
b. js交互
// pages/videoDetail/index.jsconst entities = require('../../data/entities.js')const txvContext = requirePlugin("tencentvideo")const config = require('../../modules/config')const episodes = require('../../data/episodes.js')let currentVideo;Page({ /** * 页面的初始数据 */ data: { entitie: null, id: null, entities, clips: null, currentVid:null, episodes: null, tvphide: false, vid: null, title: "电视剧", defn: "超清", changingvid: '', controls: !!config.get('controls'), autoplay: !!config.get('autoplay'), playState: '', showProgress1: true, width: "100%", height: "auto", showModalStatus: false, car:{}, detailOn: true, ch: 0, currentIndex: 0, top: 0, currVideo:{} }, play(event){ const target = event.target; const currentVid = target.dataset.vid; if(this.data.currentVid!=null){ currentVideo.pause(); } if(currentVid){ currentVideo = wx.createVideoContext(`${currentVid}`); this.txvContext.pause(); currentVideo.play(); } this.setData({ currentVid }) }, select(e){ const target = e.target; const currentVid = target.dataset.vid; const num = target.dataset.num; console.log(currentVid, num); this.setData({ vid: currentVid, clips: this.data.episodes[num-1].clips }) this.txvContext = txvContext.getTxvContext('txv0'); this.txvContext.play(); }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { //动态设置 详情的高度 防止滑动失效 wx.getSystemInfo({ success: res => { //转为rpx let ch = (750 / res.screenWidth) * res.windowHeight -478; this.setData({ ch }) }, }) const id= options.id; console.log('id', id); let episode = episodes.find(function(item){ return item.id == id; }) let entitie = entities.find(function(item){ return item.id == id; }) this.setData({ entitie }) //改变page里面的data this.setData({ id: id, episodes: episode.episodes, vid: episode.episodes[0].vid, clips: episode.episodes[0].clips }) // console.log('vid', this.data.vid); this.setData({ controls: !!config.get('controls'), autoplay: !!config.get('autoplay') }) this.txvContext = txvContext.getTxvContext('txv0'); this.txvContext.play(); this.videoContext = wx.createVideoContext('tvp'); }, onTvpPlay: function () { // console.log('play') }, onStateChange: function (e) { this.setData({ playState: e.detail.newstate }) }, onTvpContentChange: function () { }, onTimeUpdate: function (e) { }, requestFullScreen: function () { this.txvContext.requestFullScreen(); }, onFullScreenChange: function () { // console.log('onFullScreenChange!!!') }, onTvpTimeupdate: function(){ }, onTvpPause: function () { }, onTvpStateChanage: function () { }, onPicClick(e) { let dataset = e.currentTarget.dataset; this.currIndex=dataset.index this.setData({ "currVideo.vid":dataset.vid }) // console.log(this.data.currVideo) this.getTop() }, getTop(){ let query = this.createSelectorQuery(); query.selectViewport().scrollOffset(); query .selectAll(`.mod_poster`) .boundingClientRect() .exec(res => { let originTop = 0; this.setData({ top: originTop + this.currIndex * 224.5 }) }); }, /** * 生命周期函数--监听页面初次渲染完成 */ onReady: function () { }, /** * 生命周期函数--监听页面显示 */ onShow: function () { }, /** * 生命周期函数--监听页面隐藏 */ onHide: function () { }, /** * 生命周期函数--监听页面卸载 */ onUnload: function () { }, /** * 页面相关事件处理函数--监听用户下拉动作 */ onPullDownRefresh: function () { }, /** * 页面上拉触底事件的处理函数 */ onReachBottom: function () { }, /** * 用户点击右上角分享 */ onShareAppMessage: function () { console.log('share success!') }, //显示对话框 showModal: function () { // 显示遮罩层 var animation = wx.createAnimation({ duration: 200, timingFunction: "linear", delay: 0 }) this.animation = animation animation.translateY(300).step() this.setData({ animationData: animation.export(), showModalStatus: true, detailOn: false }) setTimeout(function () { animation.translateY(0).step() this.setData({ animationData: animation.export() }) }.bind(this), 200) }, //隐藏对话框 hideModal: function () { // 隐藏遮罩层 var animation = wx.createAnimation({ duration: 200, timingFunction: "linear", delay: 0 }) this.animation = animation; animation.translateY(300).step(); this.setData({ animationData: animation.export(), }) setTimeout(function () { animation.translateY(0).step() this.setData({ animationData: animation.export(), showModalStatus: false, detailOn: true }) }.bind(this), 200) }, // 默认阻止滚动 stopScroll() { return false; }})
c. 你可能会遇到的‘坑’
当你在设计简介的时候,你会发现自己设计的弹出框的内部滑动事件与 当前页的滑动事件一起触发了,那这是为啥呢?仔细想一下,你会发现是冒泡和捕获(详解参考该博文)在搞鬼,相信写过web项目的人对冒泡和捕获非常的熟悉。那么在小程序中也是有的,所以这里你就需要了解滑动穿透这个东西了。那么如何来解决这个问题呐?
解决办法:在简介中需要滑动的view中 加上catchtouchmove="stopScroll",并且在js中定义stopScroll方法并放回false即可解决。具体如下:
1. wxml:
<!-- 简介弹出框 --> <view animation="{{animationData}}" class="commodity_attr_box" wx:if="{{showModalStatus}}"> <view class="commodity_hide"> <text class="title">{{entitie.header}}</text> <view class="commodity_hide__on" bind:tap="hideModal"></view> </view> <view class="hight" catchtouchmove="stopScroll"> <scroll-view scroll-y class='hightDataView' style="height:{{ch}}rpx;"> <view class="top"> <view class="top-text">每周一二三20点更新2集,会员多看6集</view> <view class="top-descrese"> {{entitie.score}}分·VIP·{{entitie.type}}·全{{entitie.universe}}集·8.8亿 </view> </view> <view class="center"> <scroll-view class="star-list" scroll-x="{{true}}" scroll-y="{{false}}"> <block wx:for="{{entitie.stars}}" wx:key="{{index}}"> <view class="item"> <image class="starImg" src="{{item.starImg}}" lazy-load="ture" /> <view class="name">{{item.name}}</view> </view> </block> </scroll-view> </view> <view class="bottom"> <view class="title">简介</view> <view class="text">{{entitie.original_description}}</view> </view> </scroll-view> </view> </view>
2. js部分
// 默认阻止滚动 stopScroll() { return false; }
2.切换电视剧剧集
a. 实现电视剧的剧集切换思路:拿到需要播放视频的vid,将vid替换掉当前的vid,然后执行播放操作。
b.实现步骤:
1. 在.json文件中,配置腾讯视频插件组件。如下:
{ "usingComponents": { "txv-video": "plugin://tencentvideo/video" }}
2. 在wxml中使用,如下:
<view hidden="{{tvphide}}"> <txv-video
vid="{{vid}}"
class="{{detailOn ? '' : 'on'}}"
width="{{width}}"
height="{{height}}"
playerid="txv0"
autoplay="{{autoplay}}"
controls="{{controls}}"
title="{{title}}"
defn="{{defn}}"
vslideGesture="{{true}}"
enablePlayGesture="{{true}}"
playBtnPosition="center"
bindstatechange="onStateChange"
bindtimeupdate="onTimeUpdate"
showProgress="{{showProgress1}}"
show-progress="{{false}}"
bindfullscreenchange="onFullScreenChange"></txv-video></view>
其中,在txv-video中的属性配置含义:
- vid: 腾讯视频的vid,用于拿到该视频资源(必须)
- playerid:playerid必须要全局唯一,可以设置为vid,否则导致视频播放错乱(必须)
- autoplay:是否自动播放;true|false
- controls: 是否显示控制栏(播放,暂停,全屏的按钮显示)
- title:视频标题
- defn:视频清晰度,默认auto,可选值:流畅,标清,高清,超清,蓝光,4K,杜比
其他属性见:腾讯视频插件官方文档
3. js交互
select(e){ const target = e.target; const currentVid = target.dataset.vid; const num = target.dataset.num; console.log(currentVid, num); this.setData({ vid: currentVid, clips: this.data.episodes[num-1].clips }) this.txvContext = txvContext.getTxvContext('txv0'); this.txvContext.play(); }
3. 简介实现
a. 简介部分主要是wxcss的渲染,没有什么逻辑,需要注意的时,点击下拉可以使简介下拉隐藏,并有下拉的过程出现。
b. 主要代码如下:
1. wxml部分:
<view animation="{{animationData}}" class="commodity_attr_box" wx:if="{{showModalStatus}}"> <view class="commodity_hide"> <text class="title">{{entitie.header}}</text> <view class="commodity_hide__on" bind:tap="hideModal"></view> </view> <view class="hight" catchtouchmove="stopScroll"> <scroll-view scroll-y class='hightDataView' style="height:{{ch}}rpx;"> <view class="top"> <view class="top-text">每周一二三20点更新2集,会员多看6集</view> <view class="top-descrese"> {{entitie.score}}分·VIP·{{entitie.type}}·全{{entitie.universe}}集·8.8亿 </view> </view> <view class="center"> <scroll-view class="star-list" scroll-x="{{true}}" scroll-y="{{false}}"> <block wx:for="{{entitie.stars}}" wx:key="{{index}}"> <view class="item"> <image class="starImg" src="{{item.starImg}}" lazy-load="ture" /> <view class="name">{{item.name}}</view> </view> </block> </scroll-view> </view> <view class="bottom"> <view class="title">简介</view> <view class="text">{{entitie.original_description}}</view> </view> </scroll-view> </view> </view>
2. wxss部分:
.commodity_attr_box { width: 100%; height: 100%; color: #fff; overflow: hidden; position: fixed; bottom: 0; top: 420rpx; left: 0; z-index: 998; background-color: #1f1e1e; padding-top: 20rpx;}.commodity_movableView{ width: 100%; height: 2024rpx;}.commodity_hide{ position: relative; height: 50rpx;}.commodity_hide .title{ margin-left: 30rpx; font-size: 35rpx; line-height: 35rpx; font-weight: 40;}.commodity_hide .commodity_hide__on{ width: 50rpx; height: 50rpx; position: absolute; display: inline-block; right: 20rpx;}.commodity_hide .commodity_hide__on::after{ position: absolute; top: 10rpx; content: ''; color: #fff; width: 20rpx; height: 20rpx; border-top: 4rpx solid #ece3e3; border-right: 4rpx solid #ece3e3; -webkit-transform: rotate(135deg); transform: rotate(135deg);}.commodity_attr_box .hightDataView{ width: 100%;}.commodity_attr_box .hightDataView .top{ background-color:#1f1e1e; color: #fff; height: 140rpx; box-sizing: border-box; border-bottom: 4rpx solid #8b8989;}.commodity_attr_box .hightDataView .top .top-text{ font-size: 12px; margin-top: 35rpx; margin-left: 30rpx; margin-right: 50rpx; color: #C0C0C0; line-height: 25px;}.commodity_attr_box .hightDataView .top .top-descrese{ margin-left: 30rpx; font-size: 12px; line-height: 25px; color: #C0C0C0;}.commodity_attr_box .hightDataView .center{ border-bottom: 4rpx solid #8b8989;}.commodity_attr_box .hightDataView .center .star-list { width: 100%; margin-top: 30rpx; margin-left: 20rpx; margin-bottom: 50rpx; white-space: nowrap; box-sizing: border-box;}.commodity_attr_box .hightDataView .center .star-list .item{ text-align: center; display: inline-block; padding:4rpx;}.commodity_attr_box .hightDataView .center .star-list .item image{ width: 80rpx; height: 80rpx; border-radius: 50%; margin: 10rpx;}.commodity_attr_box .hightDataView .center .star-list .item .name{ font-size: 10px; font-weight: normal;}.commodity_attr_box .hightDataView .bottom{ width: 100%;}.commodity_attr_box .hightDataView .bottom .title{ margin-left: 30rpx; font-size: 35rpx; line-height: 35rpx; font-weight: 40; margin-top: 30rpx;}.commodity_attr_box .hightDataView .bottom .text{ font-size: 12px; font-weight: normal; text-indent: 34rpx; margin-top: 20rpx; color: #C0C0C0; margin-left: 30rpx;}
4. 片花部分
在设计片花部分,最主要的是采用什么方式去解决,一次页面渲染加载多个视频问题,很多人直接用for循环放置,多个视频video标签;其实这是非常笨拙的办法;小编在这做了一个比较高级的办法,那就是:页面放置的都是view来存放该视频的vid,当点击相应图片时,触发一个onPicClick事件,此时拿到需要播放的vid,并通知页面我需要播放某个视频了,请给我一个video去播放视频;
此外,你需要注意的是,你这个video出现的位置,必须是你点击的图标位置,这样就不会造成页面图片与视频位置不符的问题了。而且,采用这种办法,页可以减缓你的手机的cpu消耗,该办法算是非常高明的手法了。下面来看下怎么具体实现这种高明的手法吧。
a. wxml部分
<!-- 精彩片花 --> <view class="clips"> <view class="clips-title"> <text class="clips-title-text">精彩片花</text> </view> <view class="mod_box mod_feeds_list"> <view class="mod_bd"> <view class="figure_box" wx:for="{{clips}}" wx:for-item="video" wx:for-index="index" wx:key="{{video.vid}}"> <view class="mod_poster" data-vid="{{video.vid}}" data-index="{{index}}" bindtap="onPicClick"> <image class="poster" src="{{video.img}}"></image> <!-- 标题、时间和播放按钮 --> <view> <image class="play_icon" src="https://puui.qpic.cn/vupload/0/20181023_1540283106706_mem4262nz4.png/0"></image> <view class="time">{{video.time}}</view> <view class="toptitle two_row">{{video.title}}</view> </view> </view> </view> </view> <view wx:if="{{currVideo.vid}}" style="top:{{top+'px'}};" class="videoContainer"> <txv-video vid="{{currVideo.vid}}" playerid="tvp" autoplay="{{true}}" danmu-btn="{{true}}" width="{{'100%'}}" height="{{'194px'}}" bindcontentchange="onTvpContentChange" bindplay="onTvpPlay" bindended="onTvpEnd" bindpause="onTvpPause" bindstatechange="onTvpStateChanage" bindtimeupdate="onTvpTimeupdate" bindfullscreenchange="onFullScreenChange"></txv-video> <view class='pinList'> <footer></footer> </view> </view> </view> </view>
b.js交互部分
onPicClick(e) { let dataset = e.currentTarget.dataset; this.currIndex=dataset.index this.setData({ "currVideo.vid":dataset.vid }) // console.log(this.data.currVideo) this.getTop() }, getTop(){ let query = this.createSelectorQuery(); query.selectViewport().scrollOffset(); query .selectAll(`.mod_poster`) .boundingClientRect() .exec(res => { let originTop = 0; this.setData({ top: originTop + this.currIndex * 224.5 }) }); }
c. 特别注意:
在getTop()方法中的逻辑,此处有些费解,为啥要去设置top值。其目的就是,为去矫正你点击某个图片之后,视频可以在相应位置出现,也就达到点击图片播放的效果。
7. 短视频
该模块实现逻辑,基本与首页差不多,直接看源码即可
实现基本思路:使用swiper,scroll-view实现左右滑动菜单联动,播放视频思路与播放片花思路基本一致。
1. json配置
为啥要配置,因为我们这里使用了腾讯视频插件,以及自己定义的视频尾部的组件,该尾部用于视频分享操作,以及评论操作。配置如下:
{ "usingComponents": { "txv-video": "plugin://tencentvideo/video", "footer": "/components/footer/footer" }}
2. wxml部分
<!-- pages/shortVideo/index.wxml --><scroll-view class="short-menu {{scrollTop > 40 ? 'menu-fixed' : ''}}" scroll-x="{{true}}" scroll-y="{{false}}"> <block wx:for="{{shortCategory}}" wx:key="{{item.id}}"> <view class="name {{curentIndex === index ? 'active' : ''}}" data-id="{{item.id}}" data-index="{{index}}" bind:tap="switchTab"> {{item.name}} </view> </block></scroll-view><view class="content" style="height:{{ch}}rpx;"> <swiper class="swiper" bindchange='swiperChange' current='{{curentIndex}}'> <block wx:for="{{videos}}" wx:key="{{index}}" wx:for-item="videoList"> <swiper-item class="{{index}}"> <scroll-view scroll-y class="scroll" wx:if="{{curentIndex == index}}"> <view class="mod_box mod_feeds_list"> <view class="mod_bd"> <view class="figure_box" wx:for="{{videoList.video}}" wx:for-item="video" wx:for-index="index" wx:key="{{video.vid}}"> <view class="mod_poster" data-vid="{{video.vid}}" data-index="{{index}}" bindtap="onPicClick"> <image class="poster" src="{{video.img}}"></image> <!-- 标题、时间和播放按钮 --> <view> <image class="play_icon" src="https://puui.qpic.cn/vupload/0/20181023_1540283106706_mem4262nz4.png/0"></image> <view class="time">{{video.time}}</view> <view class="toptitle two_row">{{video.title}}</view> </view> </view> </view> </view> <view wx:if="{{currVideo.vid}}" style="top:{{top+'px'}};" class="videoContainer"> <txv-video vid="{{currVideo.vid}}" playerid="tvp" autoplay="{{true}}" danmu-btn="{{true}}" width="{{'100%'}}" height="{{'194px'}}" bindcontentchange="onTvpContentChange" bindplay="onTvpPlay" bindended="onTvpEnd" bindpause="onTvpPause" bindstatechange="onTvpStateChanage" bindtimeupdate="onTvpTimeupdate" bindfullscreenchange="onFullScreenChange"> </txv-video> <view class='pinList'> <footer></footer> </view> </view> </view> </scroll-view> </swiper-item> </block> </swiper></view>
3. js部分
// pages/shortVideo/index.jsconst config = require('../../modules/config')const txvContext = requirePlugin("tencentvideo");const sysInfo =wx.getSystemInfoSync()const shortCategory = require('../../data/shortCategory.js')const videoUrl = require('../../data/videoUrl.js')Page({ /** * 页面的初始数据 */ data: { curentIndex: 0, shortCategory: shortCategory, videos: videoUrl, ch: 0, top: 0, currVideo:{} }, //改变swiper swiperChange: function(e) {//切换 if(e.detail.source == 'touch') { let curentIndex = e.detail.current; this.setData({ curentIndex }) } }, switchTab(e){ this.setData({ curentIndex:e.currentTarget.dataset.index, toView: e.currentTarget.dataset.id }) }, onTvpTimeupdate: function(){ }, onTvpPlay: function () { }, onTvpPause: function () { }, onTvpContentChange: function () { }, onTvpStateChanage: function () { }, onPicClick(e) { let dataset = e.currentTarget.dataset; this.currIndex=dataset.index this.setData({ "currVideo.vid":dataset.vid }) console.log(this.data.currVideo) this.getTop() }, getTop(){ let query = this.createSelectorQuery(); query.selectViewport().scrollOffset(); query .selectAll(`.mod_poster`) .boundingClientRect() .exec(res => { console.log(res) console.log(res[0].scrollTop, res[1][this.currIndex].top) let originTop = res[0].scrollTop; this.setData({ top: originTop + this.currIndex * 224.5 }) }); }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { wx.getSystemInfo({ success: res => { //转为rpx let ch = (750 / res.screenWidth) * res.windowHeight - 80; this.setData({ ch }) }, }) this.videoContext = wx.createVideoContext('tvp'); }, /** * 生命周期函数--监听页面初次渲染完成 */ onReady: function () { }, /** * 生命周期函数--监听页面显示 */ onShow: function () { }, /** * 生命周期函数--监听页面隐藏 */ onHide: function () { }, /** * 生命周期函数--监听页面卸载 */ onUnload: function () { }, /** * 页面相关事件处理函数--监听用户下拉动作 */ onPullDownRefresh: function () { }, /** * 页面上拉触底事件的处理函数 */ onReachBottom: function () { }, /** * 用户点击右上角分享 */ onShareAppMessage: function () { }, // 默认阻止滚动 stopScroll() { return false; }})
8. 我的
关于,我的部分实现基本内容是展示用户头像、姓名,显示是否开通了会员,观看历史,我的看单和设置功能,由于时间关系,小编只实现设置的部分功能
1.wxml部分
<!--miniprogram/pages/mine/mine.wxml--><view class="container"> <view class="header-image"> <image bindtap="bindViewTap" class="userinfo-avatar" src="{{userInfo.avatarUrl}}" mode="cover"></image> <text class="userinfo-nickname">{{userInfo.nickName}}</text> </view> <view class="vip"> <image src="../../images/no_vip.png" mode="widthFix"></image> <text class="h2">VIP暂未开通</text> <text class="bottom" bindtap="lookBans">请点此去开通</text> </view> <view class="history" bindtap="lookBans"> <image class="icon" src="../../images/history.png" mode="widthFix"></image> <text>观看历史</text><text class="fr"></text> </view> <view class="play-list history" bindtap="lookBans"> <image class="icon" src="../../images/view.png" mode="widthFix"></image> <text>我的看单</text><text class="fr"></text> </view> <view class="history" catchtap='navigatItem' data-url='/pages/setting/setting' data-open='true'> <image class="icon" src="../../images/settings.png" mode="widthFix"></image> <text>设置</text><text class="fr"></text> </view></view>
2. js 部分
// miniprogram/pages/mine/mine.jsconst utils = require('../../utils/utils.js')//获取应用实例const app = getApp()Page({ /** * 页面的初始数据 */ data: { userInfo: {} }, navigatItem(e) { return utils.navigatItem(e) }, getUserInfo: function(e) { app.globalData.userInfo = e.detail.userInfo this.setData({ userInfo: e.detail.userInfo }) }, lookBans: function () { const that = this; wx.showModal({ content: '暂时未开发!', showCancel: false, confirmColor: '#FF4500', success(res) { } }) }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { if (app.globalData.userInfo) { this.setData({ userInfo: app.globalData.userInfo }) }else { // 在没有 open-type=getUserInfo 版本的兼容处理 wx.getUserInfo({ success: res => { app.globalData.userInfo = res.userInfo; console.log(res.userInfo) this.setData({ userInfo: res.userInfo }) } }) } }, /** * 生命周期函数--监听页面初次渲染完成 */ onReady: function () { }, /** * 生命周期函数--监听页面显示 */ onShow: function () { }, /** * 生命周期函数--监听页面隐藏 */ onHide: function () { }, /** * 生命周期函数--监听页面卸载 */ onUnload: function () { }, /** * 页面相关事件处理函数--监听用户下拉动作 */ onPullDownRefresh: function () { }, /** * 页面上拉触底事件的处理函数 */ onReachBottom: function () { }, /** * 用户点击右上角分享 */ onShareAppMessage: function () { }})
3. 你需要注意的地方
在实现 设置功能部分时,这个小编在utils中写一个共有的 工具函数,用于页面跳转等操作。utils.js源码如下:
let navigatItem = (e) => { const url = e.currentTarget.dataset.url || '/pages/main/main' const open = e.currentTarget.dataset.open const toUrl = () => { wx.navigateTo({ url, }) } if (open) { toUrl() } else { if (ifLogined()) { toUrl() } else { wx.navigateTo({ url: '/pages/mine/mine' }) } }}module.exports = { navigatItem}
项目完整源码:
9. 结语
小编在写该项目时,踩了不少的坑,这里只写出了几个。虽然有些地方用框架的话会更方便,但是我觉得徒手写项目自己的能力才会得到进阶;最后,感谢大家来学习该文章,感谢你们的支持,欢迎各位来学习讨论。
如果你喜欢这篇文章或者可以帮到你,不妨点个赞吧!