前言
前段时间学习了关于微信小程序的开发,光说不练假把式,所以就打算自己手撸一个微信小程序,而网上电商类小程序太多了,所以就选择了旅游攻略类小程序来练手。这是我第一次写小程序和第一次写文章,不足之处请多包涵,谢谢。下面我会分享我在写小程序的时候遇到的问题和获得的经验,希望能给你带来帮助,也欢迎大佬指正。最后,我要感谢我在写小程序的时候给我帮助的老师和同学,还有百度上所有给过我帮助的有名的无名的作者。我的废话说完了,先上项目效果图。
开发前的准备
- 微信开发者工具
- VsCode
- Easy Mock
项目的所有页面
自定义顶部导航栏组件
微信小程序自带的 [顶部导航栏](https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/page.html) 满足不了实际的需求所以就自己写了一个组件,顶部导航栏要想使用 [自定义组件](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/)(链接中详细介绍了关于自定义组件的使用方法)必须把 app.json 中 window 属性设置为:
"window": {
"navigationBarTextStyle": "black",// 导航栏标题颜色,仅支持 black / white
"navigationStyle": "custom" // 导航栏样式,仅支持以下值:default 默认样式 custom 自定义导航栏,只保留右上角胶囊按钮
}
wxml
<view class='nav-wrap' style='height: {{height*2 + 20}}px; background-color:{{navbarData.backgroundColor}};opacity:{{navbarData.opacity}}'>
<view style="width:100%;height:100%;">
<!-- 城市名 -->
<navigator url="/pages/destination/destination" hover-class="none">
<view class="nav-city" style='margin-top:{{height*2 + 20-36}}px;' wx:if='{{navbarData.showMain}}'>
<text>{{navbarData.cityName}}</text>
<view class="downtips"></view>
</view>
</navigator>
<navigator url="/pages/search/search" hover-class="none">
<!-- 搜索框 -->
<view class="section" style='top:{{height*2 + 20-34}}px;' wx:if='{{navbarData.showMain}}'>
// 这里的搜索框不是一个 input 组件,只是一个 view 可供点击然后跳到搜索页
<view class='search_icon'>
<icon type='search' size='14px'></icon>
</view>
<view class='placehold'> 搜索目的地 / 景点 / 攻略 </view>
</view>
</navigator>
</view>
<!-- 标题 -->
<view wx:if="{{navbarData.title!=''}}"class='nav-title'style='line-height: {{height*2 + 44}}px;'>
{{navbarData.title}}
</view>
<!-- 返回上一级按钮 和 返回主页按钮 -->
<block wx:if="{{navbarData.showCapsule===1}}">
<view class='nav'>
<view class='nav_back' bindtap="_navback">
<image src='/images/back.png'></image>
</view>
<view class="line"></view>
<view class='nav_home' bindtap="_backhome">
<image src='/images/home.png'></image>
</view>
</view>
</block>
</view>
组件中的元素都可以通过当前页面传入组件的数据控制显示与否
js 就写了两个 路由跳转 函数,微信小程序官方文档有很详细的介绍,这里就不多赘述了。
登录界面
初进小程序,会跳到登录授权页面,因为微信小程序不再支持 wx.getUserInfo 接口直接弹出授权框的开发方式,所以这里直接使用 button 组件,并将 open-type 指定为 getUserInfo 类型,获取用户基本信息。
<button style='background:green; color:#fff' open-type="getUserInfo" bindgetuserinfo="bindGetUserInfo"> 同意授权 </button
小程序在授权允许访问用户信息后,又会弹出位置授权框用来获取用户当前所在地,来渲染主页面的数据。调用小程序给的接口 wx.getLocation(需要用户授权) 来获取经纬度,再把获取到的经纬度利用 百度地图开放平台 提供给小程序使用的 API 来获取当前城市的名字,并将城市名字放入缓存,好让主页面获取到。
注意:使用 wx.getLocation()需要在 app.json 中配置
"permission": {
"scope.userLocation": {"desc": "小程序将获取你的位置信息"}
}
登录界面 js
// miniprogram/pages/login/login.js
const app = getApp()
Page({
/**
* 页面的初始数据
*/
data: {
show: false,
// 顶部导航栏数据
navbarData: {
showCapsule: 0, // 是否显示左上角图标 1 表示显示 0 表示不显示
title: '马蜂窝旅游', // 导航栏 中间的标题
backgroundColor: '#354a98', //'#354a98'
opacity: 1,
showMain: 0,
},
// 此页面 页面内容距最顶部的距离
height: app.globalData.height * 2 + 20,
},
bindGetUserInfo(res) {
let that =this
let info = res;
if (info.detail.userInfo) {
wx.login({success: function (res) {that.getPlaceData()
}
})
}
},
/**
* 生命周期函数 -- 监听页面加载
*/
onLoad: function (options) {
let that = this;
// 页面加载时判断用户是否授权过,如果授权过直接跳到主页面,没有就显示授权按钮
wx.getUserInfo({success: function (res) {
wx.switchTab({url: '/pages/main/index'})
},
fail(err) {
that.setData({show: true})
}
})
},
// 获取城市名字
getCityName(location) {return new Promise((resolve, reject) => {
let that = this;
var e = {
coord_type: "gcj02",
output: "json",
pois: 0,
ak: '',// 放上自己的 ak 密钥 密钥申请见上文百度地图开方平台链接
sn: "",
timestamp: ""
};
e.location = location;
wx.request({
url: "https://api.map.baidu.com/geocoder/v2/",
data: e,
header: {"content-type": "application/json"},
method: "GET",
success: function (t) {
let currentCity = t.data.result.addressComponent.city;
if (currentCity.slice(currentCity.length - 1) == "市") {currentCity = currentCity.slice(0, currentCity.length - 1)
}
wx.setStorageSync('currentCity', currentCity)
resolve(currentCity) // 通过城市名字 请求城市数据
}
})
})
},
// 获取经纬度
getLocation() {return new Promise((resolve, reject) => {
wx.getLocation({
type: 'wgs84',
success(res) {
const latitude = res.latitude
const longitude = res.longitude
let location = latitude + ',' + longitude
console.log(location)
resolve(location) // 获取城市名字
}
})
})
},
getPlaceData() { // 获取地理信息
let that = this
this.getLocation().then((val) => {return that.getCityName(val)
}).then(()=>{
wx.switchTab({url: '/pages/main/index'})
})
}
})
主页面
写小程序的时候我不知道主页面有两种样式,等我知道的时候已经写了不少东西了,所以就没有写成组件了,代码看起来就很冗长,这是我的失误(MangFu),希望你在想好写什么小程序的时候,一定要把小程序的页面结构想好来否则就会和我一样,要改的话就要改很多地方。
- 普通城市页面
- 热门城市页面
进入主页是,页面会先获取到缓存中的城市名字,再通过城市名字去请求数据,再根据请求到的数据中的 ishot 属性,如果 ishot 属性为真,就显示热门城市的页面,反之就显示普通城市的页面
‘我的’页面
‘我的’页面中主要是为了显示用户收藏的内容
景点详情页
因为种种原因(lan)页面中的大半数据没有放到 Easy Mock 里,马蜂窝本来就以大数据出名,数据 ttm 多了。
洲 / 国家 / 城市列表页
这个页面的布局分为三部分,头部搜索框用绝对定位定死、左部各大洲的列表用绝对定位定死,右部各大洲的国家是一个微信小程序自带的组件 scroll-view
wxml
<!-- pages/destination/destination.wxml -->
<nav-bar navbar-data='{{navbarData}}'></nav-bar>
<view class="destination" style='top: {{height}}px'>
<!-- 头部 -->
<view class="des_head">
<navigator url="/pages/search/search" hover-class="none">
<view class="des_search">
<view class="des_search_icon">
<icon type='search' size='30rpx' color="#000000"></icon>
</view>
搜索目的地
</view>
</navigator>
</view>
<!-- 左部 -->
<view class="des_continents">
<view class="des_continent {{curIndex===index?'add':''}}}"wx:for="{{continents}}"wx:for-item="continent"wx:key='{{index}}'data-index='{{index}}'bindtap="switch_des">
<view class='des_continent_name {{curIndex===index?"on":""}}}'>{{continent.name}}</view>
</view>
</view>
<!-- 右部 -->
<scroll-view class='des_cities' scroll-y>
<block wx:if="{{curIndex==0}}">
<view class="des_cities_content" wx:for="{{continents[curIndex].cities}}" wx:key="{{index}}" wx:for-item="des_city">
<view class="des_cities_title">{{des_city.title}}</view>
<view class="des_city" wx:for="{{des_city.city}}" wx:key="{{index}}" bindtap='goMain' data-city_name="{{item.city_name}}">
{{item.city_name}}
</view>
</view>
</block>
<block wx:else>
<view class="des_area" wx:for="{{continents[curIndex].cities}}" wx:key="{{index}}" wx:for-item="des_city" bindtap='goMain' data-city_name="{{des_city.city_name}}">
<view class="des_img">
<image src="{{des_city.img}}" />
</view>
<view class="des_city_name">{{des_city.city_name}}</view>
</view>
</block>
</scroll-view>
</view>
js
// pages/destination/destination.js
const app = getApp()
Page({
/**
* 页面的初始数据
*/
data: {
<!-- 顶部导航栏数据 -->
navbarData: {
showCapsule: 1, // 是否显示左上角图标 1 表示显示 0 表示不显示
title: '目的地切换', // 导航栏 中间的标题
backgroundColor: '#fff',// 背景颜色
showMain: 0 /// 显示搜索框
},
height: app.globalData.height * 2 + 20,
continents: [],
curIndex: 0 // 当前洲的索引值
},
<!-- 左部各大洲的点击事件,来改变右边显示的内容,并且改变自身样式 -->
switch_des(e) {
let curIndex = e.currentTarget.dataset.index;
this.setData({curIndex,})
},
<!-- 右部国家 / 城市的点击事件,获取点击的元素上绑定的国家 / 城市的名字,放入缓存,并跳转到主页 -->
goMain(e){
const city_name = e.currentTarget.dataset.city_name;
wx.setStorageSync('currentCity', city_name)
wx.switchTab({url: '/pages/main/index'})
},
/**
* 生命周期函数 -- 监听页面加载
*/
onLoad: function (options) {
let that = this
<!-- 请求数据 -->
wx.request({
url: 'https://www.easy-mock.com/mock/5ca457f04767c3737055c868/example/mafengwo/continents',
success:(res)=>{
that.setData({continents: res.data.continents})
}
})
}
}
搜索页
实现的功能
点击切换列表
以主页为例
其实所有的切换列表功能都差不多,实现方法就是在被点击元素上设置一个 自定义属性 (data-*) 为唯一索引值,用 bind-tap 绑定一个点击事件,通过点击事件获取这个唯一索引值,再通过唯一索引值去数据源找到想要的内容,然后通过数据控制页面上显示的内容, 在 data 数据源中设置一个数据如 mcurIndex, 表示当前选择的元素,用来区别于其他元素,显示不同的样式。
wxml
<view class='menu_list'>
<!-- {{mcurIndex===index?"on":""}} 表示如果自身的索引值为当前选择的元素索引值时,添加一个类名‘on’-->
<view class='list {{mcurIndex===index?"on":""}}'wx:for="{{placeData.allGuide}}"data-mindex="{{index}}"bindtap='selected_menu'wx:key="{{index}}">
{{item.name}}
</view>
</view>
js
selected_menu(e) {
this.setData({
mcurIndex: e.target.dataset.mindex,
size: 0,
showend: false
})
<!-- 调用自己写的函数来获取要显示的内容的数据 -->
this.bitiyan()}
滑动页面改变顶部导航栏的可见度和上拉加载
-
滑动页面改变顶部导航栏的可见度
以主页为例
这里的实现方法是使用 scroll-view 组件,组件中有个 bindscroll 属性,会在页面滚动时触发 bindscroll 绑定的事件还会给函数传递一个对象 event, 其中的 scrollTop 属性是我们需要的,根据 scrollTop 知道页面滚动了多少,然后动态设置要传给组件的数据里的 opacity 属性。
<scroll-view class="main_scro" scroll-y bindscroll="scroll" bindscrolltolower="bindDownLoad">
</scroll-view>
js
scroll(e) {
let opacity = 0;
if (e.detail.scrollTop < 60) {opacity = (e.detail.scrollTop / 100).toFixed(1);
} else {opacity = 1;}
this.data.navbarData.opacity = opacity;
if (e.detail.scrollTop<10){
this.setData({shownav: false})
}else{
this.setData({shownav: true})
}
this.setData({navbarData: this.data.navbarData,})
}
-
上拉加载
以主页为例
这里的实现方法在 scroll-view 组件中加 bindscrolltolower 属性,会在页面触底时触发 bindscrolltolower 绑定的事件。
<scroll-view class="main_scro" scroll-y bindscroll="scroll" bindscrolltolower="bindDownLoad">
</scroll-view>
bindDownLoad() {
let part = 0; // 已经显示的数据长度
let all = 0; // 总的数据长度
<!-- 判断当前城市是否为热门城市 -->
if (this.data.ishot) {// 待完善 因为效果相同就没写了} else {if (this.data.mcurIndex === 0) {
part = this.data.cur_view.length * 2;
all = this.data.placeData.allGuide[this.data.mcurIndex].content[this.data.hlcurIndex].content.length;
} else {
part = this.data.cur_view.length;
all = this.data.placeData.allGuide[this.data.mcurIndex].content.length;
}
if (part < all) {
wx.showLoading({title: '正在加载'})
setTimeout(() => {this.bitiyan(this.data.placeData)
wx.hideLoading()}, 1000)
} else {
<!-- 当所有数据都加载完了,就显示 end 图标 -->
this.setData({showend: true})
}
}
}
关于 scroll-view 组件有几点需要注意的
- 设置竖向滚动的时后一定要设高度,有时候会发现设置了高度 100% 后,当滑到底部的时候,会显示不完整,这时候要看下你是否设置了 margin/padding,或者父元素设置了 margin/padding, 这时的 scroll-view 组件的高度就要减去相应的 margin/padding
- 当设置为横向滚动时需要注意,scroll-view 中需要滑动的元素不可以用 float 浮动;scroll-view 中的包裹需要滑动的元素的大盒子用 display:flex 是没有作用的;scroll-view 中的需要滑动的元素要用 dislay:inline-block 进行元素的横向编排;包裹 scroll-view 的大盒子有明确的宽和加上样式 –> overflow:hidden;white-space:nowrap;
收藏功能
收藏功能我是写在一个组件里,本来是想和顶部组件一样,供多个页面使用,后来因为写的页面中就只有一个有用到这个组件,这里就不单独说明这个组件了,而且这个组件和顶部组件基本差不多。
收藏功能的实现,当点击某一个景点时会触发点击事件,相信你看了列表切换功能,已经知道了 bind-tap 的使用方法,这里就不重复了。这里就是获取元素上的自定义属性,通过路由传参的方法传给详情页,详情页根据传递过来的数据,去数据源里获取相应的数据, 再将数据传递给组件,当点击详情页上的收藏按钮时,会触发绑定的事件,然后会更新缓存中的 collectData 收藏夹数据。‘我的’页面会显示收藏夹中的数据
详情页 js
<!-- 生命周期函数,监听页面加载 -->
onLoad: function(options) {
<!--options 中包含了传递过来的参数 -->
let name = options.name;
this.getinfo(name)
},<!-- 通过名字获取想要的数据 -->
getinfo(name){
<!-- 先获取缓存中已经存在的收藏夹数据,如果不存在就将 collectData 设为空数组 -->
let collectData = wx.getStorageSync('collectData') || [];
if (collectData.filter(e => e.name === name).length > 0) {
this.setData({placeData: collectData.filter(e => e.name === name)[0]
})
} else {let placeData = wx.getStorageSync('placeData')
let view = placeData.allGuide[0].content.map(e => e.content)
let newView = []
for (let i = 0; i < view.length; i++) {newView.push(...view[i])
}
this.setData({placeData: newView.find(e => e.name === name)
})
}
this.setBottom();},
<!-- 设置要传递给 bottom 组件的数据 -->
setBottom(){
this.data.bottomData.placeData = this.data.placeData;
let bottomData = this.data.bottomData;
this.setData({bottomData})
}
bottom 组件的 js
// components/bottom/bottom.js
const app = getApp()
Component({
/**
* 组件的属性列表
*/
properties: {
bottomData: { // 由父页面传递的数据,变量名字自命名
type: Object,
value: {},
observer: function (newVal, oldVal) {}}
},
/**
* 组件的初始数据
*/
data: {height: ''},
attached: function () {
// 获取是否是通过分享进入的小程序
this.setData({share: app.globalData.share})
// 定义导航栏的高度 方便对齐
this.setData({height: app.globalData.height})
},
/**
* 组件的方法列表
*/
methods: {
<!-- 点击收藏按钮触发的事件 -->
collected(){<!-- 将 isCollect(是否收藏过),collectors(收藏人数)从数据中解构出来 -->
let {isCollect,collectors} = this.data.bottomData.placeData;
isCollect = !isCollect;
this.data.bottomData.placeData.isCollect = isCollect;
let collectData = wx.getStorageSync('collectData') || [];
if(isCollect){
wx.showToast({
title: '收藏成功',
icon: 'success',
duration: 2000
})
collectors++;
collectData.push(this.data.bottomData.placeData);
}else{
wx.showToast({
title: '已取消收藏',
icon: 'success',
duration: 2000
})
collectors--;
collectData = collectData.filter(e => e.name != this.data.bottomData.placeData.name)
}
this.data.bottomData.placeData.collectors = collectors;
<!-- 将收藏夹数据放入缓存 -->
wx.setStorageSync('collectData', collectData)
let bottomData = this.data.bottomData;
this.setData({bottomData})
}
}
})
搜索功能
效果一
效果二
搜索功能的实现是通过原生组件 input 上的 bindinput 属性,当键盘输入时触发 bindinput 属性绑定的方法,实时获取中输入的值,然后将获取到的值放入请求地址中请求数据,再将请求获得的数据放入页面的 data 数据源中,当请求到的数据不为空时,页面上会显示得到的所有相关数据,如效果一。当按下搜索按钮时会触发 input 框上 bindconfirm 属性绑定的事件,此时页面上会显示请求到的数据中的第一条,如效果二。
wxml
<input style='width:500rpx' bindconfirm='confirm' confirm-type='search' focus='true' placeholder="搜索目的地 / 景点 / 攻略" bindinput='search'></input>
js
// pages/search/search.js
const app = getApp()
Page({
/**
* 页面的初始数据
*/
data: {
navbarData: {
showCapsule: 1, // 是否显示左上角图标 1 表示显示 0 表示不显示
title: '马蜂窝旅游', // 导航栏 中间的标题
backgroundColor: '#ffffff', //'#354a98'
city: '',
opacity: 1,
showMain: 0
},
height: app.globalData.height * 2 + 20,
result: [],
searchparams: '',
show: true,
searchHistory: [],
showResult: false,
showconfirm: false,
placedata: []},
<!-- 清空历史纪录 -->
clear() {
this.setData({searchHistory: []
})
wx.removeStorageSync('searchHistory')
},
<!-- 当点击键盘上搜索按钮触发的事件 -->
confirm(e) {if (e.detail.value != '') {let searchHistory = wx.getStorageSync('searchHistory') || []
if (searchHistory.filter(a => a === e.detail.value).length === 0) {searchHistory.push(e.detail.value)
wx.setStorageSync('searchHistory', searchHistory)
}
if (this.data.result.length > 0) {let currentCity = this.data.result[0].name;
this.getCityDataByName(currentCity);
}
this.setData({
show: false,
showResult: false,
showconfirm: true
})
}
},
<!-- 跳到主页面 -->
gotomain(e) {wx.setStorageSync('currentCity', e.currentTarget.dataset.name)
wx.switchTab({url: '/pages/main/index',})
},
<!-- 点击历史纪录触发的事件,效果和 confirm 方法基本相同,不同的是 confirm 是从页面 data 中获取数据,而 dosearch 是从接口中获取数据 -->
gosearch(e) {
let that = this
wx.request({url: `https://www.easy-mock.com/mock/5ca457f04767c3737055c868/example/mafengwo/search?name=${e.currentTarget.dataset.name}`,
success: (res) => {if (res.data.data.length > 0) {that.getCityDataByName(res.data.data[0].name)
} else {
this.setData({
show: false,
showResult: false,
showconfirm: true
})
}
}
})
},
// 通过城市名字 获取城市数据
getCityDataByName(cityname) {
let that = this
wx.request({
url: 'https://www.easy-mock.com/mock/5ca457f04767c3737055c868/example/mafengwo/china',
success: (res) => {let placedata = [];
placedata.push(...res.data.data.china.filter(e => e.chName === cityname))
that.setData({
placedata,
show: false,
showResult: false,
showconfirm: true
})
}
})
},
<!-- 当键盘输入时触发的事件 -->
search(e) {
let that = this
wx.request({url: `https://www.easy-mock.com/mock/5ca457f04767c3737055c868/example/mafengwo/search?name=${e.detail.value}`,
success: (res) => {if (res.data.data.length > 0) {that.changecolor(res.data.data, e.detail.value)
} else {
that.setData({result: [],
searchparams: '',
showResult: false
})
}
}
})
},
<!-- 改变名字颜色 -->
changecolor(result, searchparams) {for (let j = 0; j < result.length; j++) {let i = result[j].name.search(searchparams);
let left = result[j].name.slice(0, i),
mid = result[j].name.slice(i, i + searchparams.length),
right = result[j].name.slice(i + searchparams.length);
result[j].left = left;
result[j].mid = mid;
result[j].right = right;
}
this.setData({
result,
searchparams,
show: false,
showResult: true,
showconfirm: false
})
},
_navback() {
wx.navigateBack({delta: 1})
},
/**
* 生命周期函数 -- 监听页面加载
*/
onLoad: function() {
<!-- 获取缓存中的搜索历史并放入数据源 -->
let searchHistory = wx.getStorageSync('searchHistory') || []
this.setData({searchHistory})
}
这个 API 接口是我用 Easy Mock 写的
Easy Mock 地址链接
Easy Mock 代码
{
"data": function({_req}) {
let i = 0,
<!-- 数据源_data 由于篇幅原因就放了一小段数据 -->
_data = [
{
name: '亚洲',
type: '目的地'
},
{
name: '欧洲',
type: '目的地'
},
{
name: '大洋洲',
type: '目的地'
},
{
name: '非洲',
type: '目的地'
},
{
name: '北美洲',
type: '目的地'
},
{
name: '南美洲',
type: '目的地'
},
{
name: '南极洲',
type: '目的地'
}
],
<!--_req 是 easymock 封装的对象,_req.query(将查询参数字符串进行解析并以对象的形式返回,如果没有查询参数字字符串则返回一个空对象);-->
name = _req.query.name;
if (name != '') {
<!-- 当输入的值不为空时 -->
let result = [];
let data = []
for (let j = 0; j < result.length; j++) {<!--eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码。这里主要是为了给正则表达式动态传参 -->
if (eval('/' + name + '/').test(result[j].name)) {data.push(result[j])
}
<!-- 当查询到 8 个匹配项时跳出循环 -->
if (data.length > 8) break;
}
return data
} else {
<!-- 当输入的值为空时直接返回空数组 -->
return []}
}
}
热门城市动画
因为动画只有 6 个元素,所以就没有必要写成数组遍历创建了,直接写 6 个盒子,给他们的样式初始化,让他们到自己的初始位置去。微信小程序提供了创建动画实例的 API wx.createAnimation
wxml
<view class='video a' animation="{{animation1}}" data-index='0' bindtap="_play">
<view class='context'>
<text>{{placeData.vlog[0].title}}</text>
</view>
<view class='vdoIcon'>
<image src='/images/play.png'></image>
</view>
</view>
<view class='video b' animation="{{animation2}}" data-index='1' bindtap="_play">
<view class='context'>
<text>{{placeData.vlog[1].title}}</text>
</view>
<view class='vdoIcon'>
<image src='/images/play.png'></image>
</view>
</view>
<view class='video c' animation="{{animation3}}" data-index='2' bindtap="_play">
<view class='context'>
<text>{{placeData.vlog[2].title}}</text>
</view>
<view class='vdoIcon'>
<image src='/images/play.png'></image>
</view>
</view>
<view class='video d' animation="{{animation4}}" data-index='3' bindtap="_play">
<view class='context'>
<text>{{placeData.vlog[3].title}}</text>
</view>
<view class='vdoIcon'>
<image src='/images/play.png'></image>
</view>
</view>
<view class='video e' animation="{{animation5}}" data-index='4' bindtap="_play">
<view class='context'>
<text>{{placeData.vlog[4].title}}</text>
</view>
<view class='vdoIcon'>
<image src='/images/play.png'></image>
</view>
</view>
<view class='video f' animation="{{animation6}}" data-index='5' bindtap="_play">
<view class='context'>
<text>{{placeData.vlog[5].title}}</text>
</view>
<view class='vdoIcon'>
<image src='/images/play.png'></image>
</view>
</view>
wxss
.a{opacity: 0.9;}
.b{transform: translate(170rpx,-110rpx) scale(0.8);
opacity: 0.8;
}
.c{transform: translate(210rpx,-250rpx) scale(0.7);
opacity: 0.7;
}
.d{transform: translate(10rpx,-350rpx) scale(0.6);
opacity: 0.6;
}
.e{transform: translate(-250rpx,-290rpx) scale(0.8);
opacity: 0.5;
}
.f{transform: translate(-300rpx,-130rpx) scale(0.9);
opacity: 0.8;
}
js
// 动画的运行路线
translate: function(i) {
// 获取屏幕宽度来实现自适应
let windowwidth = this.data.windowWidth;
// 动画的运行状态 status[x 轴偏移量,y 轴偏移量,scale 缩放倍数,opacity 透明度], 也是动画的运行路线
let status = [[170, -110, 0.8, 0.7],
[210, -250, 0.7, 0.6],
[10, -350, 0.6, 0.5],
[-250, -300, 0.8, 0.7],
[-300, -130, 0.9, 0.8],
[0, 0, 1, 0.9]
];
let x = 0,
y = 0,
scale = 0,
opacity = 0;
for (let j = 0; j < 6; j++) {let animationName = 'animation' + (j + 1);
x = status[(i + j) % 6][0] / 750 * windowwidth;
y = status[(i + j) % 6][1] / 750 * windowwidth;
scale = status[(i + j) % 6][2];
opacity = status[(i + j) % 6][3];
this.animation.translate(x, y).scale(scale).opacity(opacity).step()
this.setData({[animationName]: this.animation.export()// 导出动画数据传递给组件的 animation 属性})
}
},
hotCityAnimation() {
let i = 0;
<!-- 创建动画实例 -->
this.animation = wx.createAnimation({
duration: 2000,
timingFunction: 'ease',
})
let that = this
let anicontrol = this.data.anicontrol
anicontrol = setInterval(function() {that.translate(i)
if (i == 5) {i = -1;}
i++;
}, 3000)
this.setData({anicontrol})
}
这里要注意的是,因为这是写在 tabbar 页面的动画,而且用了 setinterval 定时器,会按照指定的周期(以毫秒计)来执行注册的回调函数,意思就是即使你跳转到别的页面,动画依然在运行,当你回到主页时,动画就会运行出错,出现鬼畜,所以要在主页的 onHide 周期函数,监听页面隐藏时就把定时器给清除了,并且把动画实例也清除。
onHide: function() {
let anicontrol = this.data.anicontrol;
clearInterval(anicontrol)
this.setData({
animation1: '',
animation2: '',
animation3: '',
animation4: '',
animation5: '',
animation6: ''
})
}
关于 css
写这个小程序我没有用到任何 UI 框架,这有坏处,也有好处,坏处就是代码进度贼慢,好处就是自己增加了很多对 css 的理解。有想用 UI 框架的可以使用 WeUI。链接里有详细的使用方法。
结语
因为时间和精力的缘故,小程序只写了几个页面和小部分功能,在写项目的过程中也发现了自己的很多不足,因此吃到了不少苦头,但是也学到了不少,可以说痛并快乐着。希望这篇文章能够对打算写小程序的你有一点帮助。GitHub 源码在这里,需要自取。