带你逐步在小程序中实现保存图片组件功能

why 718 2024-06-25

本篇文章带大家聊聊微信小程序保存图片组件开发,希望对大家有所帮助!

手把手带你在小程序中实现保存图片组件功能

许多微信小程序通过保存海报让用户去分享活动让更多的人知道自己的小程序,想必在平时开发小程序的时候应该有遇见过吧。

今天我就来分享下之前在公司做的一个小程序保存海报的功能。首先我先描述下之前在公司做的需求是什么样的。公司上线的小程序会有一个长期的活动目的就是去推广新用户,每个用户都要有一张属于自己的海报,通过个人海报去推广则只是单纯的一种方式。

接到任务后,我也先去万能互联网做了调查但是我的师兄和我说这个做过类似的但是当时只是单纯为了完成任务所以代码很乱,然后他就从其他项目的代码找呀找,然后找到了给我~~~ 而当时给到我的时间紧任务重呀只好先用着调整一些并且交差了。之后呢我就根据网上的文章然后一步一步踩坑,一步一步走实现了一个保存海报的组件。

思路

首先声明下组件采用的是uniapp,具体实现了可以绘制图片、绘制文字以及保存海报至相册的基本功能,在开发中这些也完全够用了。

通过canvas绘制海报。通过uni.canvasToTempFilePath 将绘制好的 canvas转为图片。通过uni.saveImageToPhotosAlbum 将本地临时路径的图片保存至手机相册中。而我的想法是将所有采用的方法全部封装到组件中,只通过父组件去调用需要使用的方法和调整相关的参数即可。 具体使用可以查看示例代码

通过canvas绘制海报内容的顺序先后问题

通过使用promise对象决定绘制海报内容的顺序先后。promise.all()方法进行canvas最后一步的绘画操作 context.draw()

注意uni.getImageInfo()

  • 在绘制图片 和 头像时,组件通过uni.getImageInfo() 去获取图片的相关信息,调用该方法成功的前提是需要在微信小程序后台配置download域名和request域名当然最好把uploadFile域名也一起配置,防止出差错。但是官方给出的提示是配置download域名白名单即可,但是获取不到图片信息,这算是一个大坑了。

  • 如果没有进行相关配置,在调试时 或者 体验版 正式版等 打开了vconsole调试工具。uni.getImageInfo() 是可以获取到图片信息的,一旦关闭了vconsole uni.getImageInfo() 将会fail, 也是个坑。

本组件方法,变量介绍

props

  • canvasInfo Object (必需)

    • canvasWidth 画布宽度

    • canvasHeight 画布高度

    • canvasId 画布标识

  • isFullScreen Boolean

    • 为ture时表示画布为手机屏幕全屏,canvasInfo设置的宽高将失效。

    • 默认为 false

methods

  • canvasInit(callback) canvas初始化,所有有关画布canvas操作需在其回调函数操作。

  • drawCanvasImage(context, src, _imageWidth, _imageHeight, dx, dy) 在canvas绘制一张图片

  • drawCircularAvatar(context, url, _circularX, _circularY, _circularR) 在canvas绘制一张圆形图片

  • drawText(options) 在canvas绘制单行、多行文本

  • startDrawToImage(context, promiseArr, callback) 将canvas操作draw()进行绘制

  • posterToPhotosAlbum(filePath) 保存至手机相册

示例代码

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

<template>

    <view>

        <view class="savePosterItem">

            <image v-show="tempFilePath" :src="tempFilePath"></image>

            <save-poster-com v-show="!tempFilePath" ref="savePoster" :canvasInfo="canvasInfo"></save-poster-com>

        </view>

         

         

        <button class="savePosterBtn" type="primary" @click="saveBtnFun">保存海报</button>

    </view>

</template>

 

