- Skyline|小程序吸顶、网格、瀑布流布局都拿下~
在之前的文章中,我们知道了新 scroll-view 可以让小程序的长列表做到丝滑滚动~ 也提到了新 scroll-view 提供了很多新能力 sticky、网格布局、瀑布流布局等,这一篇,我们就来看看这些新能力是怎么使用的~ 新 scroll-view 在原来列表模式(type="list")的基础上,新增了自定义模式(type="custom") 在自定义模式下,新增了以下新组件供开发者调用 list-view:列表布局容器sticky-section / sticky-header:吸顶布局容器grid-view:网格布局容器,可实现网格布局、瀑布流布局等sticky布局sticky 布局即在应用中常见的吸顶布局,与 CSS 中的 position: sticky 实现的效果一致,当组件在屏幕范围内时,会按照正常的布局排列,当组件滚出屏幕范围时,始终会固定在屏幕顶部。 常见的使用场景有:通讯录、账单列表、菜单列表等等。 与 position: sticky 不同的是,position: sticky 很难实现列表滚动需要的交错吸顶效果,而 sticky 组件则可以帮忙开发者轻松实现交错吸顶的效果。 sticky 的使用非常简单: 将 scroll-view 切换到 custom 模式采用 sticky-section 作为 scroll-view 的子元素sticky-header 放置吸顶内容list-view 放置列表内容 {{item.name}} ... 我们来看下采用 sticky 布局做出来的通讯录效果~ [视频] sticky 布局也可以通过给 sticky-section 配置 push-pinned-header 来声明吸顶元素重叠时是否继续上推 像下图输入框和标签列表这种类型,标签列表吸顶时还是希望保留输入框吸顶。 [视频] 网格布局网格布局即将列表切割成格子,每一行的高度固定,常见的视频列表、照片列表等通常都采用网格布局。 在此之前,实现网格布局需要开发者自行实现网格切割,再嵌入到 scroll-view 中。 新 scroll-view 直接提供了 grid-view 组件供开发者使用~ 将 scroll-view 切换到 custom 模式采用 grid-view 类型为 aligned 做为直接子节点grid-view 中直接编写列表 ... 下面是使用网格布局实现的图片列表效果~ [视频] 瀑布流布局瀑布流布局与网格布局类似,不同的是瀑布流布局中每个格子的高度都可以是不一致的,所以在小程序中实现瀑布流布局就比较复杂了。 开发者需要通过计算格子高度,然后再进行瀑布流拼接,当滚动内容过多时还需要处理节点过多导致内存不足等问题。 grid-view 组件直接支持了瀑布流模式供开发者直接使用,grid-view 组件会根据子元素高度自动布局: 将 scroll-view 切换到 custom 模式采用 grid-view 类型为 masonry 做为直接子节点grid-view 中直接编写列表 ... 下面是使用瀑布流布局实现的图片列表效果~ [视频] 想要立即体验?现在通过微信开发者工具导入 代码片段,即可体验新版 scroll-view 组件能力~
2023-08-03 - 微信小程序中实现丝滑的加购动画
前言 大家在商城类小程序中经常会看到如下图的加购动画,丝滑的抛物线动画进入购物车的过程给用户带来了更好的购物体验~ 这样的动画其实需要考虑到很多情况,要不然就很容易出现问题。 [图片] 在接下来的文章中,我们将手把手教会大家实现这样的功能。 二、技术分析 2.1 现象剖析 首先简单的看下整个加购动画的过程,小球的出现是从用户点击处产生的,随即完成一个抛物线后,进入金额处(购物车),最后购物车产生放大效果。 我们通常实现动画的方式一般就是两种,第一种就是js 控制运行的轨迹,第二种就是css实现。js实现的话势必会出现性能问题,在某些边界情况下会出现卡顿,作为前端人员想必大家都知道~ 如果使用css实现的话,我们需要解决以下几个问题: 如何确认和设置小球的初始位置?如何实现一个抛物线的动画?如何知道抛物线动画结束了?并通知购物车发生抖动和放大效果?2.2 分步实现 1.加购小球 我们知道在小程序中是没有办法动态创建一个节点的,所以小球是需要在当前页面事先准备好,以一个组件身份存在~ 小球组件的初始位置由用户点击位置进行决定,所以小球的position必须是个变量,让我们想到了css变量~ 组件接收外部两个属性 showBall控制显隐position,通过style去定义css var变量从而设置小球初始位置如下是简单的小球组件代码: // animationBall.wxml // animationBall.wxss .ball-box { width: 20rpx; height: 20rpx; border-radius: 100%; position: fixed; z-index: 10; left: var(--startX); top: var(--startY); } // animationBall.js Component({ properties: { showBall: { type: Boolean, value: false }, position: { type: Object, value: { } } }, observers: { "position.startX, position.startY": function(startX, startY) { let style = `--startX:${startX}px;--startY:${startY}px;--endX: 15vw;--endY: 92vh`; this.setData({ style }); } } }); 2.页面点击事件 我们在页面中添加小球组件,并且给加购点击动作添加事件~ 这样当我们点击时候就可以初始化小球的位置以及让小球展示出来 // 购买页面点击事件 buy.js buy(event){ this.setData({ "position.startX": event.touches[0].clientX, "position.startY": event.touches[0].clientY, showBall: true }); } // buy.wxml 3.animation动画拆解 我们确定了小球的初始位置和终点位置(终点位置是固定的,这里不赘述了),接下来就是需要去剖析抛物线动画~ 一个抛物线整体去看,会比较没有头目,但是我们可以从某个单一维度去看整个小球运动的过程,然后再将这几个维度的动画组合起来,就可以完成整个动画的设计。我们分成三个维度,分别是y轴的位移、x轴的位移、小球大小、小球透明度。 1.y轴位移:小球先有一小段的上升,上升到最高点,这一段y轴位移一直在增加,我们给它动画叫做throwTopY,接下来抛物线下降,这个过程y轴一直在减少,一直减少到我们终点为止,我们叫做throwDropY。 // animationBall.wxss @keyframes throwTopY { 0% { top: var(--startY); } 100% { top: calc(var(--startY) - 120rpx); } } @keyframes throwDropY { 0% { top: calc(var(--startY) - 120rpx); } 100% { top: var(--endY); } } 2.x轴位移:小球x轴的位移在整个运动过程中都是从右向左变化的,我们认为一直是在线性变化的,我们动画称为throwX。 // animationBall.wxss @keyframes throwX { 0% { left: var(--startX); } 100% { left: var(--endX); } } 3.小球大小:小球大小在整个过程中,上升阶段是变大的,我们称为scaleTop, 当下降的时候小球一直在变小,称为scaleDrop。 // animationBall.wxss @keyframes scaleSize { 0% { width: 20rpx; height: 20rpx; } 100% { width: 10rpx; height: 10rpx; } } 4.小球透明度:小球的透明度在初始阶段时候是能看到的,然后再接下来的过程是慢慢变成透明,直到完全透明,我们称showAndHide @keyframes showAndHide { 0% { opacity: 1; } 90% { opacity: 0.9; } 100% { opacity: 0; } } 按照以上的分析,单个维度的动画都已经写出来了,剩下来就是将他们组合在一起,组合的核心就是动画时间、动画曲线(贝塞尔曲线)以及动画停留在哪里~ 假设整个动画的过程是需要0.5秒,上升过程我们假定0.2秒,下降的过程0.3秒 我们先用伪代码写出思路来,思考0.2秒的动画中发生了什么事情? y轴位移变大小球变大所以这个0.2秒发生了 = throwTopY + scaleTop 0.3秒发生了什么事情? y轴位移变小小球变小所以这个0.3秒发生了 = throwDropY + scaleDrop 那么整体的0.5秒钟x轴和透明度其实也发生了变化,0.5秒的throwX,0.5秒的showAndHide 思路有了之后,代码其实就已经出来了 // animationBall.wxss .animationBall { animation-fill-mode: forwards; animation: throwTopY 0.2s cubic-bezier(0, 0.3, 0.3, 1) forwards, scaleTop 0.2s cubic-bezier(0.48, 0.33, 0.24, 1.18) forwards, throwDropY 0.3s cubic-bezier(0.7, 0, 1, 0.7) 0.2s forwards, scaleDrop 0.3s cubic-bezier(0.48, 0.33, 0.24, 1.18) 0.2s forwards, throwX 0.46s linear forwards, showAndHide 0.5s linear forwards; } // animationBall.wxml <view class="ball-box animationBall" wx:if="{{showBall}}" style="{{style}}"></view> 上诉中涉及到贝赛尔曲线问题,这块大家自行了解下,不是本篇文章重点。 至此我们完成了小球抛物线动画的实现,不过还有个问题,我们如何知道小球完成了抛物线,并且通知到当前页面的购物车,让购物车发生抖动或者变大的效果呢?重点就在于如何通知动画完成~ 我们可以对小球绑定一个监听动画的事件bindanimationend,这个事件代表最后一个动画结束,我们最后一个动画是showAndHide,所以我们通过此去进行判断即可,抛出事件后,页面获取到再做后续购物车的变化(这块也不是重点,本篇文章忽略) // animationBall.wxml <view bindanimationend="observeAnimation" class="ball-box animationBall" wx:if="{{showBall}}" style="{{style}}"> </view> // animationBall.js watchAnimation(res) { // 最后一组动画结束 抛出事件通知外部 if (res?.detail?.animationName === "showAndHide") { this.triggerEvent("animationHasDone"); } } 以上就是所有的内容了,按照这样操作,你也可以实现丝滑的加购动画啦~
2023-10-25 - 适配最新微信小程序隐私协议开发指南
准备工作小程序后台设置用户隐私保护指引,需要等待审核通过:设置-基本设置-服务内容声明-用户隐私保护指引小程序的基础库版本从 2.32.3 开始支持,所以要选这之后的版本在 app.json 中加上这个设置 "usePrivacyCheck": true具体步骤可以参考官方给的开发文档,里面也有官方提供的 demo 文件。https://developers.weixin.qq.com/miniprogram/dev/framework/user-privacy/ [图片] 原生小程序适配代码直接参考的官方给的 demo3 和 demo4 综合修改出的版本,通过组件的方式引用,所有相关处理逻辑全部放到了 privacy 组件内部,其他涉及到隐私接口的页面只需在 wxml 里引用一下就行了,其他任何操作都不需要,组件内部已经全部处理了。 网上有其他人分享的,要在页面 onLoad、onShow 里获取是否有授权这些,用下面的代码这些都不需要,只要页面里需要隐私授权,引入 privacy 组件后,用户触发了隐私接口时会自动弹出此隐私授权弹窗。 [图片] 新建一个 privacy 组件:privacy.wxml、privacy.wxss、privacy.js、privacy.json,完整代码在下方在涉及隐私接口的页面引入 privacy 组件,如果使用的页面比较多,可以直接在 app.json 文件里通过 usingComponents 全局引入privacy.wxml <view wx:if="{{innerShow}}" class="privacy"> <view class="privacy-mask" /> <view class="privacy-dialog-wrap"> <view class="privacy-dialog"> <view class="privacy-dialog-header">用户隐私保护提示</view> <view class="privacy-dialog-content">感谢您使用本小程序,在使用前您应当阅读井同意<text class="privacy-link" bindtap="openPrivacyContract">《用户隐私保护指引》</text>,当点击同意并继续时,即表示您已理解并同意该条款内容,该条款将对您产生法律约束力;如您不同意,将无法继续使用小程序相关功能。</view> <view class="privacy-dialog-footer"> <button id="btn-disagree" type="default" class="btn btn-disagree" bindtap="handleDisagree" >不同意</button> <button id="agree-btn" type="default" open-type="agreePrivacyAuthorization" class="btn btn-agree" bindagreeprivacyauthorization="handleAgree" >同意并继续</button> </view> </view> </view> </view> privacy.wxss .privacy-mask { position: fixed; z-index: 5000; top: 0; right: 0; left: 0; bottom: 0; background: rgba(0, 0, 0, 0.2); } .privacy-dialog-wrap { position: fixed; z-index: 5000; top: 16px; bottom: 16px; left: 80rpx; right: 80rpx; display: flex; align-items: center; justify-content: center; } .privacy-dialog { background-color: #fff; border-radius: 32rpx; } .privacy-dialog-header { padding: 60rpx 40rpx 30rpx; font-weight: 700; font-size: 36rpx; text-align: center; } .privacy-dialog-content { font-size: 30rpx; color: #555; line-height: 2; text-align: left; padding: 0 40rpx; } .privacy-dialog-content .privacy-link { color: #2f80ed; } .privacy-dialog-footer { display: flex; padding: 20rpx 40rpx 60rpx; } .privacy-dialog-footer .btn { color: #FFF; font-size: 30rpx; font-weight: 500; line-height: 100rpx; text-align: center; height: 100rpx; border-radius: 20rpx; border: none; background: #07c160; flex: 1; margin-left: 30rpx; justify-content: center; } .privacy-dialog-footer .btn::after { border: none; } .privacy-dialog-footer .btn-disagree { color: #07c160; background: #f2f2f2; margin-left: 0; } privacy.js let privacyHandler let privacyResolves = new Set() let closeOtherPagePopUpHooks = new Set() if (wx.onNeedPrivacyAuthorization) { wx.onNeedPrivacyAuthorization(resolve => { if (typeof privacyHandler === 'function') { privacyHandler(resolve) } }) } const closeOtherPagePopUp = (closePopUp) => { closeOtherPagePopUpHooks.forEach(hook => { if (closePopUp !== hook) { hook() } }) } Component({ data: { innerShow: false, }, lifetimes: { attached: function() { const closePopUp = () => { this.disPopUp() } privacyHandler = resolve => { privacyResolves.add(resolve) this.popUp() // 额外逻辑:当前页面的隐私弹窗弹起的时候,关掉其他页面的隐私弹窗 closeOtherPagePopUp(closePopUp) } closeOtherPagePopUpHooks.add(closePopUp) this.closePopUp = closePopUp }, detached: function() { closeOtherPagePopUpHooks.delete(this.closePopUp) } }, pageLifetimes: { show: function() { this.curPageShow() } }, methods: { handleAgree(e) { this.disPopUp() privacyResolves.forEach(resolve => { resolve({ event: 'agree', buttonId: 'agree-btn' }) }) privacyResolves.clear() }, handleDisagree(e) { this.disPopUp() privacyResolves.forEach(resolve => { resolve({ event: 'disagree', }) }) privacyResolves.clear() }, popUp() { if (this.data.innerShow === false) { this.setData({ innerShow: true }) } }, disPopUp() { if (this.data.innerShow === true) { this.setData({ innerShow: false }) } }, openPrivacyContract() { wx.openPrivacyContract({ success: res => { console.log('openPrivacyContract success') }, fail: res => { console.error('openPrivacyContract fail', res) } }) }, curPageShow() { if (this.closePopUp) { privacyHandler = resolve => { privacyResolves.add(resolve) this.popUp() // 额外逻辑:当前页面的隐私弹窗弹起的时候,关掉其他页面的隐私弹窗 closeOtherPagePopUp(this.closePopUp) } } } } })
2023-11-15 - mp-image-compress小程序图片压缩接口(图片压缩至指定大小),并返回width, height, size
mp-image-compress 小程序图片压缩接口(图片压缩至指定大小),并返回 width, height, size 为规范前端用户上传照片的大小,减轻存储服务器压力,当用户选取的照片过大时,在前端自动进行压缩至指定大小(比如 1M )后再上传。 1 - 仓库地址 mp-image-compress【github】 mp-image-compress【码云gitee】 2 - 测试环境 微信小程序基础库版本:2.32.3 及以上 3 - 安装方法:npm安装 [代码]npm i mp-image-compress --save [代码] 依赖包安装后,需要在小程序开发者工具中:构建npm 4 - 接口使用示例 index.js [代码]// 引用 mp-image-compress import mpImageCompress from 'mp-image-compress' Page({ data: { img: '' }, onLoad() { mpImageCompress.clearTempImg() // 页面载入时清除临时文件 }, onUnload() { mpImageCompress.clearTempImg() // 页面卸载时清除临时文件 }, chooseImage() { const that = this wx.chooseMedia({ count: 1, mediaType: ['image'], sourceType: ['album', 'camera'], sizeType: ['original', 'compressed'], async success(res) { const info = res.tempFiles[0] // 选择文件后调用图片压缩接口进行压缩。如果选取的图片没有达到指定的大小,将返回原图 const imgRes = await mpImageCompress.set(info.tempFilePath, 1024) // 1024K console.info(imgRes) /////////////////////// that.setData({ img: imgRes.filePath }) } }) }, preview() { wx.previewImage({ current: this.data.img, urls: [this.data.img] }) }, DelImg() { this.setData({ img: '' }) }, }) [代码]
2023-11-16 - 你不知道的小程序系列之生命周期执行顺序
再次开始之前先问几个问题: 你是否知道[代码]Page[代码]生命周期 与 [代码]pagelifetimes[代码] 生命周期执行顺序? 你是否知道[代码]behaviors[代码]中的生命周期与组件生命周期执行顺序? 你是否知道[代码]Page[代码]生命周期 与 组件[代码]pagelifetimes[代码]生命周期执行顺序? 要回答上面的问题,首先我们看看小程序生命周期有哪些: App onLaunch onShow onHide Page onLoad onShow onReady onHide onUnload Component created attached ready moved detached 想一下加载一个页面(包含组件)的加载顺序,按照直觉小程序加载顺序应该是这样的加载顺序(以下列子中[代码]Component[代码]都是同步组件): App(onLaunch) -> Page(onLoad) -> Component(created) 但其实并不然,小程序的加载顺序是这样的: 首先执行 [代码]App.onLaunch[代码] -> [代码]App.onShow[代码] 其次执行 [代码]Component.created[代码] -> [代码]Component.attached[代码] 再执行 [代码]Page.onLoad[代码] -> [代码]Page.onShow[代码] 最后 执行 [代码]Component.ready[代码] -> [代码]Page.onReady[代码] 其实也不难理解微信这么设计背后的逻辑,我们先看下官方的的生命周期: [图片] 可以看到,在页面[代码]onLoad[代码]之前会有页面[代码]create[代码]阶段,这其中就包含了组件的初始化,等组件初始化完成之后,才会执行页面的[代码]onLoad[代码], 之后页面[代码]ready[代码]事件也是在组件[代码]ready[代码]之后才触发的。 下面我们来看看 [代码]Behavior[代码], [代码]Behavior[代码] 与 [代码]Vue[代码]中的 [代码]mixin[代码] 类似,猜想下其中的执行顺序: Behavior.created => Component.created 测试下来和预期相符,其实在[代码]Vue[代码]的文档中有一段这样的描述: 另外,混入对象的钩子将在组件自身钩子之前调用。 这样的设计和主流设计保持一致。接下来我们看看 [代码]pageLifetimes[代码],有[代码]show[代码]和[代码]hide[代码]生命周期对应页面的展示与隐藏,预期的执行顺序: pageLifetime.show => Page.onShow 测试下来也和预期相符,那么我们可以推断出如下的结论: 当页面中包含组件时,组件的生命周期(包括pageLifetimes)总是优先于页面,[代码]Behaviors[代码]生命周期优先于组件的生命周期。但其实有个例外:页面退出堆栈,当页面[代码]unload[代码]时会执行如下顺序: Page.onUnload => Component.detached 看了以上的分析你应该知道了答案,最后做个总结(demo): [图片] 最后的最后布置个作业 异步组件(异步渲染的组件,通常是通过if条件判断是否渲染)的生命周期执行顺序是怎样的,pagelifetimes会不会执行?
2020-01-10