3

能在安卓app上使用动态添加文字、并且文字能够拖拽、编辑、删除、缩放、设置颜色、字体大小、文字排列方式,还有可以插入图片、并且图片也能缩放、删除。

假设场景:用户在后台页面上配置自定义海报,需要将数据传到app并展示。

我使用的是fabricjs。官网:http://fabricjs.com/

实现的原理就是使用webview间接实现,我在网上找了很久也没有找到比较快的方法实现,其实有一种,那就是使用canvas去实现,但是canvas的绘制很复杂、麻烦。大佬可以试试emmm

直接上代码了emmm,做个笔记。

index.vue的代码如下:
使用的插件image-tools:https://ext.dcloud.net.cn/plugin?id=123

<template>
    <view class="promotionPosterEdit">
        <web-view :src="'../../../hybrid/html/promotionPosterEdit/index.html?data=' + obj" @message="getMessage" ref="wv"></web-view>
    </view>
</template>

<script>
    import { pathToBase64, base64ToPath } from '../../../js_sdk/mmmm-image-tools/index.js';
    export default {
        data() {
            return {
                obj: null,
                currentWebview: null,
            }
        },
        onLoad() {
            let statusBarHeight = uni.getSystemInfoSync()['statusBarHeight']
            let top = uni.getStorageSync('navbarHeight') + 30
            let height = uni.getSystemInfoSync()['screenHeight'] - 64 - uni.getStorageSync('navbarHeight') - 74
            let width = uni.getSystemInfoSync()['screenWidth'] - 100
            let screenHeight = uni.getSystemInfoSync()['screenHeight']
            let obj = {
                statusBarHeight: statusBarHeight,
                top: top,
                height: height,
                width: width,
                screenHeight: screenHeight
            }
            this.obj = JSON.stringify(obj)    //这里是传给webview的参数用来调整样式的
        },
        onReady() {
            const self = this
            self.currentWebview = self.$scope.$getAppWebview().children()[0]
        },
        methods: {
            getMessage(e) {
                console.log('接收html发送的数据', e.detail)
                if(e.detail.data[0].msg === 'openCamera') {    //打开手机相册
                    uni.chooseImage({
                        count: 1,
                        success: (res) => {
                            console.log('临时路径', res.tempFilePaths[0])
                            pathToBase64(res.tempFilePaths[0]).then(base64 => {
                                let info = base64.replace(/[\r\n]/g, '')
                                const self = this;
                                self.currentWebview.evalJS(`uniEvent(${JSON.stringify(info)})`);    //app向html发送数据
                            }).catch(error => {
                                console.log('转换失败:', error);
                            });
                        }
                    })
                } else if(e.detail.data[0].msg === 'save') {    //保存到手机相册
                    console.log('app', e.detail.data[0].canvas)
                    uni.showLoading({
                        title: '保存中...'
                    })
                    let imageStr = e.detail.data[0].canvas
                    base64ToPath(imageStr).then(path => {
                        console.log('转换下载图片', path)
                        this.saveImage(path);
                    }).catch(error => {
                        console.error('临时路径转换出错了:', error);
                    });
                } else if(e.detail.data[0].msg === 'wx') {        //转发到微信
                    uni.showToast({
                        icon: 'none',
                        title: '暂未开放'
                    })
                } else if(e.detail.data[0].msg === 'friend') {        //转发到微信朋友圈
                    uni.showToast({
                        icon: 'none',
                        title: '暂未开放'
                    })
                } else if(e.detail.data[0].msg === 'addEmptyText') {    //不能添加空文字
                    uni.showToast({
                        icon: 'none',
                        title: '请输入文字'
                    })
                }
            },
            // 保存canvas图片到手机相册
            saveImage(filePath) {
              uni.saveImageToPhotosAlbum({
                filePath, // 需要临时文件路径,base64无法保存
                success: () => {
                    uni.hideLoading()
                    uni.showToast({
                        icon: 'none',
                        title: '保存成功'
                    })
                },
                fail: (error) => {
                  console.error('保存失败,请重试', error);
                }
              });
            },
        }
    }
</script>

<style lang="scss" scoped>
    .promotionPosterEdit {
        min-height: 100vh;
        background-color: #303030;
    }
</style>

