本文用来介绍关于如何在微信小程序中实现materia风格的ui化
注意:该ui使用微信小程序原生语法,动画均使用animate以及过渡效果实现,未使用微信的api创建动画
1.准备
创建一个自定义组件 sc-button
文件目录
在sc-button.json中指明这是一个自定义组件
{
"component":true
}
2. 封装button
2.1 初始html格式
<button class="btn-class">
<slot></slot>
</button>
2.2 处理微信原生事件以及指令
微信小程序的button有很多内置的微信指令例如 open-type,size,plain
等以及原生的方法如getuserinfo,getphonenumber
等 所以我们封装button的时候要把这些能力进行相应的处理。
可以分为两类:一种是指令,一种是事件,
指令 可以从properties里将微信原生的button的所有指令声明,然后直接赋值到内部封装的button里。
事件 我们可以根据事件的捕获冒泡以及open-type的唯一性,让其在触发后根据open-type选择事件直接冒泡到外层即可,但是需要将获取的value也传递出去
例如:
properties: {
openType: {
type: String
},
size: {
type: String,
value: 'default'
},
plain: {
type: Boolean,
value: false
}
},
data: {
// 事件的map表
openTypeToBindEvent: {
'getUserInfo': 'getuserinfo',
'getphonenumber': 'getphonenumber',
'launchApp': 'error',
'contact': 'contact'
}
},
methods: {
// 绑定未冒泡的事件手动触发到上一层
_returnEventData(e) {
this.triggerEvent(`${this.data.openTypeToBindEvent[this.properties.openType]}`);
}
}
然后直接赋值到button里,注意,这里需要判断一下值是否存在
<button class="btn-class"
bind:getuserinfo="{{openType === 'getUserInfo' ? '_returnEventData' : '' }}"
bind:getphonenumber="{{openType === 'getphonenumber' ? '_returnEventData' : '' }}"
bind:error="{{openType === 'launchApp' ? '_returnEventData' : '' }}"
bind:contact="{{openType === 'contact' ? '_returnEventData' : '' }}"
open-type="{{openType || ''}}"
size="{{size || ''}}"
plain="{{plain || ''}}"
>
<slot></slot>
</button>
2.3 material 的 涟漪实现
2.3.1 重置/增加button的一些样式
button {
display: flex;
justify-content: center;
align-items: center;
position: relative;
box-sizing: border-box;
overflow: hidden;
line-height: 66px;
min-width: 88px;
height: 36px;
padding: 0 16px;
margin: 0;
font-size: 32rpx;
border-radius: 2px;
transition: all .2s cubic-bezier(.4,0,.2,1); // 增加过渡效果
}
2.3.2 增加涟漪
注意:微信小程序不支持js操纵dom元素 即没有appenChild一类的方法来添加元素,所以我们只能声明一个元素来进行涟漪的展示
<button class="btn-class"
capture-bind:tap="{{ripple ? '_addRipple' : ''}}"
capture-bind:longpress="{{ripple ? '_longPress' : ''}}"
capture-bind:touchend="{{ripple ? '_touchend' : ''}}"
bind:getuserinfo="{{openType === 'getUserInfo' ? '_returnEventData' : '' }}"
bind:getphonenumber="{{openType === 'getphonenumber' ? '_returnEventData' : '' }}"
bind:error="{{openType === 'launchApp' ? '_returnEventData' : '' }}"
bind:contact="{{openType === 'contact' ? '_returnEventData' : '' }}"
open-type="{{openType || ''}}"
size="{{size || ''}}"
plain="{{plain || ''}}"
>
<slot></slot>
<!-- 涟漪view -->
<view class="ripple">
</view>
</button>
涟漪的动画css样式
/* 涟漪的初始样式 */
.ripple {
border-radius: 100%;
background-color: #000000;
left: 20px;
top: 20px;
opacity: 0.3;
transform: scale(0.3);
width: 10px;
height: 10px;
position: absolute;
}
/* 涟漪的点击扩散动画 */
.ripple-animation {
animation: ripple 0.6s ease-out;
animation-fill-mode: forwards;
}
/* 涟漪的长按扩散动画 */
.ripple-animation-hold{
animation: ripple-hold 1s ease-out;
animation-fill-mode: forwards;
}
@keyframes ripple {
from {
transform: scale(0.1);
opacity: 0.3;
}
to {
transform: scale(2.5);
opacity: 0;
}
}
@-webkit-keyframes ripple-hold {
from {
transform: scale(0.1);
opacity: 0.3;
}
to {
transform: scale(2.5);
opacity: 0.3;
}
}
涟漪的播放控制
两种播放控制
点击 - ripple-animation 动画
长按 - ripple-animation-hold 动画
然后我们在点击的时候播放 ripple-animation 长按播放ripple-animation-hold 即可
那么如何判断这个view的位置以及大小呢,因为每个人点击button的位置不一样,button的大小不一样,如果view过小就可能覆盖不到整个button,过大就太耗费性能。
所以,大小我们定为button长边的两倍
然后点击button的哪个位置,ripple就在哪个位置播放,因此必须设置ripple的position为absolute,我们就可以通过控制其left,以及top来控制ripple的位置。
问题就是ripple的位置,大小该如何设置
下面,我们在html里声明view的位置大小属性。
<view style="width:{{width}}px;height:{{width}}px;left:{{left}}px;top:{{top}}px"
class="ripple-class {{click?'ripple-animation':hold?'ripple-animation-hold':''}}">
</view>
添加点击事件
methods: {
// 短按(长按同理)
_tap(e) {
// 获取button的大小,位置
this._queryMultipleNodes('btn-class').then(res => {
// 关于button的属性 // 关于button位置的属性
const button = res[0], viewPort = res[1];
const boxWidth = parseInt(button.width); // button的宽度
const boxHeight = parseInt(button.height); // button的长度
const rippleWidth = boxWidth > boxHeight ? boxWidth : boxHeight;
// 我们需要计算的是ripple相对于button左上角的距离
// 注意 e.detail.y(点击位置)是相当于文档的高度不是当前窗口的高度,因此需要减去滚动的距离以及button的top
const rippleX = (e.detail.x - (button.left + viewPort.scrollLeft)) - rippleWidth / 2;
const rippleY = (e.detail.y - (button.top + viewPort.scrollTop)) - rippleWidth / 2;
this.setData({
click:true,
width: rippleWidth,
left: rippleX ,
top: rippleY
});
});
},
// 该方法返回选择元素的大小,位置
_queryMultipleNodes: function (e) {
return new Promise((resolve, reject) => {
const query = this.createSelectorQuery();
query.select(e).boundingClientRect();
query.selectViewport().scrollOffset();
query.exec(function (res) {
resolve(res);
});
})
}
}
实现效果
效果图但是这样出现了一个bug,即点击多次,不会出现多个涟漪效果,而是会导致一个view的动画结束然后重复播放
解决办法:
采用wx-for来循环产出ripple,这样可以实现多个涟漪的效果,那么我们可以定义一个ripple数组,每次点击的时候不断往该数组push进新的ripple然后由浏览器渲染就好了,我们还需要分配 wx-key来避免渲染数组的性能问题。
我们需要为每个rippleItem分配 短按 播放动画的标识 startAnimate 以及 长按播放动画标识 holdAnimate
data: {
rippleList: [],
rippleId: 0
},
methods:{
_tap(e) {
if (!this.properties.disabled) {
this._queryMultipleNodes('.' + this.data.btnClass).then(res => {
const button = res[0], viewPort = res[1];
const boxWidth = parseInt(button.width); // button的宽度
const boxHeight = parseInt(button.height); // button的长度
const rippleWidth = boxWidth > boxHeight ? boxWidth : boxHeight;
const rippleX = (e.detail.x - (button.left + viewPort.scrollLeft)) - rippleWidth / 2;
const rippleY = (e.detail.y - (button.top + viewPort.scrollTop)) - rippleWidth / 2;
this.data.rippleList.push({
rippleId: `ripple-${this.data.rippleId++}`,
width: rippleWidth ,
left: rippleX ,
top: rippleY ,
startAnimate: true,
holdAnimate: holdAnimate || false
});
this.setData({
rippleList: this.data.rippleList
});
});
}
}
}
实现效果
效果图到这里我们又发现了一个问题 就是 ripple在产出的时候 并未删除,所以它会一直增加增加增加
就像这样
多个ripple未删除
所以我们可以将播放完毕的动画从rippleList删除,这样可以进行一定的优化,利用小程序的animationend事件可以触发每个ripple的动画播放完毕事件,然后取得id并从rippleList找到这个id删除即可。可以找到这个id的item实在是太耗费性能了。
于是我们想到,每个动画播放完毕一定是这个list的最前面的一个item,也就是每次触发动画播放完毕事件我们只需要删除list中的第一个就好了,但是小程序需要每次都执行setData方法来对数组进行更新,这会导致我们按一百个ripple就执行一百次setData,大量耗费性能,因此需要一个防抖来控制setData的执行,
<view wx:for="{{rippleList}}"
wx:key="rippleId"
id="{{item.rippleId}}"
style="width:{{item.width}}px;height:{{item.height}}px;left:{{item.left}}px;top:{{item.top}}px"
class="ripple-class {{item.startAnimate ? item.holdAnimate ? 'ripple-animation-slow-hold' :'ripple-animation-slow' : ''}}"
bind:animationend="{{item.holdAnimate ? null : '_scbuttonrippleAnimationend'}}">
</view>
_buttonrippleAnimationend() {
// 防抖
this.data.rippleList.shift();
if (this.data.timer) {
clearTimeout(this.data.timer);
this.data.timer = setTimeout(deleteRipple.bind(this), 300);
} else {
this.data.timer = setTimeout(deleteRipple.bind(this), 300);
}
function deleteRipple() {
this.setData({
rippleList: this.data.rippleList
});
clearTimeout(this.data.timer);
this.data.timer = null;
}
}
网友评论