<script>

    import SavePosterCom from &#39;@/components/SavePosterCom/SavePosterCom.vue&#39;

    export default {

        components: {

            SavePosterCom

        },

        data() {

            return {

                canvasInfo: {

                    canvasWidth: 620,

                    canvasHeight: 950,

                    canvasId: &#39;save-poster&#39;

                },

                tempFilePath: &#39;&#39;,

                canvasBgUrl: &#39;https://images.pexels.com/photos/4065617/pexels-photo-4065617.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500&#39;,

                avatarUrl: &#39;https://p9-passport.byteacctimg.com/img/user-avatar/4dbf31fa6dec9c65b78a70d28d843c04~300x300.image&#39;

            }

        },

        onLoad() {

            let {

                drawCanvasImage,

                drawCircularAvatar,

                drawText

            } = this.$refs.savePoster.$options.methods

            this.$refs.savePoster.canvasInit(({

                context,

                comThis

            }) => {

                // 获取画布宽高

                let canvasWH = comThis.canvasWH

                // 绘制海报背景图

                let promise_1 = drawCanvasImage(context, this.canvasBgUrl, canvasWH.canvasWidth, canvasWH.canvasHeight)

                // 必须先绘制玩海报背景图 再去操作其他绘制内容

                promise_1.then(res => {

                    let promise_2 = drawCircularAvatar(context, this.avatarUrl, canvasWH.canvasWidth / 2, canvasWH.canvasHeight /

                        7, 70)

                     

                    let promise_3 = drawText({

                        context: context,

                        text: &#39;皮皮虾仁&#39;,

                        dx: (canvasWH.canvasWidth / 2) + 60,

                        dy: canvasWH.canvasHeight / 4,

                        fontSize: 30,

                        fontColor: &#39;#5D4037&#39;

                    })

                     

                    let promise_4 = drawCanvasImage(context, this.avatarUrl, 150, 150, (canvasWH.canvasWidth / 2) + 85, (canvasWH.canvasHeight -

                        165))

                      

                    this.$refs.savePoster.startDrawToImage(context, [promise_1,promise_2,promise_4], (tempFilePath) => {

                        this.tempFilePath = tempFilePath

                    })

                })

            })

        },

        methods: {

            saveBtnFun() {

                uni.showModal({

                    title: &#39;保存海报&#39;,

                    content: &#39;海报将被保存至相册中&#39;,

                    confirmText: &#39;保存&#39;,

                    success: (res) => {

                        if(res.confirm) {

                            this.$refs.savePoster.posterToPhotosAlbum(this.tempFilePath)

                        }

                    }

                })

            }

        }

    }

</script>

 

<style>

    .savePosterItem {

        text-align: center;

    }

    .savePosterItem > image {

        width: 620rpx;

        height: 950rpx;

    }

     

    .savePosterBtn {

        margin-top: 40rpx;

        width: 80%;

    }

</style>

组件源码

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

<template>

    <view>

        <canvas :canvas-id="canvasInfo.canvasId" :style="{width: canvasWH.canvasWidth + &#39;px&#39;, height: canvasWH.canvasHeight + &#39;px&#39;}"></canvas>

    </view>

</template>

 