/hybrid/html/promotionPosterEdit/index.html的代码如下:
使用的插件html2canvas 1.4.1 https://html2canvas.hertzen.com
fabricjs5.3.0

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport"
            content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
        <title>网络网页</title>
        <link rel="stylesheet" href="./css/index.css" />
    </head>
    <body>
        <div class="post-message-section">
            <div class="headers">
                <div class="header">
                    <div class="left item" data-action="navigateBack">
                        <div class="back">
                            <img src="../img/back.png" alt="back" />
                        </div>
                    </div>
                    <div class="title item" style="color: #fff">
                        <div class="txt">宣传海报编辑</div>
                    </div>
                    <div class="right item">
                        <div class="share">
                            <img src="../img/share.png" alt="back" />
                        </div>
                    </div>
                </div>
            </div>

            <div class="myCanvas">
                <canvas id="canvas"></canvas>
            </div>


            <div class="shares">
                <div class="sharesmask"></div>
                <div class="content">
                    <div class="itemsurl">
                        <div class="item wx">
                            <div class="imgs">
                                <img src="../img/wechat.png" alt="wechat" srcset="" />
                            </div>
                            <div class="txt">微信</div>
                        </div>
                        <div class="item friends">
                            <div class="imgs">
                                <img src="../img/friends.png" alt="friends" srcset="" />
                            </div>
                            <div class="txt">朋友圈</div>
                        </div>
                        <!-- <div class="item">
                            <div class="imgs">
                                <img src="../../../static/download.png" alt="download" srcset="" />
                            </div>
                            <div class="txt">生成海报</div>
                        </div> -->
                    </div>
                    <div style="height: 6px;width: 100%;background-color: #ececec;"></div>
                    <div class="cancel">取消</div>
                </div>
            </div>

            
            <div class="addText">
                <div class="addTextmask"></div>
                <div class="addTextContent">
                    <div class="title">文字样式设置</div>
                    <div class="tabtool">
                        <div class="items">
                            <input class="color" type="color" value="#000000" />
                        </div>
                        <div class="item bold">
                            <img src="../img/bold.png" alt="bold" srcset="" />
                        </div>
                        <div class="item italic">
                            <img src="../img/italic.png" alt="italic" srcset="" />
                        </div>
                        <div class="item amplify">
                            <img src="../img/amplify.png" alt="amplify" srcset="" />
                        </div>
                        <div class="item reduce">
                            <img src="../img/reduce.png" alt="reduce" srcset="" />
                        </div>
                    </div>
                    <div class="text">
                        <textarea id="MainText" cols="30" row="10" style="font-size: 18px;" placeholder="请输入文字"></textarea>
                    </div>
                    <div class="confirm">
                        <button class="cancelAddText btn btn-red" type="button">取消</button>
                        <button class="confirmAddText btn" type="button">确认</button>
                    </div>
                </div>
            </div>


            <div class="footer">
                <div class="itemBox">
                    <div class="item reload">
                        <div class="icons">
                            <img src="../img/reload.png" alt="reload" />
                        </div>
                        <div class="txt">恢复默认</div>
                    </div>
                    <div class="item pic">
                        <div class="icons">
                            <img src="../img/pic.png" alt="pic" />
                        </div>
                        <div class="txt">图片/二维码</div>
                    </div>
                    <div class="item edit-pen">
                        <div class="icons">
                            <img src="../img/edit-pen.png" alt="edit-pen" />
                        </div>
                        <div class="txt">添加文字</div>
                    </div>
                    <div class="item downs">
                        <div class="icons">
                            <img src="../img/downs.png" alt="downs" />
                        </div>
                        <div class="txt">保存本地</div>
                    </div>
                </div>
            </div>
            
            <div class="deleteFooter">
                <div class="delete">
                    <div class="box">
                        <img src="../img/delete.png" alt="delete" srcset="" />
                        <div class="tip">拖到此处删除</div>
                    </div>
                </div>
                <div class="deleteactive">
                    <div class="box">
                        <img src="../img/deleteactive.png" alt="deleteactive" srcset="" />
                        <div class="tip">松手即可删除</div>
                    </div>
                </div>
            </div>

            <input class="imageinput" style="display: none;" type="image" accept="image/jpeg,image/jpg,image/png" capture />


        </div>
        <script src="../js/fabric.js"></script>
        <script src="../js/html2canvas.min.js"></script>
        <script type="text/javascript">
            var userAgent = navigator.userAgent;
            if (/quickapp/i.test(userAgent)) {
                // quickapp
                document.write('<script type="text/javascript" src="https://quickapp/jssdk.webview.min.js"><\/script>');
            }
            if (!/toutiaomicroapp/i.test(userAgent)) {
                document.querySelector('.post-message-section').style.visibility = 'visible';
            }
        </script>
        <!-- uni 的 SDK -->
        <script src="../js/uni-webview.js"></script>
        <!-- <script type="text/javascript" src="https://unpkg.com/@dcloudio/uni-webview-js@0.0.1/index.js"></script> -->
        <script type="text/javascript">
            // console.log('接收参数', decodeURIComponent(location.href.split('data=')[1]))
            let params = JSON.parse(decodeURIComponent(location.href.split('data=')[1]))
            document.getElementsByClassName('headers')[0].style.paddingTop = params.statusBarHeight + 'px'
            document.getElementsByClassName('myCanvas')[0].style.paddingTop = params.top + 30 + 'px'
            document.getElementById('canvas').setAttribute('width', params.width + 'px')
            document.getElementById('canvas').setAttribute('height', params.height + 'px')
            document.getElementsByClassName('post-message-section')[0].style.height = params.screenHeight + 'px'

            // 生成canvas
            var canvas = new fabric.Canvas('canvas');
            // ...这里可以写canvas对象的一些配置,后面将会介绍
            

            // console.log('fabric.Textbox', fabric.Textbox)
            // 如果<canvas>标签没设置宽高,可以通过js动态设置
            // canvas.setWidth(350)
            // canvas.setHeight(200)
            function initCanvas() {

                // 读取图片地址,设置画布背景
                fabric.Image.fromURL('../img/10801920.png', (img) => {
                    img.set({
                        // 通过scale来设置图片大小,这里设置和画布一样大
                        scaleX: canvas.width / img.width,
                        scaleY: canvas.height / img.height,
                    });
                    // 设置背景
                    canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas));
                    canvas.renderAll();
                }, {
                    crossOrigin: 'anonymous'    //加上这个是因为我需要将海报下载到手机相册,允许跨域,如果没设置是下载不了图片的
                });


                // 通过图片路径添加
                fabric.Image.fromURL('../img/10801920.png', (img) => {
                    img.set({
                        scaleX: canvas.width / img.width / 2,
                        scaleY: canvas.height / img.height / 2,
                        hasControls: true, // 是否开启图层的控件
                    });
                    // 添加对象后, 如下图
                    canvas.add(img);
                    // deleteDiv(img)
                }, {
                    crossOrigin: 'anonymous'
                });
                
                
                
                
                // IText不可让文字竖排。textbox可以让文字竖排,自动根据宽度填写文字
                const IText = new fabric.Textbox('这是一段初始默认文字', {
                    left: 50,
                    top: 50,
                    // width: 50,    //不设置宽度,拉伸自动换行
                    fontSize: 18, // 字体大小
                    fontWeight: 600, // 字体粗细
                    fill: '#aaaaff', // 字体颜色
                    fontStyle: 'italic',  // 斜体
                    splitByGrapheme: true, //true文本会实时根据宽度进行换行
                    hasControls: true, //元素操作控件显示\即描边正方体
                    
                    // fontFamily: 'Delicious', // 设置字体
                    // fontFamily: 'monospace', // 设置字体
                    // fontFamily: 'fantasy', // 设置字体
                    // stroke: 'green', // 描边颜色
                    // strokeWidth: 3, // 描边宽度
                    // borderColor: 'orange',
                    // editingBorderColor: 'orange' // 点击文字进入编辑状态时的边框颜色
                    
                }, {
                    crossOrigin: 'anonymous'
                });
                // 添加文字
                canvas.add(IText)
                // deleteDiv(IText)
            }
            initCanvas()
            
            // 删除元素操作
            function deleteDiv(target) {
                target['on']('moving', function(info) {
                    document.getElementsByClassName('deleteFooter')[0].style.display = 'block'
                    if(info.pointer.y > params.height) {
                        document.getElementsByClassName('delete')[0].style.display = 'none'
                        document.getElementsByClassName('deleteactive')[0].style.display = 'block'
                    } else {
                        document.getElementsByClassName('delete')[0].style.display = 'block'
                        document.getElementsByClassName('deleteactive')[0].style.display = 'none'
                    }
                })
                target['on']('mouseup', function(info) {
                    if(info.pointer.y > params.height + 5) {
                        canvas.remove(target)
                    }
                    document.getElementsByClassName('delete')[0].style.display = 'block'
                    document.getElementsByClassName('deleteactive')[0].style.display = 'none'
                    document.getElementsByClassName('deleteFooter')[0].style.display = 'none'
                })
            }


            // webview页面加载完成
            document.addEventListener('UniAppJSBridgeReady', function() {
                // webview加载完成,显示页面
                document.getElementsByClassName('post-message-section')[0].style.display = 'block'

                // 返回上一页
                document.querySelector('.left').addEventListener('click', function(evt) {
                    uni.navigateBack()
                });
                // 分享
                document.querySelector('.right').addEventListener('click', function(evt) {
                    console.log(getComputedStyle(document.documentElement).getPropertyValue("--sat"))
                    console.log(getComputedStyle(document.documentElement).getPropertyValue("--sab"))
                    document.querySelector('.shares').style.display = 'block'
                    slideTimer(0, 'add', 1, 30)
                });
                document.querySelector('.sharesmask').addEventListener('click', function(evt) {
                    console.log('点击分享遮罩层', evt.target)
                    slideTimer(200, 'dec', 1, 30)
                })
                // 取消分享
                document.querySelector('.cancel').addEventListener('click', function(evt) {
                    slideTimer(200, 'dec', 1, 30)
                })
                // 高度动画
                function slideTimer(height, type, setup, times) {
                    let timer = null
                    let heights = height
                    let setups = setup
                    timer = setInterval(() => {
                        if(type === 'add') {
                            heights += 10 * setups
                        } else {
                            heights -= 10 * setups
                        }
                        console.log(Number(document.querySelector('.content').style.height.replace('px', '')))
                        document.querySelector('.content').style.height = heights + 'px'
                        setups++
                        if (Number(heights) <= 0 || Number(document.querySelector('.content').style.height.replace('px', '')) >= 200) {
                            clearInterval(timer)
                            if(type !== 'add') {
                                document.querySelector('.shares').style.display = 'none'
                            }
                            heights = 0
                            setups = 1
                        }
                    }, times)
                }
                
                // 分享到微信
                document.querySelector('.wx').addEventListener('click', function(evt) {
                    console.log('分享到微信')
                    uni.postMessage({
                        data: {
                            msg: 'wx'
                        }
                    });
                })
                
                // 分享到朋友圈
                document.querySelector('.friends').addEventListener('click', function(evt) {
                    console.log('分享到朋友圈')
                    uni.postMessage({
                        data: {
                            msg: 'friend'
                        }
                    });
                })

                // 点击添加文字
                document.querySelector('.edit-pen').addEventListener('click', function(evt) {
                    console.log('添加文字')
                    document.querySelector('.addText').style.display = 'block'
                    initAddText()
                    document.getElementsByClassName('color')[0].onchange = function(evt) {
                        console.log('颜色改变', evt.target.value)
                        document.getElementById('MainText').style.color = evt.target.value
                    }
                })
                
                // 点击加粗文字
                document.querySelector('.bold').addEventListener('click', function(evt) {
                    console.log('点击加粗文字')
                    if(document.getElementById('MainText').style.fontWeight === 'bold') {
                        document.getElementById('MainText').style.fontWeight = 'normal'
                    } else {
                        document.getElementById('MainText').style.fontWeight = 'bold'
                    }
                })
                
                // 点击斜体文字
                document.querySelector('.italic').addEventListener('click', function(evt) {
                    console.log('点击斜体文字')
                    if(document.getElementById('MainText').style.fontStyle === 'italic') {
                        document.getElementById('MainText').style.fontStyle = 'normal'
                    } else {
                        document.getElementById('MainText').style.fontStyle = 'italic'
                    }
                })
                
                // 点击文字变大
                document.querySelector('.amplify').addEventListener('click', function(evt) {
                    console.log('点击文字变大')
                    console.log(Number(document.getElementById('MainText').style.fontSize.replace('px', '')))
                    console.log(document.getElementById('MainText').style.fontSize.replace('px', ''))
                    document.getElementById('MainText').style.fontSize = Number(document.getElementById('MainText').style.fontSize.replace('px', ''))+1 + 'px'
                })
                
                // 点击文字变小
                document.querySelector('.reduce').addEventListener('click', function(evt) {
                    console.log('点击文字变小')
                    if(Number(document.getElementById('MainText').style.fontSize.replace('px', '')) === 12) {
                        console.log('无法再减了')
                    } else {
                        document.getElementById('MainText').style.fontSize = Number(document.getElementById('MainText').style.fontSize.replace('px', ''))-1 + 'px'
                    }
                })
                
                // 确认添加文字到canva中
                document.getElementsByClassName('confirmAddText')[0].addEventListener('click', function(evt) {
                    console.log('确认添加到canva中')
                    
                    if(document.getElementById('MainText').value !== '') {
                        // IText不可让文字竖排。textbox可以让文字竖排,自动根据宽度填写文字
                        const IText = new fabric.Textbox(document.getElementById('MainText').value, {
                            left: 50,
                            top: 50,
                            // width: 50,    //不设置宽度,拉伸自动换行
                            fontSize: Number(document.getElementById('MainText').style.fontSize.replace('px', '')), // 字体大小
                            fontWeight: document.getElementById('MainText').style.fontWeight, // 字体粗细
                            fill: document.getElementById('MainText').style.color, // 字体颜色
                            fontStyle: document.getElementById('MainText').style.fontStyle,  // 斜体
                            splitByGrapheme: true, //true文本会实时根据宽度进行换行
                            hasControls: true, //元素操作控件显示\即描边正方体
                        }, {
                            crossOrigin: 'anonymous'
                        });
                        // 添加文字
                        canvas.add(IText);
                        deleteDiv(IText)
                        
                        initAddText()
                        document.querySelector('.addText').style.display = 'none'
                    } else {
                        uni.postMessage({
                            data: {
                                msg: 'addEmptyText'
                            }
                        });
                    }
                })  
                
                // 关闭添加文字
                document.querySelector('.addTextmask').addEventListener('click', function(evt) {
                    initAddText()
                    document.querySelector('.addText').style.display = 'none'
                })
                
                // 取消添加文字
                document.getElementsByClassName('cancelAddText')[0].addEventListener('click', function(evt) {
                    console.log('取消添加文字')
                    initAddText()
                    document.querySelector('.addText').style.display = 'none'
                })
                
                // 初始化添加文字
                function initAddText() {
                    document.getElementById('MainText').style.fontWeight = 'normal'
                    document.getElementById('MainText').style.fontStyle = 'normal'
                    document.getElementById('MainText').style.fontSize = '18px'
                    document.getElementById('MainText').style.color = '#000000'
                    document.getElementById('MainText').value = ''
                    document.getElementsByClassName('color')[0].value = '#000000'
                }
                

                // 恢复默认canvas
                document.querySelector('.reload').addEventListener('click', function(evt) {
                    console.log('恢复默认')
                    canvas.clear();
                    initCanvas()
                })


                // app主动与html通信,用于添加图片到canvas中
                function addUniEvenPassthrough() {
                    window.uniEvent = function(info) {
                        console.log('获取完相册了', info)
                        fabric.Image.fromURL(info, (img) => {
                            img.set({
                                scaleX: canvas.width / img.width / 2,
                                scaleY: canvas.height / img.height / 2,
                                hasControls: true, // 是否开启图层的控件
                            });
                            // 添加对象后, 如下图
                            canvas.add(img);
                            deleteDiv(img)
                        }, {
                            crossOrigin: 'anonymous'
                        });
                    }
                }
                addUniEvenPassthrough()
                
                // 点击了图片/二维码
                document.querySelector('.pic').addEventListener('click', function(evt) {
                    console.log('点击了图片/二维码')
                    uni.postMessage({
                        data: {
                            msg: 'openCamera'
                        }
                    });
                })

                // 海报保存本地
                document.querySelector('.downs').addEventListener('click', function(evt) {
                    console.log('保存本地')
                    if (document.getElementsByClassName('canvasimg')[0]) {
                        document.getElementsByClassName('canvasimg')[0].remove()
                    }
                    let dom = document.getElementById('canvas')
                    var img = new Image();
                    img.crossOrigin = "anonymous"; //关键、允许跨域
                    var image = dom.toDataURL('image/png')
                    img.src = image;
                    img.setAttribute('style','position: absolute; top: 0px; left: -666px;width: 320px; height: 750px;')
                    img.setAttribute('class', 'canvasimg')
                    document.body.appendChild(img);

                    let canvasdom = document.getElementsByTagName('canvas')[0]
                    html2canvas(canvasdom, {
                        width: canvasdom.clientWidth, //dom 原始宽度
                        height: canvasdom.clientHeight,
                        scrollY: 0, // html2canvas默认绘制视图内的页面,需要把scrollY,scrollX设置为0
                        scrollX: 0,
                        useCORS: true, //支持跨域
                        scale: 2, // 设置生成图片的像素比例,默认是1,如果生成的图片模糊的话可以开启该配置项。(设置为1无法触发长按扫一扫)
                    }).then((res) => {
                        console.log(res)
                        uni.postMessage({
                            data: {
                                msg: 'save',
                                canvas: res.toDataURL('image/png').replace(/[\r\n]/g, '')
                            }
                        });
                    }).catch(err => {
                        console.log('生成失败')
                    })
                })

            });
        </script>
    </body>
</html>

效果图:
image.png
image.png
image.png


会说话的树
41 声望0 粉丝