实战篇 3:天气页面样式布局开发¶
先写一个 icon 组件¶
「新鲜天气」项目中,用到的 icon 比较多,比如天气图标、心情签到的表情,都是来自 icon 组件,本小节介绍下如何自定义个 icon 组件。
我们项目的自定义组件放在 client/components 目录下,首先在其目录下创建 icon 目录,创建组件的页面、样式和 JS 文件:
client/components
└── icon
├── index.js
├── index.json
├── index.scss
├── index.wxml
└── weather.ttf
组件需要在自己的页面配置文件(page.json,即 index.json)中声明自己是一个组件:
// index.json
{
"component": true
}
编写组件代码¶
icon 组件的 WXML 部分代码很简单:
<!--index.wxml-->
<text class="icon icon-{{ type }}"></text>
笔者定义了一个 icon 的类型字段,该字段由使用方传入,所以对应 JS 的写法为:
// index.js
Component({
properties: {
type: {
type: String,
value: ''
}
}
});
JS 中使用了 Component 构造器,调用 Component 构造器时可以指定组件的属性、数据、方法等。上面代码中定义了组件可以接受的 properties 有 type,type 是一个字符串类型的值,默认值是空字符串。
跟所有的 icon 样式写法一样,笔者通过图标共有的 class icon 定义了统一的样式,包括字体、大小等:
/* index.scss */
@font-face {
font-family: "weather";
src: url("./weather.ttf") format("truetype");
}
.icon {
font-style: normal;
-webkit-font-smoothing: antialiased;
-webkit-text-stroke-width: .4rpx;
-moz-osx-font-smoothing: grayscale;
}
.icon::after,
.icon::before {
font-family: weather !important;
}
然后通过 .icon-{{type}} 定义了不同的图标对应 class 的 content:
/* index.scss */
.icon.icon-xiaolian:before {
content: "\e60f";
}
.icon.icon-shidu:before {
content: "\e610";
}
.icon.icon-zhongyu:before {
content: "\e611";
}
字体文件的引用问题¶
在小程序内,不支持使用 webfont 的 @font-face 引入本地的 ttf 等文件,这时候需要使用线上地址或者 base64。
在新鲜天气的开发中,笔者使用了 Gulp 直接将 webfont 转换成 base64 引入,这样写代码的时候就不需要关注这些事情了,具体做法为:
const rename = require('gulp-rename')
const postcss = require('gulp-postcss')
const pxtorpx = require('postcss-px2rpx')
const base64 = require('postcss-font-base64')
const combiner = require('stream-combiner2')
gulp.task('wxss', () => {
const combined = combiner.obj([
gulp.src(`${src}/**/*.{wxss,scss}`),
sass().on('error', sass.logError),
postcss([pxtorpx(), base64()]),
rename((path) => (path.extname = '.wxss')),
gulp.dest(dist)
])
combined.on('error', handleError)
})
除了字体以外,图片类静态资源的引入可以使用本地资源相对路径、
base64和线上地址,如果是线上地址,则必须是以https开头的协议。
在项目中使用自定义组件¶
在需要使用自定义组件的页面配置文件 page.json 中添加 usingComponents 字段:
"usingComponents": {
"icon": "../../components/icon/index"
}
其中,icon 就是我们自定义的标签名称,后面的值则是相对于页面文件、icon 组件对应的路径。
这样引入后,在页面中就可以直接通过 icon 标签来使用自定义的 icon 组件了:
<!--定位icon-->
<icon type="dingwei" />
<!--天气icon-->
<icon type="{{ today.icon }}" class="logo"></icon>
也可以使用 CSS(WXSS)来控制它的样式:
icon {
float: right;
font-size: 44rpx;
height: 44rpx;
width: 44rpx;
}
如何编辑字体:icon 组件使用的字体是来自 iconfont.cn,然后在百度的字体编辑器中进行编辑。
天气预报页面¶
天气预报是小程序的第一个页面,首先在 app.json 中的 pages 处添加入口:
"pages": ["pages/weather/index"]
如果有多个页面,当前开发的页面可以放到
pages数组的最前面,这样小程序的默认页面就是当前开发的页面,方便实时开发和查看效果。
添加完入口之后,在 client/pages 目录下创建 weather 文件夹,目录结构如下:
pages
└── weather
├── index.js
├── index.json
├── index.scss
├── index.wxml
└── index.wxs
因为我们要使用上文完成的 icon 组件,所以在 index.json 中添加 usingComponents 字段,天气页面还支持下拉刷新,于是最终版本的 index.json 配置如下:
//index.json
{
"enablePullDownRefresh": true,
"usingComponents": {
"icon": "../../components/icon/index"
}
}
页面整体框架¶
首先我们来完成页面的整体框架 WXML 代码,页面整体包裹在 .wrapper 中,页面使用 .container 来区分不同的页面模块,模块之间通过 .container 的 margin-bottom 属性隔开。
<!--weather/index.wxml-->
<view class="wrapper" style="background: url({{backgroundImage}}) center -178rpx / 100% no-repeat {{backgroundColor}};">
<view class="container" id="canvas-wrapper">
<view class="now">
<!--当前实时天气和空气质量-->
</view>
<view class="two-days">
<!--今明两天天气-->
</view>
</view>
<view class="weather" style="background-color: {{backgroundColor}}">
<view class="container">
<!--24 小时天气-->
</view>
<view class="container">
<view class="week">
<!--七天天气-->
</view>
</view>
<view class="container">
<view class="life-style">
<!--生活指数-->
</view>
</view>
</view>
</view>
// weather/index.scss
// 定义 container 间隔
$grid-margin: 20rpx;
.container {
margin-bottom: $grid-margin;
max-width: 750rpx;
box-sizing: border-box;
color: #fff;
}
.wrapper 的背景图片和 .weather 的背景色都是根据天气情况更换的,需要根据天气数据赋值,这里笔者设置了默认值:
Page({
data: {
// 页面数据
backgroundImage: '../../images/cloud.jpg',
backgroundColor: '#62aadc'
...
实时天气部分页面布局¶
天气页面的「实时天气」部分页面布局相对复杂,最终效果如图所示。
首先是当前天气部分页面结构:
<!--weather/index.wxml-->
<view class="now">
<view class="location" bindtap="chooseLocation">
<icon type="dingwei" />
<text>{{ address }}</text>
</view>
<view class="air-quality" wx:if="{{air.aqi}}">
<text class="circle" style="background: {{ air.color }}"></text>
<text class="value">{{ air.name }} {{ air.aqi }}</text>
</view>
<view class="now-weather">
<view class="temp">
<text>{{ current.temp }}</text>
<text class="degree">°</text>
</view>
<view class="cur-weather">
<view class="inline">
<icon type="{{ current.icon }}"></icon>
<text>{{ current.weather }}</text>
</view>
<view class="inline today">
<text class="item">{{ utils.humidity(current.humidity) }}</text>
<text class="item">{{ utils.wind(current.wind, current.windLevel) }}</text>
</view>
</view>
<view class="tips" wx:if="{{tips}}">
<text>{{tips}}</text>
</view>
</view>
</view>
上面页面需要的 AppData 示例数据为:
"air": {
"status": 0,
"aqi": "77",
"color": "#00cf9a",
"name": "良"
},
"current": {
"backgroundImage": "https://tianqi-1d3bf9.tcb.qcloud.la/bg/day/overcast.jpg",
"backgroundColor": "#5c7a93",
"temp": "35",
"wind": "南风",
"windLevel": "1",
"weather": "阴",
"humidity": "73",
"icon": "yin",
"ts": "2018-08-12 14:54"
},
上面的 WXML 中,笔者还使用了 utils 的两个方法。utils的方法来自于index.wxs,要使用index.wxs需要在页面的顶部引入它:
<wxs src="./index.wxs" module="utils"></wxs>
WXS 相对 JS 来说语法更加受限,但是因为 WXML 的「双括号」数据绑定中对表达式的支持不够完善,我们在小程序开发中,可以使用 WXS 来增强 WXML 的表达式,其中 humidity 和 wind 的 WXS 代码如下:
// weather/index.wxs
module.exports = {
// 湿度处理
humidity: function(h) {
if (h) {
return '湿度 ' + h + '%'
}
return h
},
// 根据风的 code 和风力输出文案
wind: function(code, level) {
if (!code) {
return '无风'
}
if (level) {
level = level.toString().split('-')
level = level[level.length - 1]
return code + ' ' + level + '级'
}
return code
}
}
实时天气模块中,今明两天的 WXML 结构如下:
<!--weather/index.wxml-->
<!--今明两天天气数据-->
<view class="two-days">
<view class="item">
<view class="top">
<text class="date">今天</text>
<text class="temp">{{ today.temp }}</text>
</view>
<view class="bottom">
<text>{{ today.weather }}</text>
<icon type="{{ today.icon }}" class="logo"></icon>
</view>
</view>
<view class="item">
<view class="top">
<text class="date">明天</text>
<text class="temp">{{ tomorrow.temp }}</text>
</view>
<view class="bottom">
<text>{{ tomorrow.weather }}</text>
<icon type="{{ tomorrow.icon }}" class="logo"></icon>
</view>
</view>
</view>
由此可见,需要的 AppData 示例数据结构是:
"today": {
"temp": "24/30°",
"icon": "leizhenyu",
"weather": "雷阵雨"
},
"tomorrow": {
"temp": "24/30°",
"icon": "leizhenyu",
"weather": "雷阵雨"
},
需要说明的是,在今明两天天气布局中,笔者使用了 flex 布局,flex 布局使得小程序的页面布局更灵活,结构更明晰:
// weather/index.scss
@mixin flex-row {
display: flex;
flex-direction: row;
}
.today {
@include flex-row;
.item {
display: block;
flex: 1;
padding-right: 16rpx;
margin: 0 16rpx 0 0;
border-right: 2rpx solid rgba(255, 255, 255, .4);
}
}
屏幕适配:自定义导航样式¶
为了UI效果,笔者使用了自定义导航条样式,即在app.json中增加配置:
"window": {
"navigationStyle": "custom"
},
经过上面配置,就没有导航条了,整个界面直接是天气预报页面的背景图,现在遇见了小程序的屏幕适配问题,笔者界面设计是定位地址文案部分跟小程序的胶囊操作区域对齐,如下图所示:
但是如果只是简单的使用 rpx,在 iPhone 6 的视觉稿(具体原因见基础篇 1:小程序开发基础知识)标准下实现:
<!--pages/weather/index.wxml-->
<view class="container" id="canvas-wrapper" style="padding-top: 64rpx">
上面直接使用 rpx 来布局,由于不同手机的屏幕尺寸不同,实际产生的效果是:
要做好屏幕适配,需要用到 rpx 的基础知识和wx.getSystemInfo()方法。
我们通过学习基础知识了解,rpx 是按照屏幕宽度来定义的,不管屏幕多宽,屏幕宽度始终定义为 750rpx,宽度不同,则 1rpx 实际宽度不同,如果用这个不确定的 rpx 来对高度做统一是实现不了的,所以我们固定使用padding-top: 64rpx实际根据不同屏幕它是高度不一致的。手机都有状态栏(statusBar),状态栏高度也是不同手机不同的尺寸,所以,最终在不同的手机会出现上面图片的效果。
要解决这个屏幕适配问题,即确定一个固定的 padding-top 值,需要将状态栏高度和 rpx 的实际对应 px 值进行统一计算。
首先,在页面结构中,使用 {{paddingTop}} 来表示 padding-top 值。
<!--pages/weather/index.wxml-->
<view class="container" id="canvas-wrapper" style="padding-top: {{paddingTop}}px">
这个值是计算之后的 px 值,所以单位是 px!这个值在 iPhone 6 手机中是32px(iPhone 6 屏幕宽度为375px,所以750rpx = 375px)。下面我们需要获取系统的状态栏高度(statusBarHeight),可以使用wx.getSystemInfo()或者它的同步方法wx.getSystemInfoSync() 获取:
//pages/weather/index.js
wx.getSystemInfo({
success: (res) => {
// 状态栏高度和屏幕宽度,单位都是px
console.log(res.statusBarHeight, res.windowWidth)
}
})
经过获取状态栏高度发现,iPhone 6 手机的状态栏高度为20px,所以计算出差值为12px(32px - 20px)。下面我们只需要将状态栏高度获取之后,加上12px即可。所以最终paddingTop计算代码是:
//pages/weather/index.js
wx.getSystemInfo({
success: (res) => {
// 状态栏高度和屏幕宽度
// console.log(res.statusBarHeight, res.windowWidth)
// console.log(scale * res.statusBarHeight*2+24)
this.setData({
paddingTop: res.statusBarHeight+12
})
}
})
Tips:rpx 并不是「万能油」,根据实际情况也可以使用 px 来解决实际问题。
WXML 的循环:24小时、一周天气和生活指数¶
天气数据中,24 小时和一周天气都是由数组组成:
// 24小时天气数据
"hourlyData": [
{
"temp": "29",
"time": "16:00",
"weather": "雷阵雨",
"icon": "leizhenyu"
}
// ...
],
// 一周天气数据
"weeklyData": [
{
"day": "雷阵雨",
"dayIcon": "leizhenyu",
"dayWind": "南风",
"dayWindLevel": "1-2",
"maxTemp": "30",
"minTemp": "24",
"night": "中雨",
"nightIcon": "zhenyuye",
"nightWind": "南风",
"nightWindLevel": "1-2",
"time": 1534032000000
}
// ...
],
// 生活指数
"lifeStyle": [
{
"name": "舒适度", // 指数名称
"icon": "guominzhishu", // 指数对应的icon图标type
"info": "较不舒适", // 指数数值
// 指数的详情
"detail": "白天虽然有雨,但仍无法削弱较高气温带来的暑意,同时降雨造成湿度加大会您感到有些闷热,不很舒适。"
}
// ...
]
对于这些数组结构,我们在写页面的时候可以使用 WXML 的循环语句 wx:for 来输出 WXML:
<!--weather/index.wxml-->
<!--24小时天气-->
<scroll-view scroll-x class="hourly">
<view class="scrollX">
<view class="item" wx:for="{{hourlyData}}">
<text class="time">{{ item.time }}</text>
<icon type="{{item.icon}}" class="icon"></icon>
<text class="temp">{{item.temp}}°</text>
</view>
</view>
</scroll-view>
<!--一周天气数据-->
<view class="week">
<view class="week-weather">
<view class="item" wx:for="{{weeklyData}}">
<view class="day">{{ utils.formatWeeklyDate(index) }}</view>
<view class="date">{{ utils.formatDate(item.time) }}</view>
<view class="daytime">
<view class="wt">{{item.day}}</view>
<icon type="{{item.dayIcon}}" class="img"></icon>
</view>
<view class="night">
<icon type="{{item.nightIcon}}" class="img"></icon>
<view class="wt">{{item.night}}</view>
</view>
<view class="wind">{{ utils.wind(item.nightWind) }}</view>
<view class="wind" wx:if="{{item.nightWind}}">{{ utils.windLevel(item.nightWindLevel) }}</view>
<view class="wind" wx:else></view>
</view>
</view>
<!--生活指数-->
<view class="life-style">
<view class="item" wx:for="{{lifeStyle}}" data-name="{{item.name}}" data-detail="{{item.detail}}" bindtap="indexDetail">
<view class="title">
<icon type="{{item.icon}}"></icon>
{{item.name}}
</view>
<view class="content">{{item.info}}</view>
</view>
</view>
这里需要特别说下「24小时天气」和「生活指数」。对于「24小时天气」,笔者使用了 scroll-view 组件 + flex 布局,根据数组数据的长度(和风天气免费 API 只能获取间隔 3 个小时共 8 个小时天气)来计算 scroll-view 的整体宽度,然后按照等比例划分:
// weather/index.scss
@mixin flex-column {
display: flex;
flex-direction: column;
}
// hourly
.hourly {
.scrollX {
position: relative;
// 总长度,116*8
width: 928rpx;
padding: 40rpx 0;
height: 150rpx;
}
.item {
@include flex-column;
width: 116rpx;
}
}
「生活指数」布局是上下两行:
flex 布局中使用横向(flex-row)布局,要达到 4x2 的布局效果,需要将子项设置为25%宽度,并且设置父容器 flex-wrap: wrap:
// weather/index.scss
.life-style {
@include flex-row;
flex-wrap: wrap;
.item {
text-align: center;
width: 25%;
height: 188rpx;
border-right: 2rpx solid rgba(255, 255, 255, .1);
border-bottom: 2rpx solid rgba(255, 255, 255, .1);
box-sizing: border-box;
padding: 50rpx 0 0;
}
}
小结¶
本节主要从整体上介绍了「新鲜天气」天气预报页面的布局实现,用到了 WXS 来增强 WXML 的数据绑定表达式,使用了多种 flex 布局效果,对于数组型数据,使用了 WXML 中的循环语句 wx:for 来实现。