<script>

    export default {

        name: &#39;savePosterCom&#39;,

        data() {

            return {

                userPhoneWHInfo: {},

                canvasWH: {

                    canvasWidth: 0,

                    canvasHeight: 0

                }

            }

        },

        props: {

            // 决定保存下来的图片的宽高

            canvasInfo: {

                type: Object,

                default: () => {

                    return {

                        canvasWidth: 0,

                        canvasHeight: 0,

                        canvasId: &#39;canvasId&#39;

                    }

                }

            },

            // canvas画布是不是全屏,默认是false。 false时使用必须传 canvasInfo

            isFullScreen: Boolean

        },

        created() {

            this.userPhoneWHInfo = this.getPhoneSystemInfo()

            if (this.isFullScreen) { // 画布全屏

                this.canvasWH.canvasWidth = this.userPhoneWHInfo.windowWidth

                this.canvasWH.canvasHeight = this.userPhoneWHInfo.windowHeight

            } else { // 指定宽高

                this.canvasWH.canvasWidth = this.canvasInfo.canvasWidth

                this.canvasWH.canvasHeight = this.canvasInfo.canvasHeight

            }

        },

        mounted() {},

        methods: {

            /**

            * 获取用户手机屏幕信息

            */

            getPhoneSystemInfo() {

                const res = uni.getSystemInfoSync();

                return {

                    windowWidth: res.windowWidth,

                    windowHeight: res.windowHeight

                }

            },

            /** 获取 CanvasContext实例

            * @param {String} canvasId

            */

            getCanvasContextInit(canvasId) {

                return uni.createCanvasContext(canvasId, this)

            },

            /** 保存海报组件初始化

            * @param {Function} callback(context) 回调函数

            */

            canvasInit(callback) {

                let context = this.getCanvasContextInit(this.canvasInfo.canvasId)

                if (context) {

                    callback({

                        context: context,

                        comThis: this

                    })

                }

            },

            /** 将上诉的绘制画到画布中 并且 将画布导出为图片

            *  @param context 画布

            *  @param {Promise[]} 存放Promise的数组

            *  @param {Function} callback 保存图片后执行的回调函数(本地图片临时路径)

            */

            startDrawToImage(context, promiseArr, callback) {

                // 将之前在绘图上下文中的描述(路径、变形、样式)画到 canvas 中

                let canvasId = this.canvasInfo.canvasId

                let tempFilePath = &#39;&#39;

                Promise.all(promiseArr).then(res => {

                    context.draw(false, async () => {

                        callback(await this.canvasToImage(canvasId))

                    })

                })

            },

            /**

            * 在canvas绘制一张图片

            * @param context 画布

            * @param src 图片资源

            * @param _imageWidth 图片宽度

            * @param _imageHeight 图片高度

            */

            drawCanvasImage(context, src, _imageWidth, _imageHeight, dx, dy) {

                return new Promise((resolve, reject) => {

                    uni.getImageInfo({

                        src: src,

                        success: res => {

                            context.drawImage(res.path, (dx - _imageWidth), (dy - _imageHeight), _imageWidth, _imageHeight)

                            resolve(context)

                        },

                    })

                })

            },

            /** 绘制一个圆形头像

            * @param  context 画布

            * @param  url     图片地址

            * @param  _circularX  圆心X坐标

            * @param  _circularY  圆心Y坐标

            * @param  _circularR  圆半径

            */

            drawCircularAvatar(context, url, _circularX, _circularY, _circularR) {

                let dx = _circularX - _circularR;

                let dy = _circularY - _circularR;

                let dwidth = _circularR * 2;

                let dheight = _circularR * 2

                return new Promise((resolve, reject) => {

                    uni.downloadFile({

                        url: url,

                        success: res => {

                            context.save()

                            context.beginPath()

                            // _circularX圆的x坐标  _circularY圆的y坐标  _circularR圆的半径

                            context.arc(_circularX, _circularY, _circularR, 0, 2 * Math.PI)

                            context.clip()

                            // dx: 图像的左上角在目标canvas上 X 轴的位置

                            // dy: 图像的左上角在目标canvas上 Y 轴的位置

                            // dwidth: 在目标画布上绘制图像的宽度,允许对绘制的图像进行缩放

                            // dheight: 在目标画布上绘制图像的高度,允许对绘制的图像进行缩放

                            context.drawImage(res.tempFilePath, dx, dy, dwidth, dheight)

                            context.restore()

                            // context.draw()

                            resolve(context)

                        }

                    })

                })

            },

            /** 绘制多行文本 注:, 和 空格都算一个字

            * @param context 画布

            * @param text 需要被绘制的文本

            * @param dx 左上角x坐标

            * @param dy 右上角y坐标

            * @param rowStrnum 每行多少个字 (默认为text字体个数->单行)

            * @param fontSize 文字大小 (默认16)

            * @param fontColor 文字颜色 (默认black)

            * @param lineHeight 单行文本行高 (默认0)

            */

            drawText(options) {

                let {

                    context,

                    text,

                    dx,

                    dy,

                    rowStrnum = text.length,

                    lineHeight = 0,

                    fontSize = 16,

                    fontColor = &#39;black&#39;

                } = options

                return new Promise((resolve, reject) => {

                    context.setFontSize(fontSize)

                    context.setFillStyle(fontColor)

                    context.setTextBaseline(&#39;middle&#39;)

                    // 获取需要绘制的文本宽度

                    let textWidth = Number(context.measureText(text).width)

                    // console.log(&#39;textWidth&#39;,textWidth)

                    // 获取文本的字数

                    let textNum = text.length

                    // 获取行数 向上取整

                    let lineNum = Math.ceil(textNum / rowStrnum)

                    // console.log(&#39;textNum&#39;,textNum)

                    // console.log(&#39;lineNum&#39;,lineNum)

                    for (let i = 0; i < lineNum; i++) {

                        let sliceText = text.slice(i * rowStrnum, (i + 1) * rowStrnum)

                        // fillText 的 dx = 文字最左边的距离到屏幕政策的距离

                        context.fillText(sliceText, dx - textWidth, dy + i * lineHeight);

                    }

                    resolve(context)

                })

            },

            /** 将画布导出为图片

            * @param canvasId 画布标识

            */

            canvasToImage(canvasId) {

                return new Promise((resolve, reject) => {

                    uni.canvasToTempFilePath({

                        canvasId: canvasId, // 画布标识

                        success: res => {

                            // 在H5平台下,tempFilePath 为 base64

                            resolve(res.tempFilePath)

                        },

                        fail: err => {

                            console.log(&#39;err&#39;, err)

                            reject(err)

                        }

                    }, this)

                })

            },

            /** 保存生成的图片到本地相册中

            *  @param {String} filePath 图片临时路劲

            */

            posterToPhotosAlbum(filePath) {

                console.log(&#39;filePath&#39;,filePath)

                uni.showLoading({

                    title: &#39;保存中...&#39;

                })

                uni.saveImageToPhotosAlbum({

                    filePath: filePath,

                    success: (res) => {

                        uni.showToast({

                            title: &#39;保存成功,请前往手机相册中查看&#39;,

                            mask: true,

                            icon: &#39;none&#39;,

                            duration: 2000

                        })

                    },

                    fail: (err) => {

                        console.log(&#39;err&#39;,err)

                        if (err.errMsg.includes(&#39;deny&#39;)||err.errMsg.includes(&#39;denied&#39;)) { // 用户选择拒绝

                            this.openSetting()

                        } else if (err.errMsg.includes(&#39;fail cancel&#39;)) { // 用户在保存图片时 取消了

                            uni.showToast({

                                title: &#39;已取消保存,无法保存至相册&#39;,

                                mask: true,

                                icon: &#39;none&#39;,

                                duration: 2000

                            })

                            return

                        }

                    },

                    complete: () => {

                        uni.hideLoading()

                    }

                })

            },

            /**

            * 打开摄像头设置权限页面

            */

            openSetting() {

                uni.showModal({

                    title: &#39;温馨提示&#39;,

                    content: &#39;保存图片至相册中,需要您同意添加访问相册权限&#39;,

                    cancelText: &#39;拒绝&#39;,

                    confirmText: &#39;同意&#39;,

                    success: res => {

                        if (res.confirm) {

                            uni.openSetting({

                                success: settingdata => {

                                    if (settingdata.authSetting[&#39;scope.writePhotosAlbum&#39;]) {

                                        console.log(&#39;获取权限成功,给出再次点击图片保存到相册的提示。&#39;)

                                        uni.showToast({

                                            title: &#39;授权成功,请再次点击保存&#39;,

                                            icon: &#39;none&#39;,

                                            duration: 2000,

                                        })

                                    } else {

                                        console.log(&#39;获取权限失败,给出不给权限就无法正常使用的提示&#39;)

                                        uni.showToast({

                                            title: &#39;需要访问相册权限&#39;,

                                            icon: &#39;none&#39;,

                                            duration: 2000,

                                        })

                                    }

                                },

                                fail: (res) => {

                                    console.log(&#39;err&#39;, err)

                                }

                            })

                        } else {

                            uni.showToast({

                                title: &#39;已拒绝授权,无法保存至相册&#39;,

                                mask: true,

                                icon: &#39;none&#39;,

                                duration: 2000

                            })

                            return

                        }

                    }

                })

            }

        }

    }

</script>

 

<style>

</style>

效果

jgedx-x12pb.gif



版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:浅议小程序中创建页面二维码的方法
下一篇:浅议小程序中下拉刷新与上拉加载功能的实现方式及代码示例
相关文章

 发表评论

暂时没有评论,来抢沙发吧~