个人案例
- 题库盒子
疫情期间帮助学生在线答题
题库盒子扫码体验
- 获取图片exif元数据信息
官方api并没有关于图片exif信息提取的api,搜索各种资料无果 不过发现了一个js版本的exif.js 直接拿到小程序上并不能使用,经过修改现在可以使用。并没有太多的测试,如果有发现bug希望共同维护一下 直到官方出了可以获取exif的api 使用方法: 1,引入js var myexif = require('../../libs/myexif.js'); 2,调用handleBinaryFile()方法(方法参数是bufferArray) wx.chooseImage({//选择图片 sizeType: ['compressed'],//图片不能经过压缩处理 success(res) { var array = wx.getFileSystemManager().readFileSync(res.tempFilePaths[0] );; var r =myexif.handleBinaryFile(array); console.log(r); } }); 百度网盘(myexif.js): 链接: https://pan.baidu.com/s/1Z84kyPHowXlgyg1czKyu7A 提取码: we9d
2018-11-26 - navigateToMiniProgram和navigator跳转小程序后显示页面不存在?
两种跳转小程序的方法都用了,不加path是能跳转到首页的,加上之后就显示“页面不存在”。从网上找了个测试的能跳转,自己测了3个都显示页面不存在 [图片] [图片] [图片] [图片]
2019-10-10 - 如何实现快速生成朋友圈海报分享图
由于我们无法将小程序直接分享到朋友圈,但分享到朋友圈的需求又很多,业界目前的做法是利用小程序的 Canvas 功能生成一张带有小程序码的图片,然后引导用户下载图片到本地后再分享到朋友圈。相信大家在绘制分享图中应该踩到 Canvas 的各种(坑)彩dan了吧~ 这里首先推荐一个开源的组件:painter(通过该组件目前我们已经成功在支付宝小程序上也应用上了分享图功能) 咱们不多说,直接上手就是干。 [图片] 首先我们新增一个自定义组件,在该组件的json中引入painter [代码]{ "component": true, "usingComponents": { "painter": "/painter/painter" } } [代码] 然后组件的WXML (代码片段在最后) [代码]// 将该组件定位在屏幕之外,用户查看不到。 <painter style="position: absolute; top: -9999rpx;" palette="{{imgDraw}}" bind:imgOK="onImgOK" /> [代码] 重点来了 JS (代码片段在最后) [代码]Component({ properties: { // 是否开始绘图 isCanDraw: { type: Boolean, value: false, observer(newVal) { newVal && this.handleStartDrawImg() } }, // 用户头像昵称信息 userInfo: { type: Object, value: { avatarUrl: '', nickName: '' } } }, data: { imgDraw: {}, // 绘制图片的大对象 sharePath: '' // 生成的分享图 }, methods: { handleStartDrawImg() { wx.showLoading({ title: '生成中' }) this.setData({ imgDraw: { width: '750rpx', height: '1334rpx', background: 'https://qiniu-image.qtshe.com/20190506share-bg.png', views: [ { type: 'image', url: 'https://qiniu-image.qtshe.com/1560248372315_467.jpg', css: { top: '32rpx', left: '30rpx', right: '32rpx', width: '688rpx', height: '420rpx', borderRadius: '16rpx' }, }, { type: 'image', url: this.data.userInfo.avatarUrl || 'https://qiniu-image.qtshe.com/default-avatar20170707.png', css: { top: '404rpx', left: '328rpx', width: '96rpx', height: '96rpx', borderWidth: '6rpx', borderColor: '#FFF', borderRadius: '96rpx' } }, { type: 'text', text: this.data.userInfo.nickName || '青团子', css: { top: '532rpx', fontSize: '28rpx', left: '375rpx', align: 'center', color: '#3c3c3c' } }, { type: 'text', text: `邀请您参与助力活动`, css: { top: '576rpx', left: '375rpx', align: 'center', fontSize: '28rpx', color: '#3c3c3c' } }, { type: 'text', text: `宇宙最萌蓝牙耳机测评员`, css: { top: '644rpx', left: '375rpx', maxLines: 1, align: 'center', fontWeight: 'bold', fontSize: '44rpx', color: '#3c3c3c' } }, { type: 'image', url: 'https://qiniu-image.qtshe.com/20190605index.jpg', css: { top: '834rpx', left: '470rpx', width: '200rpx', height: '200rpx' } } ] } }) }, onImgErr(e) { wx.hideLoading() wx.showToast({ title: '生成分享图失败,请刷新页面重试' }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') }, onImgOK(e) { wx.hideLoading() // 展示分享图 wx.showShareImageMenu({ path: e.detail.path, fail: err => { console.log(err) } }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') } } }) [代码] 那么我们该如何引用呢? 首先json里引用我们封装好的组件share-box [代码]{ "usingComponents": { "share-box": "/components/shareBox/index" } } [代码] 以下示例为获取用户头像昵称后再生成图。 [代码]<button class="intro" bindtap="getUserInfo">点我生成分享图</button> <share-box isCanDraw="{{isCanDraw}}" userInfo="{{userInfo}}" bind:initData="handleClose" /> [代码] 调用的地方: [代码]const app = getApp() Page({ data: { isCanDraw: false }, // 组件内部关掉或者绘制完成需重置状态 handleClose() { this.setData({ isCanDraw: !this.data.isCanDraw }) }, getUserInfo(e) { wx.getUserProfile({ desc: "获取您的头像昵称信息", success: res => { const { userInfo = {} } = res this.setData({ userInfo, isCanDraw: true // 开始绘制海报图 }) }, fail: err => { console.log(err) } }) } }) [代码] 最后绘制分享图的自定义组件就完成啦~效果图如下: [图片] tips: 文字居中实现可以看下代码片段 文字换行实现(maxLines)只需要设置宽度,maxLines如果设置为1,那么超出一行将会展示为省略号 代码片段:https://developers.weixin.qq.com/s/J38pKsmK7Qw5 附上painter可视化编辑代码工具:点我直达,因为涉及网络图片,代码片段设置不了downloadFile合法域名,建议真机开启调试模式,开发者工具 详情里开启不校验合法域名进行代码片段的运行查看。 最后看下面大家评论问的较多的问题:downLoadFile合法域名在小程序后台 开发>开发设置里配置,域名为你图片的域名前缀 比如我文章里的图https://qiniu-image.qtshe.com/20190605index.jpg。配置域名时填写https://qiniu-image.qtshe.com即可。如果你图片cdn地址为https://aaa.com/xxx.png, 那你就配置https://aaa.com即可。
2022-01-20 - 小程序将小程序码与图片结合生成海报分享朋友圈
样例参考(瑞幸咖啡小程序) [图片][图片][图片] 需求分析 服务器端会返回不确认的图片资源到前端 前端将返回的每张图片都要贴上小程序码 将贴上小程序码的图片使用 swiper 组件轮播 用户点击保存时,将图片保存至相册。 至于点击保存如何保存至相册(wx.saveImageToPhotosAlbuml 了解一下,注意一下授权问题即可) 碰到的问题 canvas为原生组件, 而原生组件的层级是最高的,所以页面中的其他组件无论设置 z-index 为多少,都无法盖在原生组件上。 [代码]采用方法: 通过定位,将其移除到不在可视范围,如iphone6 .canvas { position: relative; left: -375px; } [代码] 绘制多张图片时, 一个canvas 标签只能对应画一张图片 (目前我测试的是这样,如有其它方法,欢迎评论交流) [代码]采用方法: 1. html端循环要生成的图片张数,对应循环出多个 canvas 组件,分别设置不同的 canvasId 区分 2. 封装一个绘制海报的函数,返回一个Promise对象,用于后面绘制完所有的图片,统一赋值渲染,避免多次触发数据更新 3. js端 循环执行一次 绘制海豹函数,存于一个数组列表 4. 使用 Promise.all 函数,统一绘制完毕将生产图片路径赋值 html: <canvas v-for="u in m.urlList" :key="u" :canvas-id="'poster'+index" class="canvas" style="width:100%; height:100%;"> </canvas> <swiper :indicator-dots="true" :circular="true" :autoplay="true" indicator-color="rgba(255,255,255,.2)" indicator-active-color="#fff" class="swiper"> <swiper-item v-for="f in m.filePaths" :key="index"> <image :src="f"> </swiper-item> </swiper> js: // 数据 (mpvue 开发) function data(){ return { m : { urlLIst: [ // 图片资源 '/static/poster0.jpg', '/static/poster1.jpg' ], filePaths: [], // 生成图片(贴上小程序码) } } } try { let res = wx.getSystemInfoSync(); // 同步获取系统信息 let w = res.windowWidth; // 手机可用区域宽度 let h = res.windowHeight; // 手机可用区域高度 let codeUrl = '/static/code.jpg'; // 小程序码 let drawList = []; // 用于保存绘制海报图Promise对象 m.urlList.forEach( (u, i)=> { // 传入canvas组件ID,图片路径(测试使用的是本地路径) drawList.push(drawPoster('poster'+i, u, codeUrl)); }) // 统一更新数据 Promise.all(drawList).then((valuse)=>{ m.filePaths = valuse; }); // 封装绘制图片函数 function drawPoster(canvasId, bgUrl, codeUrl){ return new Promise( (resolve, reject) => { // 创建画布实例 let ctx = wx.createCanvasContext(canvasId); // 绘制背景图: 图片路径,x坐标,y坐标,宽,高 ctx.drawImage(bgUrl, 0, 0, w, h); // 绘制小程序码 ctx.drawImage(codeUrl, w-120, h-120, 100, 100); // 绘制 ctx.draw(false, ()=>{ // 该通过函数将canvas绘制导出为图片 wx.canvasToTempFilePath({ x: 0, y: 0, width: w, height: h, canvasId: canvasId, success(res){ resolve(res.tempFilePath); } }); }); } }catch(e){ // 自己封装了一成 wx.$toast(e); } [代码] 最终demo效果图 [图片][图片] 在社区中暂未看到多张海报实现的方案,如果有更好的实现方案,欢迎交流
2019-07-30 - 【从业应试练习】上线了
今天是第一次写自己小程序的案例,也把文章转到社区留个足迹,希望使用者提出改进建议。小程序名称【从业应试练习】,设置了每日一练、刷题练习、历年真题、精品试卷模拟以及个性化以往错题和收藏题目复习服务等,帮助应试人员有效利用零碎的时间进行练习复习。 做工程的往往都是忙人,很难有固定的时间来复习应试。但是在工作服务过程中又经常存在因相互等待而浪费时间,有效利用这些零碎的时间来进行复习,最便捷的就是应用随身所带的手机了(带书籍到工地复习还真有点不好意思^_^),因此想到了要利用微信小程序做一款方便做题复习。春节的疫情要求居家刚好促进了这个想法的实现,大概经过1月多的代码拼凑终于提交通过了审核(业余水平,不要笑话我写代码效率太低呵),然后是间断的题库整理补充和2轮的改进,最终【从业应试练习】V1.0.2版和大家见面了,公开分享,欢迎使用并提出改进建议。感谢编制过程中给予帮助的各位友人,特别是“小程序开放社区沟通群”中朋友的解答指教,小程序编制中也借鉴采用了网上查找来的许多方法、技巧等,在此也一并感谢。 [图片] [图片][图片][图片]
2020-03-28 - 在线答题小程序交互设计整理七
突然看到一个非常友好的在线答题的小程序交互设计,在后面的优化中,可以参考下 在线答题小程序交互设计整理七 ### 本文概述 今天体验的小程序名字是:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx [图片] [图片] [图片] [图片] 总结 在答题页,采用了类似卡片的形式,这个是加分项。
2020-01-15 - “签到”功能,不是你想象的那么简单
签到功能的存在,不仅能够提高用户活跃,还能够刺激用户完成指定行为。签到的同时伴随的荣誉感、仪式感等精神奖励。 [图片] 如果通过签到可赚取积分、经验值以及兑换奖品,达到激励用户的目的,那么它还有个更高级的名字:游戏化思维。 发现在小程序里以「签到」为基础的游戏化思维策略,已经成为开发者的基本打法。 1、如果你是高频小程序,要做签到,这会进一步提高用户粘性; 2、如果你是低频小程序,更要做签到,这是低频变高频的快速通道。 小程序里的签到到底怎么玩? 放长线钓大鱼 在很多人看来,签到或许只是提高用户留存率的方法,其实签到并不仅限于留存,还有拉新促活。 用优质商品吸引用户签到,每次签到可得积分,积分可换购商品,如果用户想获得更多积分,只能多做任务 这样一来,用签到,便完成了引导用户闭环营销。 免费送“钱”,你要不要? 用户在平台里每天签到1次,都能获得积分 用户拿着积分,唯一能做的事,就是尽情的兑换商品,无需考虑“省钱”,反正除了小程序以外,积分在其他地方也毫无用武之地。 而通过连续的签到行为逐渐增加用户的获利程度,刺激用户养成连续签到习惯。 不提留存的签到,都是耍流氓 虽然有些人说签到不仅是为了留存,但用这一玩法真正受益的还是小程序的留存率。 促进内容生产 签到导致产生内容,可以进一步活跃用户,带动整个社群的气氛,让用户在这个圈子里产生互动。这是留存的力量。 就连最近非常流行的步数类小程序,也离不开签到所带来的“回头率”。 [图片] 签到对于小程序本身,也是一种具有仪式感的功能,如果你使用得当,一定能为你的小程序锦上添花。
2020-02-21 - 小程序悬浮按钮,悬浮导航球
[图片] 一个开源的悬浮按钮组件,小程序原生支持。 一直很喜欢华为的导航按钮,能够完美适合大屏手机,自由停放位置,不论是左手习惯还是右手习惯,都很方便(可能我手比较小,左右上角够不着)。 支持功能 支持自由拖动,停放 支持自定义事件(单击,双击,长按) 支持自定义导航球中间的文字/图片 开发难点 使用wxs 悬浮球的开发思路比较简单,一个view,样式[代码]position:fixed[代码],支持拖动。在web开发中,我们能够比较容易实现这样的功能。要想在小程序中实现高性能的交互动画(touch类),一定要了解如何使用页面的[代码]wxs[代码]这个残疾JS来操作对象(调试很麻烦,js极度残疾) [代码]<wxs module="tool"> function tStart(e, ins){} function tMove(e, ins){ e.instance.setStyle('transform: translate3d(...)') // e.instance指向当前操作对象 // setStyle 设置该对象的style样式 } function tEnd(e, ins){} module.exports = { tStart: tStart, tMove: tMove, tEnd: tEnd } </wxs> <view catch:touchstart="{{tool.tStart}}" catch:touchmove="{{tool.tMove}}" ... /> [代码] 这里使用catch,而不是使用bind来绑定事件,事件指向[代码]wxs[代码]的方法。考虑到悬浮导航球是作为工具在其他场景中使用,为了不会污染touch事件,或者导致页面不必要的滚动。 位移距离 手机宽高不一致,即x轴的运动距离小于y轴运动距离(单位时间),假定手机宽高比为1:2,x轴运动1px,y轴则运动了2px,我们可以设置一定的系数,使得拖动效果符合预期。 监听事件 最终的事件响应一定是在page页面(或者组件内部)实现事件监听,wxs有一套事件调用机制 [代码]function tStart(e, ins){ ins.callMethod('onTouchStart', e) // 调用当前组件/页面在逻辑层(App Service)定义的函数。funcName表示函数名称,args表示函数的参数 } [代码] wxs相关文档 GITHUB开源 DEMO及文档关注小程序 [图片]
2020-03-06 - 开源社区小程序「玉帛书」后端已经开源了
社区小程序「玉帛书」服务端API 开源地址 基于eggjs开发(eggjs+mysql+redis) [代码]$ cd api $ npm install $ npm run dev [代码] 具体步骤 手动创建mysql数据库:community 在api目录下运行命令: [代码]npx sequelize db:migrate [代码] 打开config目录下的config.default.js进行相关配置 [代码] /** * 微信小程序配置 */ config.miniprogram = { appid: '', secret: '', }; /** * qq小程序配置 */ config.qqminiprogram = { appid: '', secret: '' }; /** * 公众号配置 */ config.yitao = { appid: '', secret: '', }; /**七牛存储配置 */ config.qiniu = { AccessKey: '', SecretKey: '', bucket: '', }; [代码]
2020-03-08 - 各种国内地图坐标系总结
一、国内的常用坐标系 1、WGS-84坐标系:地心坐标系,GPS原始坐标体系 在中国,任何一个地图产品都不允许使用GPS坐标,据说是为了保密。 2、GCJ-02 坐标系:国测局坐标,火星坐标系 1)国测局02年发布的坐标体系,它是一种对经纬度数据的加密算法,即加入随机的偏差。 2)互联网地图在国内必须至少使用GCJ-02进行首次加密,不允许直接使用WGS-84坐标下的地理数据,同时任何坐标系均不可转换为WGS-84坐标。 3)是国内最广泛使用的坐标体系,高德、腾讯、Google中国地图都使用它。 3、CGCS2000坐标系:国家大地坐标系 该坐标系是通过中国GPS 连续运行基准站、 空间大地控制网以及天文大地网与空间地网联合平差建立的地心大地坐标系统。 4、BD-09坐标系 百度中国地图所采用的坐标系,由GCJ-02进行进一步的偏移算法得到。 5、搜狗坐标系 搜狗地图所采用的坐标系,由GCJ-02进行进一步的偏移算法得到。 6、图吧坐标系 图吧地图所采用的坐标系,由GCJ-02进行进一步的偏移算法得到。 二、国内地图软件所采用的坐标系简介 1、百度地图 1)境内(包括港澳台):BD09 a、在GCJ-02坐标系基础上再次加密 b、支持WGS-84、GCJ-02转换成BD09,反向不支持,并且批量转换一次有条数限制 2)境外:WGS-84 2、高德地图: 1)境内:GCJ-02 a、WGS-84——>GCJ-02(高德有接口提供,反过来没有) 2)境外:暂不支持 3)AMap 就是高德地图,是高德地图在纳斯达克上市用的名字,主要面向互联网企业或个人提供免费API服务 4)MapABC 是高德集团底下的图盟公司,主要面向大众型企业或政府机关,并提供付费的有偿服务 5)Amap和MapABC,数据和服务都是共享的,所以Mapabc用Amap的API是正常的 3、google地图 1)境内:GCJ-02 a、数据来源于高德,两者互通 2)境外:WGS-84 4、天地图 全球统一:CGCS2000 5、腾讯地图:soso地图 境内:GCJ02 6、微软bing地图:BingMap 全球统一:WGS-84 7、搜狗地图 境内:搜狗坐标系 a、在GCJ-02坐标系基础上再次加密 b、支持WGS-84、GCJ-02、BD09转换成搜狗坐标,反向不支持 8、图吧地图: MapBar 境内:图吧坐标系 a、在GCJ-02坐标系基础上再次加密 9、阿里云地图 境内:GCJ-02 10、灵图地图:51ditu 境内:GCJ-02 三、各个坐标系之间的转换 1、以下代码,提供的转换算法如下: 1)WGS-84 ——> GCJ02 2)GCJ02 ——> WGS-84 3)GCJ02 ——> BD09 4)BD09 ——> GCJ02 5)BD09 ——> WGS-84 1. package com.xy; 2. 3. /** 4. * 各地图API坐标系统比较与转换; 5. * WGS84坐标系:即地球坐标系,国际上通用的坐标系。设备一般包含GPS芯片或者北斗芯片获取的经纬度为WGS84地理坐标系, 6. * 谷歌地图采用的是WGS84地理坐标系(中国范围除外); 7. * GCJ02坐标系:即火星坐标系,是由中国国家测绘局制订的地理信息系统的坐标系统。由WGS84坐标系经加密后的坐标系。 8. * 谷歌中国地图和搜搜中国地图采用的是GCJ02地理坐标系; BD09坐标系:即百度坐标系,GCJ02坐标系经加密后的坐标系; 9. * 搜狗坐标系、图吧坐标系等,估计也是在GCJ02基础上加密而成的。 10. */ 11. public class PositionUtil { 12. 13. public static final String BAIDU_LBS_TYPE = "bd09ll"; 14. 15. public static double pi = 3.1415926535897932384626; 16. public static double a = 6378245.0; 17. public static double ee = 0.00669342162296594323; 18. 19. /** 20. * 84 to 火星坐标系 (GCJ-02) World Geodetic System ==> Mars Geodetic System 21. * 22. * @param lat 23. * @param lon 24. * @return 25. */ 26. public static Gps gps84_To_Gcj02(double lat, double lon) { 27. if (outOfChina(lat, lon)) { 28. return null; 29. } 30. double dLat = transformLat(lon - 105.0, lat - 35.0); 31. double dLon = transformLon(lon - 105.0, lat - 35.0); 32. double radLat = lat / 180.0 * pi; 33. double magic = Math.sin(radLat); 34. magic = 1 - ee * magic * magic; 35. double sqrtMagic = Math.sqrt(magic); 36. dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi); 37. dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi); 38. double mgLat = lat + dLat; 39. double mgLon = lon + dLon; 40. return new Gps(mgLat, mgLon); 41. } 42. 43. /** 44. * * 火星坐标系 (GCJ-02) to 84 * * @param lon * @param lat * @return 45. * */ 46. public static Gps gcj_To_Gps84(double lat, double lon) { 47. Gps gps = transform(lat, lon); 48. double lontitude = lon * 2 - gps.getWgLon(); 49. double latitude = lat * 2 - gps.getWgLat(); 50. return new Gps(latitude, lontitude); 51. } 52. 53. /** 54. * 火星坐标系 (GCJ-02) 与百度坐标系 (BD-09) 的转换算法 将 GCJ-02 坐标转换成 BD-09 坐标 55. * 56. * @param gg_lat 57. * @param gg_lon 58. */ 59. public static Gps gcj02_To_Bd09(double gg_lat, double gg_lon) { 60. double x = gg_lon, y = gg_lat; 61. double z = Math.sqrt(x * x + y * y) + 0.00002 * Math.sin(y * pi); 62. double theta = Math.atan2(y, x) + 0.000003 * Math.cos(x * pi); 63. double bd_lon = z * Math.cos(theta) + 0.0065; 64. double bd_lat = z * Math.sin(theta) + 0.006; 65. return new Gps(bd_lat, bd_lon); 66. } 67. 68. /** 69. * * 火星坐标系 (GCJ-02) 与百度坐标系 (BD-09) 的转换算法 * * 将 BD-09 坐标转换成GCJ-02 坐标 * * @param 70. * bd_lat * @param bd_lon * @return 71. */ 72. public static Gps bd09_To_Gcj02(double bd_lat, double bd_lon) { 73. double x = bd_lon - 0.0065, y = bd_lat - 0.006; 74. double z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * pi); 75. double theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * pi); 76. double gg_lon = z * Math.cos(theta); 77. double gg_lat = z * Math.sin(theta); 78. return new Gps(gg_lat, gg_lon); 79. } 80. 81. /** 82. * (BD-09)-->84 83. * @param bd_lat 84. * @param bd_lon 85. * @return 86. */ 87. public static Gps bd09_To_Gps84(double bd_lat, double bd_lon) { 88. 89. Gps gcj02 = PositionUtil.bd09_To_Gcj02(bd_lat, bd_lon); 90. Gps map84 = PositionUtil.gcj_To_Gps84(gcj02.getWgLat(), 91. gcj02.getWgLon()); 92. return map84; 93. 94. } 95. 96. public static boolean outOfChina(double lat, double lon) { 97. if (lon < 72.004 || lon > 137.8347) 98. return true; 99. if (lat < 0.8293 || lat > 55.8271) 100. return true; 101. return false; 102. } 103. 104. public static Gps transform(double lat, double lon) { 105. if (outOfChina(lat, lon)) { 106. return new Gps(lat, lon); 107. } 108. double dLat = transformLat(lon - 105.0, lat - 35.0); 109. double dLon = transformLon(lon - 105.0, lat - 35.0); 110. double radLat = lat / 180.0 * pi; 111. double magic = Math.sin(radLat); 112. magic = 1 - ee * magic * magic; 113. double sqrtMagic = Math.sqrt(magic); 114. dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi); 115. dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi); 116. double mgLat = lat + dLat; 117. double mgLon = lon + dLon; 118. return new Gps(mgLat, mgLon); 119. } 120. 121. public static double transformLat(double x, double y) { 122. double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y 123. + 0.2 * Math.sqrt(Math.abs(x)); 124. ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0; 125. ret += (20.0 * Math.sin(y * pi) + 40.0 * Math.sin(y / 3.0 * pi)) * 2.0 / 3.0; 126. ret += (160.0 * Math.sin(y / 12.0 * pi) + 320 * Math.sin(y * pi / 30.0)) * 2.0 / 3.0; 127. return ret; 128. } 129. 130. public static double transformLon(double x, double y) { 131. double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 132. * Math.sqrt(Math.abs(x)); 133. ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0; 134. ret += (20.0 * Math.sin(x * pi) + 40.0 * Math.sin(x / 3.0 * pi)) * 2.0 / 3.0; 135. ret += (150.0 * Math.sin(x / 12.0 * pi) + 300.0 * Math.sin(x / 30.0 136. * pi)) * 2.0 / 3.0; 137. return ret; 138. } 139. 140. public static void main(String[] args) { 141. 142. // 北斗芯片获取的经纬度为WGS84地理坐标 31.426896,119.496145 143. Gps gps = new Gps(31.426896, 119.496145); 144. System.out.println("gps :" + gps); 145. Gps gcj = gps84_To_Gcj02(gps.getWgLat(), gps.getWgLon()); 146. System.out.println("gcj :" + gcj); 147. Gps star = gcj_To_Gps84(gcj.getWgLat(), gcj.getWgLon()); 148. System.out.println("star:" + star); 149. Gps bd = gcj02_To_Bd09(gcj.getWgLat(), gcj.getWgLon()); 150. System.out.println("bd :" + bd); 151. Gps gcj2 = bd09_To_Gcj02(bd.getWgLat(), bd.getWgLon()); 152. System.out.println("gcj :" + gcj2); 153. } 154. } 2、百度在线转换API 1. http://api.map.baidu.com/ag/coord/convert?from=0&to=4&x=longitude&y=latitude 2. from: 来源坐标系 (0表示WGS-84坐标,2表示GCJ-02坐标) 3. to: 转换后的坐标 (4就是百度自己啦,这个必须是4才行) 4. x: 精度 5. y: 纬度 得到的经纬度需要进一步转换才能得到BD-09坐标 1. import java.io.BufferedReader; 2. import java.io.IOException; 3. import java.io.InputStream; 4. import java.io.InputStreamReader; 5. import java.io.OutputStreamWriter; 6. import java.net.URL; 7. import java.net.URLConnection; 8. import com.sun.org.apache.xerces.internal.impl.dv.util.Base64; 9. public class BaiduAPIConverter extends Thread { 10. public static void testPost(String x, String y) throws IOException { 11. try { 12. URL url = new URL("http://api.map.baidu.com/ag/coord/convert?from=2&to=4&x="+ 13. x + "&y=" + y); 14. URLConnection connection = url.openConnection(); 15. connection.setDoOutput(true); 16. OutputStreamWriter out = new OutputStreamWriter(connection.getOutputStream(), "utf-8"); 17. // remember to clean up 18. out.flush(); 19. out.close(); 20. // 一旦发送成功,用以下方法就可以得到服务器的回应: 21. String sCurrentLine, sTotalString; 22. sCurrentLine = sTotalString = ""; 23. InputStream l_urlStream; 24. l_urlStream = connection.getInputStream(); 25. BufferedReader l_reader = new BufferedReader(new InputStreamReader(l_urlStream)); 26. while ((sCurrentLine = l_reader.readLine()) != null) { 27. if (!sCurrentLine.equals("")) 28. sTotalString += sCurrentLine; 29. } 30. sTotalString = sTotalString.substring(1, sTotalString.length() - 1); 31. String[] results = sTotalString.split("\\,"); 32. if (results.length == 3) { 33. if (results[0].split("\\:")[1].equals("0")) { 34. String mapX = results[1].split("\\:")[1]; 35. String mapY = results[2].split("\\:")[1]; 36. mapX = mapX.substring(1, mapX.length() - 1); 37. mapY = mapY.substring(1, mapY.length() - 1); 38. mapX = new String(Base64.decode(mapX)); 39. mapY = new String(Base64.decode(mapY)); 40. System.out.println("\t" + mapX + "\t" + mapY); 41. } 42. } 43. sleep(10000); 44. } catch (InterruptedException e) { 45. // TODO Auto-generated catch block 46. e.printStackTrace(); 47. } 48. } 49. /** 50. * @param args 51. * @throws IOException 52. */ 53. public static void main(String[] args) throws IOException { 54. testPost("120.151379", "30.184678"); 55. System.out.println("ok"); 56. } 57. } 3、百度地图获取WGS-84坐标 在百度地图中取得WGS-84坐标,调用如下方法: BMapManager.getLocationManager().setLocationCoordinateType( MKLocationManager.MK_COORDINATE_WGS84); 这样从百度api中取得的坐标就是WGS-84了,可是这种坐标如果显示到百度地图上就会偏移,也就是说取出一个坐标,原封不动的显示上去就偏移了,所以为了显示也是正常就需要在绘制到百度地图上之前转换成BD-09。 转换成BD-09,调用方法: GeoPoint wgs84; GeoPoint bd09 = CoordinateConvert.bundleDecode(CoordinateConvert.fromWgs84ToBaidu(wgs84)); 这里实在不明白为何要设计成CoordinateConvert.fromWgs84ToBaidu(wgs84)返回了一个Bundle,所以还需要CoordinateConvert.bundleDecode()再转成GeoPoint。 4、CGCS2000坐标与WGS-84坐标的转换 CGCS2000与WGS84的基本定义是一致的,采用的参考椭球非常相近,椭球常数中仅扁率有细微差别,虽然因此会造成同一点在两个坐标系中的值会有微小差异,但是,在当前测量精度水平下这种微小差值是可以忽略的,因此,可以认为CGCS2000和WGS84是相容的,在坐标系的实现精度范围内两种坐标系下的坐标是一致的。 另一方面,由于两者本身就不是由彼此加密或者解密就可以得到的关系,所以转换的算法十分复杂。通常采用工具软件来进行转换,如ArcGIS、BIGEMAP等。 四、取自一段大神的总结 采用自家坐标体系,而不采用国内通用的火星坐标体系,实在是自寻短处。当然,百度是因为做的足够大、足够好,所以很霸道,也为以后一统天下而不让别人瓜分之而做准备吧。搜狗虽然用自家坐标体系,但能将地球坐标直接导入,此举也属唯一。而图吧地图不知道学什么加密方式,以前用地球坐标用的好好的,现在用图吧自己的坐标,难道是因为给百度做过所以也来了这么一招?或者沿用百度?不得而知。 本文的目的在于:做地图开发的时候,不希望被一家地图API迁就,所以采用火星坐标GCJ-02是正确的选择,希望本文能够对选择使用谁家API的开发者提供一点帮助吧。就我个人而言,我绝不会使用非火星坐标系统的地图API,虽然百度地图API很好很强大确实很吸引我。
2020-03-24 - 大数据量swiper滑动优化
问题场景 事情是这样的,我做了一个在线答题小程序,有一个顺序练习模块,每次顺序练习,都要把整个题库过一遍,每个题库题目数量不一,有的几百,有的上千,为方便讨论,我们假定某个题库1000道题目, 具体答题模块是用swiper来实现的,当swiper的数组很大的时候,setData会有三四秒的延迟,我们都知道setData的效率,但是没想到这么厉害。 问题描述问题不是swiper本身,而是setData https://developers.weixin.qq.com/miniprogram/dev/framework/performance/tips.html 官方资料https://developers.weixin.qq.com/miniprogram/dev/component/swiper.html 功能演示 [视频] https://developers.weixin.qq.com/community/develop/article/doc/00040235334788f8651a168d951413 这个问题困扰了我几个周时间, 不敢梳头,因为白头发会掉了一地,不要问我为什么是白头发 不敢照镜子,因为黑眼圈更重了; 不敢出门走路,因为问题没有解决,抬不起头来。 [图片] 昨天我在群里发了一个有偿征求优化方案,小伙伴很积极,讨论了一个晚上,第二天就有朋友把优化的方案,递给我,今天中午验证有效,亲测。 [图片] 现在好了,问题得到圆满解决,心情都不一样了,原来处处都是美 [图片] 解决方案大家都能想到方案就是分页,虽然总数组长度为1000,但是每次渲染到swiper的可能只有3、5、7不等的小数组,这样通过setData传递到UI层的时候才不会卡。 但是分页的逻辑要我们自己来控制,怎么判断左滑动,怎么判断右滑动,滑动边界问题,很多细节,总之我为了这个问题花了一个周末没有解决, 就是在大方向明确的情况下,还是写不出来。 具体的实现方案晚点我整理下发出来,同时也希望想挑战的同学锻炼下。 在这里特别感谢群里的两位小伙伴 社区相关帖子 https://developers.weixin.qq.com/community/develop/article/doc/000ecafb3486f07000c92c3225c013 https://developers.weixin.qq.com/community/develop/doc/000e4c77da47208296f8b0b4c51800 感谢@~~娃娃 @~~ dinner
2020-03-25 - 【笔记】小程序优化过程中遇到几个问题总结
这几天一直在优化小程序遇到几个问题 1、setData大数据了赋值是卡顿 2、swiper滑动 3、缓存利用, 4、数据规整前置处理 5、mysql的数据如何导入云开发的数据库 等项目完结,挨着总结下,先记录在这里, 这几天一直在优化小程序遇到几个问题 相关帖子 堂堂大腾讯,请大幅度优化一下setData的效率吧 https://developers.weixin.qq.com/community/develop/doc/000a06914000c821652836dc756800 微信小程序答题页实现——swiper渲染优化 https://developers.weixin.qq.com/community/develop/article/doc/000ecafb3486f07000c92c3225c013 这几天一直在优化小程序遇到几个问题
2020-03-08 - 为什么人人都对我的小程序印象深刻?还不是因为“漂亮”!
界面的设计真的很重要! 我的小程序很多,但是每一个都让访问过的人赞叹不已,客户反馈的都是两个字“漂亮”! 同样的程序,做出了不一样的高级感,就像是同样的食物,米其林厨师出品出不一样的高贵味道! 深耕微信生态有7年了,从公众号到小程序,一直在做微信生态内的服务商。技术过硬,同样需要界面上的“靓”来搭配。 小程序的设计是一门学问:UI只是一部分,注重功能体验的UI才是好设计,打个比喻,注重功能的UI设计是定制衣服,单纯设计UI只是在衣服上画画! 通过以下几个小程序的案例,你可以发现那些「功能与设计微妙结合」的地方吗?一起来发现吧~ [图片][图片][图片][图片]
2020-03-06 - 手把手教你开通小程序流量主
流量主这个概念在小程序出来之前的公众号时代已经推出,主要是给一部分有流量的用户提供广告展示机会,用来换取一定利益 前几天我刚注册了一个小程序,一顿操作猛如虎,今天终于开通了,每一次开通流量主的感觉真的无比美好,是不是以后我就可以趟着数钱钱了,所谓趟赚了 今天具体实操下 [图片] 占位 [图片] 占位 [图片] 占位 扫码可以绑定,方便既是查收日常流量主收益 [图片] 占位 [图片] https://ad.weixin.qq.com/guide/1189 开通流量主后,小程序开发者要对应在自己的小程序添加广告位,才会有广告收益,这里跟公众号不一样,公众号是开通流量主后,自动在底部或者文章内部展示广告位。 看看日常的一些收益吧 [图片] 占位占位占位占位占位占位占位占位占位占位占位占位占位占位占位占位占位占位占位占位占位占位占位占位占位占位占位占位v 开通流量主之后如何添加广告位 [图片] 占位 [图片] 占位 https://ad.weixin.qq.com/guide/1195 [图片] 占位 [图片] 占位 [图片] 占位 点击立即创建 [图片] 占位 点击新建广告 [图片] 占位 选择创建视频广告 [图片] [图片] 占位 [图片] 占位 复制代码,我们就可以把广告位插入到具体要展示的小程序广告位置
2020-03-24 - SAUI改版
这次的更新: 首页改版(提供两种风格,可点击底部的风格按钮进行切换)onChangeStyle: function(e, params, inst) { const $indexSwiper = this.getElementsById('indexSwiper') //找到实例 if(params.cur == 0){ $indexSwiper.update({'$list.type': {is: 'swiper', circular: false, duration: 300, 'indicator-dots': true, current: 0}, '$list.listClass': 'swiper', '$list.data': indexSwiperData('block')}) //--color是css的变量,这里icon的content需要像以下的写法才能正常显示。这是首页改版的唯一难点,试了半天 inst.update({'tap': 'onChangeStyle?cur=1', itemStyle: '--color: #2567FF; --icon: "\\e83e"'}) } else{ // 通过参数判断 重置为初始值 $indexSwiper.reset() inst.reset() } } update的时候,去修改适配的方法,通过方法去改变页面的结构 2.仿携程酒店列表的完整版本 筛选部分,我做了好几天,好几个版本。因为组件与组件的嵌套,父级组件会影响子级组件。 3.仿app store的优化 修复了appstore的列表页面,之前展开详情后滚动的位置不对。而且提供了两种数据方式,一种云数据库的请求,一种本地模拟数据 这次更新后,可能后面的精力更多的放在优化上。当然 也不排除突然看到什么东西想做的。不过优化会是我一直下去的方向 默认风格: [图片] [图片] 轮播风格: [图片] 仿携程酒店列表:(这个的重点,难点在于筛选部分) [图片] 仿app Store的功能优化: [图片] 项目正在等待审核,预计明天能审核通过。有兴趣的童鞋可以搜索SAUI或者扫以下的二维码关注我的小程序 [图片]
2020-03-24 - 在线答题小程序交互设计整理二
在线答题小程序交互设计整理二 文章概述 本文体验的小程序是:题库吧 [图片] 最近我会体验市面上不同在线答题类小程序,每个小程序仅仅截图,不做分析,每篇文章整理一个小程序,为我的小程序提供设计上的思路 在线答题小程序主要有三个细节: 答题环境 得分环境 错题记录环节 这三部分是文章的重点。 小程序截图 [图片] [图片] [图片] [图片] [图片] [图片] [图片] 小程序体验总结 今天之所以整理这个小程序,以下两点是我比较认可的: 因为从我个人角度来讲,我非常喜欢这个答题结果页,非常清晰的列出了每个题目的答题情况 答题卡这个模块也非常棒,可以很清晰的看到具体哪个题目作答了,这些细节都是在开发过程中必须去考虑的。
2019-12-18 - 小程序-SAUI-首页改版-仿筛选功能
在这,先谢谢某设计师的免费赞助设计~~~ 哈哈 先不说其它,直接上图先。 [图片] [图片] 下面是仿携程,同程做的筛选部分。实现了基本功能,还在继续优化当中… [图片] [图片] 以上,算是最近的重大更新吧。 对于改版,用的时间不多,因为我的样式是通过配置样式表去整体控制的,大概如下。我希望做到的是,通过简单的配置文件,就可能改变整个网站的风格 [图片] 在筛选的功能上,用的精力比较多。 筛选目前是用list组件来生成的,弹出层通过update来显示跟隐藏。目前弹出层做了两种形式,a 纯粹的列表组件 b tabs组件,简单的数据存储 [代码]const screenListData = [ { title: { title: '筛选', itemClass: 'item-arrow' }, dot: [{ ......... itemClass: 'ss-scrore-list-pop', aim: 'onClosePop?id=1' }], tap: 'onScrennItem?btn=true&id=1', }, { title: '价格星级' }, { title: { title: '智能排序', itemClass: 'item-arrow' }, dot: [{ '@list': { $$id: 'listx', data: [], itemClass: 'item-check', listClass: 'bg-fff ss-left' }, itemClass: 'ss-scrore-list-pop', aim: 'onClosePop?id=3' }], tap: 'onScrennItem?id=3', } ] Pager({ data: { screenList: Pager.list({ data: screenListData, listClass: 'ss-scrore-list color-333', itemClass: 'flex-1' }) }, onScrennItem: function (e, query, inst) { //点击筛选列表 头部 const $screenList = this.getElementsById('screenList') const $screenPop = this.getElementsById('screenPop') const $id = query['id'] $screenList.reset().findAndUpdate(e, function(res) { res.itemClass += ' active' res.dot[0].itemClass += ' active' return res }) if ($id == 1){ const $testtabs = this.getElementsById('test-tabs') const $tabsbtn = this.getElementsById('tabs-btn') $testtabs.update({ data: app.hooks.getItem('saveScreenData') != undefined ? adapter.tabsFunc(data.tabsData, app.hooks.getItem('saveScreenData')) : adapter.tabsFunc(data.tabsData) }) $tabsbtn.removeClass('disN') } else { const $listx = this.getElementsById('listx') $listx.update({ data: app.hooks.getItem('saveScreenListData') != undefined ? adapter.listxFunc(data.listx, app.hooks.getItem('saveScreenListData'), 'radio') :adapter.listxFunc(data.listx, '', 'radio') }) } }, }) [代码] [图片]
2019-09-05 - 单张、多张图片上传(图片转base64格式)实践经验
定义初始数据: data: { imgList: [], // 图片集合 baseImg: [], // base64图片集合 maxImg: 8, // 图片上传最高数量(根据需求设置) } 第一步:从本地相册选择图片或使用相机拍照(wx.chooseImage) // 选择图片 selectPictures: function() { const that = this; // 最多上传图片数量 if (that.data.imgList.length < that.data.maxImg) { wx.chooseImage({ count: that.data.maxImg - that.data.imgList.length, // 最多可以选择的图片张数(最大数量-当前已上传数量=当前可上传最大数量) sizeType: "compressed", success: function(res) { for (let i = 0; i < res.tempFilePaths.length; i++) { that.data.imgList.push(res.tempFilePaths[i]); } // 显示图片(同步渲染到页面) that.setData({ imgList: that.data.imgList }) } }) } else { wx.showToast({ title: "最多上传" + that.data.maxImg + "张照片!" }) } } count:最多可以选择的图片张数(默认9) sizeType:所选的图片的尺寸(original-原图,compressed-压缩图) sourceType:选择图片的来源(album-从相册选图,camera-使用相机) 第二步:将图片本地路径转为base64图片格式(wx.getFileSystemManager().readFile) // 图片转base64 conversionAddress: function() { const that = this; // 判断是否有图片 if (that.data.imgList.length !== 0) { for (let i = 0; i < that.data.imgList.length; i++) { // 转base64 wx.getFileSystemManager().readFile({ filePath: that.data.imgList[i], encoding: "base64", success: function(res) { that.data.baseImg.push('data:image/png;base64,' + res.data); //转换完毕,执行上传 if (that.data.imgList.length == that.data.baseImg.length) { that.upCont(that.data.textCont, that.data.baseImg); } } }) } } else { wx.showToast({ title: "请先选择图片!" }) } } filePath:要读取的文件的路径 (本地路径) encoding:指定读取文件的字符编码(ascii,base64,binary,hex......) 第三步:执行上传,把图片数组传输给后端即可 // 执行上传 upCont: function (baseImg) { const that = this; wx.request({ url: "上传地址", method: "POST", data: { imglist: baseImg }, success: function (res) { if (res.data.code == 200) { wx.showModal({ title: "提示", content: "提交成功,棒棒哒!" }) // 清空当前数据 that.data.imgList = []; } else { wx.showModal({ title: "提示", content: "上传失败!" }) } } }) } 删除功能:被选中图片移除当前图片数组 // 删除图片(选中图片移除) delImg: function(e) { const that = this; const index = e.currentTarget.dataset.index; // 当前点击图片索引 that.data.imgList.splice(index, 1); that.setData({ imgList: that.data.imgList }) } tips:点击提交按钮后可以增加显示loading提示框:wx.showLoading(),返回结果后隐藏loading提示框:wx.hideLoading(),此方法可以避免重复点击! 完整代码: 1.js代码(直接复制文中代码即可) 2.wxml <view class="img-list"> <view class="txt">图片 {{imgList.length}} / {{maxImg}}</view> <view class="list"> <!-- 图片展示列表 --> <view class="li" wx:for="{{imgList}}" wx:key="index"> <image class="file" src="{{item}}"></image> <!-- 删除图片 --> <image class="close" src="/images/close.png" data-index="{{index}}" bindtap="delImg"></image> </view> <!-- 添加图片 --> <view class="li" bindtap="selectPictures"> <image class="file" src="/images/upload.jpg"></image> </view> </view> </view> <view class="btn" bindtap="conversionAddress">提 交</view> 3.wxss .img-list{ width: 700rpx; margin: 0 auto;} .img-list .txt{ width: 680rpx; padding: 40rpx 0 20rpx; margin: 0 auto; color: #b2b2b2;} .img-list .list{ width: 700rpx; overflow: hidden;} .img-list .list .li{ width: 160rpx; margin: 10rpx 0 0 10rpx; height: 160rpx; border: 1rpx solid #fff; float: left; position: relative;} .img-list .list .li:last-child{ border: 1rpx solid #f7f7f7;} .img-list .list .li .file{ display: block; width: 160rpx; height: 160rpx;} .img-list .list .li .close{ position: absolute; top: 0; right: 0; width: 44rpx; height: 44rpx; background: #fff;} .btn{ background: #f60; width: 680rpx; border-radius: 10rpx; line-height: 88rpx; color: #fff; text-align: center; margin: 50rpx auto 0;} 效果图: [图片]
2020-03-12 - 在线答题小程序题库批量导入经验
本经验面向用云开发做在线答题小程序的同学 占位 对于一个在线答题小程序,题库就是ta的灵魂,经过最近一段时间的摸索,总结出一套批量题库导入的方法,仅供大家参考, 本导入方案支持单选、多选、判断、填空、简答 方案实现过程中包括对excel原子数据进行加工,包括但不限于 1、对于单选、多选、判断,会从四个选项中提取内容组装成对象数组 2、对于填空、解答,会从答案中提取内容并解析成对象数组 本次导入模板如下 [图片] 具体导入代码如下所示 该方案采用PHP代码实现,借助PHPExcel解析excel 占位 [图片] 占位 [图片] 占位 [图片] 占位 [图片] 占位 [图片] 占位 目前该代码实现生成的JSON字符串已成功经过线上验证。
2020-03-10 - 自定义客服 消息卡片
小程序如何合理合规引导用户关注公众号或者添加客服微信点击客服,出现"你可能发送的图片"或者"你可能发送的小程序" 在之前的文章中我写过,如果通过小程序合理合规引导用户关注公众号,具体见下文 https://developers.weixin.qq.com/community/develop/article/doc/000208cb3fc438638ae9fd6d451013 其中提到一种方式就是客服消息, 自定义卡片消息交互设计示例 具体示例图如下所示: 以下截图来自小程序抽奖助手 [图片] [图片] [图片] [图片] 自定义卡片消息代码片段 https://developers.weixin.qq.com/miniprogram/dev/component/button.html 该技术是自定义客服卡片,具体代码可以参考 欢迎使用代码片段,可在控制台查看代码片段的说明和文档 联系客服 https://developers.weixin.qq.com/s/NHTz9tml76c3 [图片]
2020-03-14 - 多彩天气——为你的小程序加个天气模块吧
多彩天气小程序插件上线了,帮助小程序快速接入天气预报模块。 [图片][图片]授权插件添加插件,搜索插件id:wxc5af96529fe97d4b或“多彩天气”添加 引入插件修改app.json{ "permission": { "scope.userLocation": { "desc": "根据用户当前位置获取实时天气信息" } }, "usingComponents": { "weather": "plugin://weather/weather" }, "plugins": { "weather": { "version": "1.1.0", "provider": "wxc5af96529fe97d4b" } } } } 如果只有一个页面需要用到,那么在页面的page.json中引入{ "usingComponents": { "weather": "plugin://weather/weather" } } 使用代码Demo# 只渲染一个小方块 # 占整行渲染 样式# 只渲染一个小方块 需要在wxss中加入样式 weather { display: inline-block; } 占用空间大小 sizexssm支持2种风格 themelight 白色dark 黑色背景色 bgcolor直接填写颜色值就可以,支持以下两种 #0081ffrgb(0, 129, 255);展示效果[图片]
2020-09-28 - 「分享」高性能双列瀑布流极简实现(附示例)❤️
前言 在日常开发过程中,经常会有双列瀑布流场景的需求出现,如商品列表、文章列表等,本文将简单介绍这种情景下如何高效、精准的实现双列瀑布流场景,支持刷新、加载更多等,实现效果如下。 [图片] [图片] 开发思路 瀑布流视图有一种参差的美感,常规列表布局如 flex wrap 等由于存在行高度限制,无法让第二行的 item 对齐上一行最矮处,因此,瀑布流布局时采用双列 scrollview 的 flex 布局。 参差布局的实现,采用代码计算左右两列的高度,然后对左右两列总高度进行比较,新加入的 item 总是排在总高度较小的那列后面。 计算时可以尽可能的缓存高度,例如左右两列高度在每次计算时都缓存起来,有新的 item 加入列表时直接增加左右两列高度即可,不需要重新从头计算。 index.js [代码]const tplWidth = (750 - 24 - 8) / 2; const tplHeight = 595; // plWidth * 1.66 newPhotos.forEach(photo => { const { height, width } = photo let photoHeight = tplWidth if (height > width) { photoHeight = tplHeight photo.display = 'long' } else { photo.display = 'short' } if (leftHeight < rightHeight) { leftList.push(photo) leftHeight += photoHeight } else { rightList.push(photo) rightHeight += photoHeight } }) [代码] index.wxml [代码]<!-- list --> <view class='list'> <!-- left --> <view class='left-list'> <block wx:for="{{leftList}}" wx:key="{{item._id}}"> <cell photo="{{item}}" bindclick='onCellClicked' /> </block> </view> <!-- right --> <view class='right-list'> <block wx:for="{{rightList}}" wx:key="{{item._id}}"> <cell photo="{{item}}" bindclick='onCellClicked' /> </block> </view> </view> [代码] index.css [代码].list { display: flex; flex: 1; position: relative; flex-direction: row; justify-content: space-between; padding-left: 12rpx; padding-right: 12rpx; padding-top: 8rpx; } .left-list { display: flex; position: relative; flex-direction: column; width: 359rpx; } .right-list { display: flex; position: relative; flex-direction: column; width: 359rpx; } [代码]
2019-02-27 - 小程序评测,显示暂无数据?
小程序评测,上个月性能指标是优秀,现在已经中下旬了,性能指标和用户指标还是暂无数据,看说明是月初更新,请问何时能评测呢? [图片]
2019-10-18 - 微信小程序答题页——swiper渲染优化及swiper分页实现
前言 swiper的加载太多问题,网上资料好像没有一个特别明确的,就拿这个答题页,来讲讲我的解决方案 这里实现了如下功能和细节: 保证swiper-item的数量固定,加载大量数据时,大大优化渲染效率记录上次的位置,页面初次加载不一定非得是第一页,可以是任何页答题卡选择某一index回来以后的数据替换,并去掉swiper切换动画,提升交互体验示例动图 [图片] 截图 [图片] [图片] 问题原因 当swiper-item数量很多的时候,会出现性能问题 我实现了一个答题小程序,在一次性加载100个swipe-item的时候,低端手机页面渲染时间达到了2000多ms 也就是说在进入答题页的时候,会卡顿2秒多去加载这100个swiper-item 思考问题 那我们能不能让他先加载一部分,然后滑动以后再去改变item的数据,让swiper一直保持一定量的swiper-item? 注意到官方文档有这么两个属性可以利用,我们可以开启衔接滑动,然后再bindchange方法中去修改data [图片] 1、保证swiper-item的数量固定,加载大量数据时,优化渲染效率 假设我们请求到的数据的为list,实际渲染的数据为swiperList 我们现在给他就固定3个swiper-item,前后滑动的时候去替换数据 正向滑动的时候去替换滑动后的下一页数据,反向滑动的时候去替换滑动后的上一页数据 当我们知道了要替换的条件,我们便可以去替换数据了 但是我们应该考虑到临界值的问题,如果当前页是list第一项和最后一项该怎么办,向左向右滑是不是得禁止啊 这边是判断没数据会让它再弹回去 2、记录上次的位置,页面初次加载不一定非得是第一页,可以是任何页 有很多时候,我们是从某一项直接进来的,比如说上次答题答到了第五题,我这次进来要直接做第六题 那么我们需要去初始化这个swiperList,让它当前页、上一页、下一页都有数据 3、答题卡选择某一index回来以后的数据替换,并去掉swiper切换动画,提升交互体验 从答题卡选择index,那就不仅仅是滑动上下页了,它可以跳转到任何页,所以也采用类似初始化swiperList的方法 swiper切换动画我这边是默认250ms,但是发现有时候从答题卡点击回来,你在答题卡点击的下一项不知道会从左还是从右滑过来 体验真的很差,一开始不知道怎么禁掉动画,其实在跳转到答题卡页的时候把duration设为0就可以了 然后在答题卡页的unload方法中恢复 关键点: 在固定3个swiper-item的同时,要保证我们可以有办法来替代微信自带swiper的current属性和change方法 swiper-limited-load使用方法及说明: 将components中的swiper-limited-load复制到您的项目中在需要的页面引用此组件,并且创建自己的自定义组件item-view在初始化数据时,为你的list的每一项指定index属性具体可以参照项目目录start-swiper-limited-load中的用法说明:其它属性和swiper无异,你们可以自己单独添加你们需要的属性总结 一开始很头疼,为什么微信小程序提供的这个swiper,没去考虑这方面 然后在网上和社区找也没有一个特别好的解决方案。 后来想想,遇到需求就静下来解决吧。 项目地址:https://github.com/pengboboer/swiper-limited-load 如果错误,欢迎指出。 如有新的需求也可以提出来,如果有时间的话,我会帮你们完善。 如果能帮到你们,记得给一个star,谢谢。 ---补充 有很多朋友在评论区提到了分页的需求,抽时间写了一个分页的Demo和大家分享一下。 还是以答题为例,比如我们一共有500条数据,一页20条,可能需要如下功能,乍一看不就加了个分页,挺简单的,其实实现起来挺麻烦的,下面说一下思路和一些需要特别注意的点: 1、从其他页面跳转到答题页时,不光只能默认在第一题,可以是任意一题,比如第80题。 跳转到任意一题,那么需要我们根据index算出该数据在第几页,然后需要请求该页数据,最后显示对应的index。我的思路更注重用户体验,不可能是上滑或者下滑才开始去请求数据,一定是要用户滑动前提前请求好数据。所以起码要保证左右两侧在初始化那一刻都有数据。如果此题和它的上一题下一题都在同一页,那么我们只需要请求一页数据(第15题,那么只需请求第1页数据)。如果此题和它的上一题或者下一题不在同一页,那么我们可能需要请求两页数据。(第20题,那么需要请求第1页和第2页数据) 2、左滑、右滑没数据时,都可以加载新数据。直到滑到第一题或者最后一题。 如果我们初始化时是第24题,那么我们左滑到第21题时,就应该去请求第一页的数据。那么用户在看完21题时,再滑到20题,可能就根本不会感知到通过网络请求了数据。但是如果用户此刻滑动特别快:滑到21题时请求了网络,请求还没成功,就又向左滑了。那么我们需要限制用户的滑动,给用户一个提示:数据正在加载中。 3、从答题卡点击任意一题可以跳转到相应的题目,并且左右滑动显示正常数据 比如我们初始化是跳转到了第80题,不一会点击答题卡又要跳转到200题,一会又跳转到150题。各种无序操作,你也不知道用户要往哪里点。 一开始是想着维护一个主list,点到哪道题往list中添加这道题所在的当页的数据,但是还得判断这一页或者左滑右滑请求新一页的数据得往list的哪个位置添加。这来回来去乱七八糟的判断就很麻烦了,很容易出bug。而且list长度太长了以后insert的性能也不好。 后来就去想,要不答题卡点击任意一题都清空旧的list,然后请求新的数据,左右滑动没数据了再请求新的数据呗。但是这样很浪费资源,并且用户体验也不好,用户已经从第1题答到第200题了,这时用户从答题卡选择了一个25题,还得重新请求网络。而且200道题的数据都没了,那再选个26题,再重新请求网络?网络有延时不说,还浪费资源。 最后转念一想,这时候就需要弄一个缓存了。所以最终的解决方法就出来了:我们维护一个map,在网络请求成功后,在map中保存对应页的数据,同时我们维护一个主list来显示对应的题目。当我们在答题卡选择某一题目,就清空list,然后判断map中有没有该页的数据,如果有就直接拿来,没有就再去网络请求。这个处理方式,写法相对来说简单,不需要乱七八糟的判断,也不浪费资源,用户体验也很不错。 总结 以上就是一些思路和要注意的地方。这个Demo断断续续花了好几天时间写出来的。可能我说的比较啰嗦比较细,只是想让需要用到这个分页Demo的同学能理解我是如何实现的。 如果觉得能帮到你,记得给一个star,谢谢。同时如果这个demo有bug或者你们有新想法,欢迎提出来。
2021-01-07 - 可不可以多个exports放在一个云函数里?
每次新建云函数时都是以exports.main开头,但是看到官方文档有介绍插入数据库云函数是exports.add开头,是不是可以把多个exports放在同一个云函数里呢? 比如云函数名为database.js 里面可以有exports.add, exports.get 等等?可以的话应该怎么做? 谢谢
2020-01-15 - xquery实现的小程序步进器
[图片] 这样一个简单的组件,翻了ant.design才知道这种组件叫做步进器(steper),步进器常用于购物车等需要增减数量的场景,最近的旅游项目中用于增减房间数和人数,从产品的角度来理解步进器很简单,但在开发角度来说需要适应多种场景及控制一些状态 边界值(最大值,最小值),初始值,步进值(一次增减数量)需要可控 边界状态,即超出后显示为什么状态 可供外部设置边界状态的api方法 内部加减方法 可供外部调用的加减方法(一些场景中,外部有一个总量约束,比如sku场景) 加减回调方法,比如当数量超出时提示用户相关信息 多实例模式,实例之间即隔离又能交互 大致需求如上,demo及实现部分如下 GITHUB源码 小程序代码片段 wxml [代码]<view class="container"> <ui-item item="{{steperConfig}}" /> </view> [代码] Page 因为是直接使用Item组件实现,所以组件写在Page页面中,当然独立成组件看需求了 [代码]const Pager = require('../components/aotoo/core/index') let lib = Pager.lib function mkSteper(id=lib.suid('step_'), min, max, step=1) { return { $$id: id, itemClass: 'steper-class', title: [ {title: '-', aim: 'reduce', itemClass: 'steper-reduce'}, {title: '0', aim: 'custom', itemClass: 'steper-counter'}, {title: '+', aim: 'plus', itemClass: 'steper-plus'}, ], methods: { __ready(){ this.count = 0 this.min = min||0 this.max = max||10 this.step = step||1 this.stat = { reduce: true, plus: true, count: true } }, reduce(e, param, inst){ let step = this.step if (!inst) { inst = this.children[0] } if (e === false) { this.stat.reduce = false inst.addClass('disable') } if (e === true) { this.stat.reduce = true inst.removeClass('disable') } if (typeof e === 'number') { step = e } this.count -= step if (this.count <= this.min) { this.count = this.min this.stat.reduce = false inst.addClass('disable') } if (this.count < this.max && !this.stat.plus) { this.stat.plus = true let $plus = inst.siblings('steper-plus') $plus.removeClass('disable') } this.changeNum(inst) this.hooks.emit('reduce', {count: this.count}, this) }, plus(e, param, inst){ let step = this.step if (!inst) { inst = this.children[2] } if (e === false) { this.stat.plus = false inst.addClass('disable') } if (e === true) { this.stat.plus = true inst.removeClass('disable') } if (typeof e === 'number') { step = e } this.count += step if (this.count >= this.max) { this.count = this.max this.stat.plus = false inst.addClass('disable') } if (this.count > this.min && !this.stat.reduce) { this.stat.reduce = true let $reduce = inst.siblings('steper-reduce') $reduce.removeClass('disable') } this.changeNum(inst) this.hooks.emit('plus', {count: this.count}, this) }, changeNum(inst){ let count = this.count if (typeof inst === 'number') { count = inst inst = undefined } if (!inst) { inst = this.children[1] } let $counter = inst.siblings('steper-counter') $counter.update({ title: count }) } } } } Pager({ data: { steperConfig: mkSteper('steper'), }, onReady(){ let $steper = this.getElementsById('steper') $steper.hooks.on('plus', function(param) { if (this.count === 10) { Pager.alert('不能再多了,仓库没货了') } }) $steper.hooks.on('reduce', function(param) { if (param.count <= 0) { Pager.alert('大哥,买点啊') } }) } }) [代码] over了
2019-12-28 - 小程序tabside两栏选项卡
[图片] tabside两栏选项卡,一般作为分类使用,常见于小米/华为官方产品分类,左边一栏为产品分类菜单,右边为分类产品列表 菜单栏 菜单栏一般是一个单一的列表,需要考虑多余一屏列表的顺滑滚动,使用scroll-view容器作为列表展示容器 内容页 内容页有多种展示方式,如同页模式,整个内容页为一个整体页面,swiper模式,分屏显示不同菜单栏目的内容,demo中使用swiper的方式 效果 与tabpro选项卡类似,实际上本demo正式基于tabpro组件改造而成 菜单tap事件切换swiper内容 内容页swiper切换激活某一分类菜单 放出钩子方法供业务调用 源码 github搜索 aotoo-xquery -> pages -> tabside 微信搜索小程序: xquery 小程序代码片段 [图片]
2020-01-01 - 这个tab pro,是高级一点的小程序tab选项卡
[图片] 选项卡页面 类似于腾讯新闻的标签选项卡 点击菜单栏的选项开,切换内容页 滑动内容页,切换菜单栏的分类项 保持被激活的分类选项卡始终在页面显示区内 思路 需要一个scroll-view容器作为菜单容器,并支持横向滚动 需要一个swiper-view容器作为内容存放页 swiper-view切换时,切换菜单分类,可以使用scroll-view的[代码]scroll-into-view[代码]属性 点击菜单分类时,切换swiper-view显示对应内容,使用swiper-view的[代码]current[代码]属性 实现效果 [图片] 完整源码 github搜索 aotoo-xquery -> pages -> tabpro 微信搜索小程序: xquery 小程序代码片段 wxml [代码]<view class="container"> <ui-list list="{{tabConfig}}" class="tab-boxer" /> </view> [代码] 使用xquery内置的list组件可以生成菜单结构 构建菜单选项卡 [代码]listConfig = { $$id: id, listClass: 'tab-header', itemClass: 'tab-item', type: { "is": 'scroll', "scroll-x": true, }, data: [ {title: '选项-1', id: 'sc-1', tap: 'onTap?id=1'}, {title: '选项-2', id: 'sc-2', tap: 'onTap?id=2'}, {title: '选项-3', id: 'sc-3', tap: 'onTap?id=3'}, {title: '选项-4', id: 'sc-4', tap: 'onTap?id=4'}, {title: '选项-5', id: 'sc-5', tap: 'onTap?id=5'}, ... ] } [代码] $$id list组件的实例id,通过此id可以调用list组件的实例及使用定义的方法 type 指定list以什么模式展现列表 [代码]type: {is: 'scroll'}[代码] 表示整个列表以scroll-view的模式展现列表 [代码]type: {is: 'swiper'}[代码] 以swiper-view的方式展示列表 不设置type,以普通列表展示 data 菜单项集合 tap 会自动生成view的[代码]bind:tap[代码]事件,xquery的封装支持query参数 id 作为菜单页唯一标识,[代码]scroll-into-view[代码]需要此标识 swiper容器 list有一个footer底部容器,有数据时由item组件生成,基于组件嵌套的原则,在该容器中嵌入使用swiper组件 [代码]listConfig.footer = { "@list": { type: { is: 'swiper', bindchange: 'onSwiperChange', }, data: [ {title: '内容-1', id: 'sw-1'}, {title: '内容-2', id: 'sw-2'}, {title: '内容-3', id: 'sw-3'}, {title: '内容-4', id: 'sw-4'}, ... ] } } } [代码] type 指定list以什么模式展现列表 [代码]type: {is: 'swiper'}[代码] 以swiper-view的方式展示列表 data 菜单项集合 id 作为内容页唯一标识,该id与scroll-view的id需要要映射关系 到此我们的容器部分准备好了,接下来要完成对应的事件方法 组件内方法 [代码]listConfig.methods = methods: { __ready(){ // 初始化,组件挂载后自动执行 this.swiperInst = this.find('.swiper-content').data[0] //swiper实例 this.scrollInst = this // 菜单实例 // 设置默认值 this.scrollInst.find({id: 'sc-1'}).addClass('active') this.activePage.hooks.emit('set-content', {id: 'sw-1'}, this.swiperInst.find({id: 'sw-1'})) this.hasChangeContent = {'sw-1': true} }, // 菜单响应方法 onTap(e, param, inst) { let that = this let id = param.id inst.siblings().removeClass('active') inst.addClass('active') this.swiperInst.update({"type.current": id-1}, function(){ let hc = that.hasChangeContent let swId = 'sw-'+id if (!hc[swId]) { // 执行外部的钩子 that.hooks.emit('tap-to', {id: swId}, that.swiperInst) that.hasChangeContent[swId] = true } }) }, // swiper切换响应方法 onSwiperChange(e){ let that = this this.scrollInst.forEach(item => item.removeClass('active')) let id = e.detail.currentItemId.replace('sw-', '') id = parseInt(id) let scrollId = 'sc-'+id let activeIt = this.scrollInst.find({id: scrollId}) activeIt.addClass('active') let intoId = id === 1 ? id : id-3 this.update({ "type.scroll-into-view": 'sc-'+intoId }, function() { let hc = that.hasChangeContent let swId = 'sw-' + id if (!hc[swId]) { // 执行外部挂载的钩子方法 that.hooks.emit('switch-to', {id: swId}, that.swiperInst) } }) } [代码] [代码]__ready[代码] list组件挂载到页面后自动执行该方法,设置默认值 [代码]onTap[代码] 响应菜单项的点击事件 [代码]onSwiperChange[代码] swiper-view切换时响应方法 [代码]this.update({ "type.scroll-into-view": 'sc-'+intoId }[代码] 菜单项滚动到指定项 页面响应方法 组件与业务是隔离的,业务中需要调用钩子去修改相关内容 [代码]Pager({ onLoad(){ // 在外部中挂载修改内容的方法 this.hooks.once('set-content', function(param){ if (param.id === 'sw-1') { this.update({ title: '我是选项一', img: {src: '/images/mk1.jpeg', mode: 'aspectFit'} }) } ... }) }, onReady(){ let that = this let $tab = this.getElementsById('tabselect') // 组件外挂载钩子方法,对应修改swiper-item中的内容 // 菜单点击响应 $tab.hooks.on('tap-to', function(param) { let target = this.find({id: param.id}) that.hooks.emit('set-content', param, target) }) // swiper-view切换响应方法 $tab.hooks.on('switch-to', function (param) { let target = this.find({id: param.id}) that.hooks.emit('set-content', param, target) }) } }) [代码]
2019-12-31 - CanvasContext.drawImage真机不显示图片?
开发工具上使用CanvasContext.drawImage 括号9个参数那种方法:[图片] 在开发工具上可以正常裁剪图片 真机上打开调试模式也可以正常裁剪 但是关掉调试就不行了 图片变形 对比图: [图片]
2019-08-14 - 【开箱即用】分享几个好看的波浪动画css效果!
以下代码不一定都是本人原创,很多都是借鉴参考的(模仿是第一生产力嘛),有些已忘记出处了。以下分享给大家,供学习参考!欢迎收藏补充,说不定哪天你就用上了! 一、第一种效果 [图片] [代码]//index.wxml <view class="zr"> <view class='user_box'> <view class='userInfo'> <open-data type="userAvatarUrl"></open-data> </view> <view class='userInfo_name'> <open-data type="userNickName"></open-data> , 欢迎您 </view> </view> <view class="water"> <view class="water-c"> <view class="water-1"> </view> <view class="water-2"> </view> </view> </view> </view> //index.wxss .zr { color: white; background: #4cb4e7; /*#0396FF*/ width: 100%; height: 100px; position: relative; } .water { position: absolute; left: 0; bottom: -10px; height: 30px; width: 100%; z-index: 1; } .water-c { position: relative; } .water-1 { background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjYwMHB4IiBoZWlnaHQ9IjYwcHgiIHZpZXdCb3g9IjAgMCA2MDAgNjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCAzLjQgKDE1NTc1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT53YXRlci0xPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IuaIkSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9Ii0iIHNrZXRjaDp0eXBlPSJNU0FydGJvYXJkR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMjEuMDAwMDAwLCAtMTMzLjAwMDAwMCkiIGZpbGwtb3BhY2l0eT0iMC4zIiBmaWxsPSIjRkZGRkZGIj4KICAgICAgICAgICAgPGcgaWQ9IndhdGVyLTEiIHNrZXRjaDp0eXBlPSJNU0xheWVyR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyMS4wMDAwMDAsIDEzMy4wMDAwMDApIj4KICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0wLDcuNjk4NTczOTUgTDQuNjcwNzE5NjJlLTE1LDYwIEw2MDAsNjAgTDYwMCw3LjM1MjMwNDYxIEM2MDAsNy4zNTIzMDQ2MSA0MzIuNzIxMDUyLDI0LjEwNjUxMzggMjkwLjQ4NDA0LDcuMzU2NzQxODcgQzE0OC4yNDcwMjcsLTkuMzkzMDMwMDggMCw3LjY5ODU3Mzk1IDAsNy42OTg1NzM5NSBaIiBpZD0iUGF0aC0xIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==") repeat-x; background-size: 600px; -webkit-animation: wave-animation-1 3.5s infinite linear; animation: wave-animation-1 3.5s infinite linear; } .water-2 { top: 5px; background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjYwMHB4IiBoZWlnaHQ9IjYwcHgiIHZpZXdCb3g9IjAgMCA2MDAgNjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCAzLjQgKDE1NTc1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT53YXRlci0yPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IuaIkSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9Ii0iIHNrZXRjaDp0eXBlPSJNU0FydGJvYXJkR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMjEuMDAwMDAwLCAtMjQ2LjAwMDAwMCkiIGZpbGw9IiNGRkZGRkYiPgogICAgICAgICAgICA8ZyBpZD0id2F0ZXItMiIgc2tldGNoOnR5cGU9Ik1TTGF5ZXJHcm91cCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTIxLjAwMDAwMCwgMjQ2LjAwMDAwMCkiPgogICAgICAgICAgICAgICAgPHBhdGggZD0iTTAsNy42OTg1NzM5NSBMNC42NzA3MTk2MmUtMTUsNjAgTDYwMCw2MCBMNjAwLDcuMzUyMzA0NjEgQzYwMCw3LjM1MjMwNDYxIDQzMi43MjEwNTIsMjQuMTA2NTEzOCAyOTAuNDg0MDQsNy4zNTY3NDE4NyBDMTQ4LjI0NzAyNywtOS4zOTMwMzAwOCAwLDcuNjk4NTczOTUgMCw3LjY5ODU3Mzk1IFoiIGlkPSJQYXRoLTIiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDMwMC4wMDAwMDAsIDMwLjAwMDAwMCkgc2NhbGUoLTEsIDEpIHRyYW5zbGF0ZSgtMzAwLjAwMDAwMCwgLTMwLjAwMDAwMCkgIj48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==") repeat-x; background-size: 600px; -webkit-animation: wave-animation-2 6s infinite linear; animation: wave-animation-2 6s infinite linear; } .water-1, .water-2 { position: absolute; width: 100%; height: 60px; } .back-white { background: #fff; } @keyframes wave-animation-1 { 0% { background-position: 0 top; } 100% { background-position: 600px top; } } @keyframes wave-animation-2 { 0% { background-position: 0 top; } 100% { background-position: 600px top; } } .user_box { display: flex; z-index: 10000 !important; opacity: 0; /* 透明度*/ animation: love 1.5s ease-in-out; animation-fill-mode: forwards; } .userInfo_name { flex: 1; vertical-align: middle; width: 100%; margin-left: 5%; margin-top: 5%; font-size: 42rpx; } .userInfo { flex: 1; width: 100%; border-radius: 50%; overflow: hidden; max-height: 50px; max-width: 50px; margin-left: 5%; margin-top: 5%; border: 2px solid #fff; } [代码] 二、第二种效果 [图片] [代码]//index.wxml <view class="waveWrapper waveAnimation"> <view class="waveWrapperInner bgTop"> <view class="wave waveTop" style="background-image: url('https://s2.ax1x.com/2019/09/26/um8g7n.png')"></view> </view> <view class="waveWrapperInner bgMiddle"> <view class="wave waveMiddle" style="background-image: url('https://s2.ax1x.com/2019/09/26/umGZ38.png')"></view> </view> <view class="waveWrapperInner bgBottom"> <view class="wave waveBottom" style="background-image: url('https://s2.ax1x.com/2019/09/26/umGuuQ.png')"></view> </view> </view> //index.wxss .waveWrapper { overflow: hidden; position: absolute; left: 0; right: 0; height: 300px; top: 0; margin: auto; } .waveWrapperInner { position: absolute; width: 100%; overflow: hidden; height: 100%; bottom: -1px; background-image: linear-gradient(to top, #86377b 20%, #27273c 80%); } .bgTop { z-index: 15; opacity: 0.5; } .bgMiddle { z-index: 10; opacity: 0.75; } .bgBottom { z-index: 5; } .wave { position: absolute; left: 0; width: 500%; height: 100%; background-repeat: repeat no-repeat; background-position: 0 bottom; transform-origin: center bottom; } .waveTop { background-size: 50% 100px; } .waveAnimation .waveTop { animation: move-wave 3s; -webkit-animation: move-wave 3s; -webkit-animation-delay: 1s; animation-delay: 1s; } .waveMiddle { background-size: 50% 120px; } .waveAnimation .waveMiddle { animation: move_wave 10s linear infinite; } .waveBottom { background-size: 50% 100px; } .waveAnimation .waveBottom { animation: move_wave 15s linear infinite; } @keyframes move_wave { 0% { transform: translateX(0) translateZ(0) scaleY(1) } 50% { transform: translateX(-25%) translateZ(0) scaleY(0.55) } 100% { transform: translateX(-50%) translateZ(0) scaleY(1) } } [代码] 三、第三种效果 [图片] [代码]//index.wxml <view class="container"> <image class="title" src="https://ftp.bmp.ovh/imgs/2019/09/74bada9c4143786a.png"></image> <view class="content"> <view class="hd" style="transform:rotateZ({{angle}}deg);"> <image class="logo" src="https://ftp.bmp.ovh/imgs/2019/09/d31b8fcf19ee48dc.png"></image> <image class="wave" src="wave.png" mode="aspectFill"></image> <image class="wave wave-bg" src="wave.png" mode="aspectFill"></image> </view> <view class="bd" style="height: 100rpx;"> </view> </view> </view> //index.wxss image{ max-width:none; } .container { background: #7acfa6; align-items: stretch; padding: 0; height: 100%; overflow: hidden; } .content{ flex: 1; display: flex; position: relative; z-index: 10; flex-direction: column; align-items: stretch; justify-content: center; width: 100%; height: 100%; padding-bottom: 450rpx; background: -webkit-gradient(linear, left top, left bottom, from(rgba(244,244,244,0)), color-stop(0.1, #f4f4f4), to(#f4f4f4)); opacity: 0; transform: translate3d(0,100%,0); animation: rise 3s cubic-bezier(0.19, 1, 0.22, 1) .25s forwards; } @keyframes rise{ 0% {opacity: 0;transform: translate3d(0,100%,0);} 50% {opacity: 1;} 100% {opacity: 1;transform: translate3d(0,450rpx,0);} } .title{ position: absolute; top: 30rpx; left: 50%; width: 600rpx; height: 200rpx; margin-left: -300rpx; opacity: 0; animation: show 2.5s cubic-bezier(0.19, 1, 0.22, 1) .5s forwards; } @keyframes show{ 0% {opacity: 0;} 100% {opacity: .95;} } .hd { position: absolute; top: 0; left: 50%; width: 1000rpx; margin-left: -500rpx; height: 200rpx; transition: all .35s ease; } .logo { position: absolute; z-index: 2; left: 50%; bottom: 200rpx; width: 160rpx; height: 160rpx; margin-left: -80rpx; border-radius: 160rpx; animation: sway 10s ease-in-out infinite; opacity: .95; } @keyframes sway{ 0% {transform: translate3d(0,20rpx,0) rotate(-15deg); } 17% {transform: translate3d(0,0rpx,0) rotate(25deg); } 34% {transform: translate3d(0,-20rpx,0) rotate(-20deg); } 50% {transform: translate3d(0,-10rpx,0) rotate(15deg); } 67% {transform: translate3d(0,10rpx,0) rotate(-25deg); } 84% {transform: translate3d(0,15rpx,0) rotate(15deg); } 100% {transform: translate3d(0,20rpx,0) rotate(-15deg); } } .wave { position: absolute; z-index: 3; right: 0; bottom: 0; opacity: 0.725; height: 260rpx; width: 2250rpx; animation: wave 10s linear infinite; } .wave-bg { z-index: 1; animation: wave-bg 10.25s linear infinite; } @keyframes wave{ from {transform: translate3d(125rpx,0,0);} to {transform: translate3d(1125rpx,0,0);} } @keyframes wave-bg{ from {transform: translate3d(375rpx,0,0);} to {transform: translate3d(1375rpx,0,0);} } .bd { position: relative; flex: 1; display: flex; flex-direction: column; align-items: stretch; animation: bd-rise 2s cubic-bezier(0.23,1,0.32,1) .75s forwards; opacity: 0; } @keyframes bd-rise{ from {opacity: 0; transform: translate3d(0,60rpx,0); } to {opacity: 1; transform: translate3d(0,0,0); } } [代码] wave.png(可下载到本地) [图片] 在这个基础上,再加上js的代码,即可实现根据手机倾向,水波晃动的效果 wx.onAccelerometerChange(function callback) 监听加速度数据事件。 [图片] [代码]//index.js Page({ onReady: function () { var _this = this; wx.onAccelerometerChange(function (res) { var angle = -(res.x * 30).toFixed(1); if (angle > 14) { angle = 14; } else if (angle < -14) { angle = -14; } if (_this.data.angle !== angle) { _this.setData({ angle: angle }); } }); }, }); [代码] 四、第四种效果 [图片] [代码]//index.wxml <view class='page__bd'> <view class="bg-img padding-tb-xl" style="background-image:url('http://wx4.sinaimg.cn/mw690/006UdlVNgy1g2v2t1ih8jj31hc0p0qej.jpg');background-size:cover;"> <view class="cu-bar"> <view class="content text-bold text-white"> 悦拍屋 </view> </view> </view> <view class="shadow-blur"> <image src="https://raw.githubusercontent.com/weilanwl/ColorUI/master/demo/images/wave.gif" mode="scaleToFill" class="gif-black response" style="height:100rpx;margin-top:-100rpx;"></image> </view> </view> //index.wxss @import "colorui.wxss"; .gif-black { display: block; border: none; mix-blend-mode: screen; } [代码] 本效果需要引入ColorUI组件库
2019-09-26 - css3动画keyframes,苹果端的animation-delay属性失效
css3动画keyframes能够支持,但是苹果端的animation-delay属性失效,大家有遇到类似问题吗,小程序对css3动画支持度好吗?
2017-05-05 - 微信小程序@keyframes myfirst 在手机上播放动画失效
微信小程序@keyframes myfirst 在手机上播放动画失效
2019-05-05