微信小程序webview页面使用painter生成海报

微信小程序webview页面使用painter生成海报

因为要在webview下生成海报,需要使用cover-view,根据接口返回数据动态更新海报内容,微信小程序生成海报组件有wxa-plugin-canvas、painter等,这里我们使用painter来生成海报图片,ui如下图:

微信小程序webview页面使用painter生成海报_第1张图片
该分享海报动态数据包括背景banner图、标题、副标题、设计师名字及二维码

分享弹窗及按钮UI及代码请见:webview页面分享弹窗

webview页面

wxml代码如下:


    
    
        
            
                
            
        

        
                
        >
        保存图片
    




js代码如下

// pages/details/introduction/introduction.js
import sharePosterObj from '../../../components/common/share/page-poster-data/introduction';
const app = getApp();
Page({
    /**
     * 页面的初始数据
     */
    data: {
        /*分享相关参数 */
        showShareSheet: false, //是否显示分享弹窗
        shareData: null, //默认为null
        showPosterPreview: false, //是否展示海报预览弹窗
        posterData: null, //生成海报数据树
        tempFilePath: '',
        maxHeight: 525, // 海报预览的最大高度
        imgHeight: '', //海报的高度
    },

    /**
     * 生命周期函数--监听页面加载
     */
    onLoad: function (options) {
        let self = this;
        wx.showLoading({
            title: '加载中',
            mask: true,
        });
        //获取动态数据,根据实际情况调整
        self.getBaseRequestData();
        app.getSystemInfo(function (systemMsg) {
            self.data.screenWidth = systemMsg.screenWidth;
            // 海报预览盒子最大高度 根据实际情况调整
            const maxHeight = Math.floor(
                systemMsg.availableHeight - (68 * systemMsg.screenWidth) / 750
            );
            self.setData({
                maxHeight,
            });
        });
    },
    // 请求接口、获取动态数据
    getBaseRequestData() {
        const self = this;
        baseUtil.baseConfigData().then(data => {
            self.data.shareData = self.data.shareData || {
                showImg: '',
                title: '',
                subordinateTitle: '',
                qrCodeImg: '',
                posterDescribe: '',
            };
       		// 在这里对shareData 重新赋值并设置setData
       		// ... 代码省略 根据具体情况设置
         	// 给painter组件设置参数
                self.setPosterData();
        });
    },
    /********分享相关开始********/
    // 点击分享按钮
    shareAction() {
        this.setData({
            showShareSheet: true,
        });
    },
    poptouchmove: function () {
        return false;
    },
    //点击取消按钮
    quickAction() {
        this.data.showShareSheet = false;
        this.setData({
            showShareSheet: false,
        });
    },
    /********生成海报相关开始********/
    //获取ctx
    getTextHeightCtx() {
        const self = this;
        return new Promise(resolve => {
            const query = wx.createSelectorQuery().in(self);
            query
                .select('#heightCanvas')
                .fields({ node: true, size: true })
                .exec(res => {
                    const canvasNode = res[0].node;
                    const ctx = canvasNode.getContext('2d');
                    resolve(ctx);
                });
        });
    },
    async setPosterData() {
        const self = this;
        if (!self.textHeightCtx) {
            const textHeightCtx = await self.getTextHeightCtx();
            self.textHeightCtx = textHeightCtx;
        }
        sharePosterObj
            .createPosterData(self.textHeightCtx, self.data.shareData)
            .then(res => {
                self.setData({
                    posterData: res,
                });
            })
            .catch(error => {
                wx.hideLoading();
                wx.showToast({
                    title: (error && error.errMsg) || '生成失败,请重试',
                    icon: 'none',
                });
            });
    },
    // 保存图片到本地
    savePoster() {
        this.isWritePhotosAlbum();
    },
    //判断是否授权(访问相册)
    isWritePhotosAlbum: function () {
        let self = this;
        //判断是否授权
        wx.getSetting({
            success: function (res) {
                let writePhotosAlbum = res.authSetting['scope.writePhotosAlbum'];
                if (writePhotosAlbum) {
                    //已授权 //true
                    self.saveImageToPhoto();
                } else if (writePhotosAlbum != undefined && writePhotosAlbum != true) {
                    //拒绝授权了 //false
                    self.data.savePhotoIng = false;
                    wx.showModal({
                        title: '',
                        content: '您还未授权保存图片到相册,请确认授权',
                        showCancel: true,
                        cancelText: '取消',
                        confirmText: '确认',
                        success: function (e) {
                            //点了查看规则
                            if (e.confirm) {
                                //针对用户保存图片的时候可能会拒绝授权,再次点击时需要调起授权窗口
                                self.openSetting();
                            } else {
                                //取消
                            }
                        },
                    });
                } else {
                    //第一次授权 //undefined
                    self.saveImageToPhoto();
                }
            },
        });
    },
    //授权操作(访问相册)
    openSetting: function () {
        let self = this;
        //调起授权弹窗
        wx.openSetting({
            success(res) {
                //同意授权
                if (res.authSetting['scope.writePhotosAlbum']) {
                    self.saveImageToPhoto();
                }
            },
        });
    },
    toPx(rpx, int) {
        if (int) {
            return parseInt(rpx * this.factor * this.pixelRatio);
        }
        return rpx * this.factor * this.pixelRatio;
    },
    toRpx(px, int) {
        if (int) {
            return parseInt(px / this.factor);
        }
        return px / this.factor;
    },
    //点击生成海报按钮
    createPosterAction() {
        if (!this.data.tempFilePath) {
            this.setData({
                clickCreatePosterBtn: true,
                showPosterPreview: false,
                showShareSheet: false,
            });
            if (!this.data.shareData) {
                // this.data.isCreating = false;
                // this.data.clickedCreateBtn = false;
                wx.showToast({
                    title: '分享数据不存在',
                    icon: 'none',
                });
                return;
            }
            wx.showLoading({
                title: '生成中...',
                mask: true,
            });
            if (!this.data.posterData) {
                this.setPosterData();
            }
            return;
        }
        this.setData({
            showPosterPreview: true,
            clickCreatePosterBtn: false,
            showShareSheet: false,
        });
    },
    onImgOK(e) {
        console.log('onImgOK', e);
        const self = this;
        self.data.tempFilePath = e.detail.path;
        //获取海报图高度
        wx.getImageInfo({
            src: e.detail.path,
            success(res) {
                const { width, height } = res;
                let scaleHeight = Math.floor((self.data.screenWidth * 0.67 * height) / width);
                if (scaleHeight > self.data.maxHeight) {
                    scaleHeight = self.data.maxHeight;
                }
                self.setData({
                    imgHeight: scaleHeight,
                });
            },
            fail() {},
        });

        self.setData({
            tempFilePath: self.data.tempFilePath,
        });
        wx.hideLoading();
        if (self.data.clickCreatePosterBtn) {
            self.setData({
                showPosterPreview: true,
            });
            self.data.clickCreatePosterBtn = false;
        }
    },
    onImgErr(err) {
        console.log('onImgErr', err);
        wx.hideLoading();
    },
    closePreviewPop() {
        this.setData({
            showPosterPreview: false,
        });
    },
    //保存图片到相册
    saveImageToPhoto: function () {
        let self = this;
        const tempFilePath = self.data.tempFilePath;
        wx.saveImageToPhotosAlbum({
            filePath: tempFilePath,
            success(res) {
                wx.showToast({
                    title: '保存成功',
                    icon: 'success',
                });
                self.setData({
                    showPosterPreview: false,
                });
            },
            fail: function (res) {
                if (res.errMsg === 'saveImageToPhotosAlbum:fail auth deny') {
                    toast('您拒绝了授权无法保存图片 ');
                } else {
                    if (
                        res.errMsg === 'saveImageToPhotosAlbum:fail:auth canceled' ||
                        res.errMsg === 'saveImageToPhotosAlbum:fail cancel'
                    ) {
                        console.log('用户取消');
                    } else {
                        wx.showToast({
                            title: '保存失败,请重试',
                            icon: 'none',
                        });
                    }
                }
            },
            complete: function (res) {
                console.log('保存图片到本地 结束', res);
            },
        });
    },
});

less样式如下:
其中引用的两个样式可以参考webview页面分享弹窗

@import '../../../components/common/share/share-sheet/share-sheet.wxss';
@import '../../../components/common/share/poster-share/poster-share.wxss';
.bottom-fixed-bar {
    position: fixed;
    box-sizing: content-box;
    bottom: 0;
    left: 0;
    width: 100%;
    padding-bottom: env(safe-area-inset-bottom);
    z-index: 100;
    background-color: #fff;
    .icon {
        margin-right: 16rpx;
        width: 32rpx;
        height: 32rpx;
    }
    .bar-info {
        width: 100%;
        height: 112rpx;
        padding: 0 48rpx;
        background-color: #fff;
    }
    .share-btn {
        position: relative;
        margin-right: 24rpx;
        width: 214rpx;
        height: 80rpx;
        border-radius: 40rpx;
        background: #ffffff;
        border: 2rpx solid #222222;
        text-align: center;
        line-height: 79rpx;
        font-size: 30rpx;
    }
    .share-main-btn {
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 2;
        width: 214rpx;
        height: 80rpx;
        line-height: 79rpx;
        color: rgba(0, 0, 0, 0);
        &::after {
            display: none;
        }
    }
    .collect-btn {
        display: flex;
        align-items: center;
        justify-content: center;
        text-align: center;
        height: 80rpx;
        line-height: 79rpx;
        border-radius: 40rpx;
        background: #0c0c0c;
        color: #fff;
        font-size: 30rpx;
    }
}

生成painter json数据树的js “page-poster-data/introduction“:

//作品详情页面的postr data 数据
import shareUtils from '../share-utils';
const app = getApp();
const createPosterData = (ctx, shareData) => {
    return new Promise(async (resolve, reject) => {
        const userInfo = app.globalData.userInfo || wx.getStorageSync('userInfo');
        const { showImg, title, qrCodeImg, subordinateTitle, posterDescribe } = shareData;
        const titleView = {
            // id: 'title',
            type: 'text',
            text: title,
            css: {
                left: '48rpx',
                width: '450rpx',
                fontWeight: '500',
                color: '#fff',
                fontSize: '50rpx',
                lineHeight: '72rpx',
                scalable: true,
                maxLines: 2,
            },
        };
        const subordinateTitleView = {
            type: 'text',
            text: subordinateTitle,
            css: {
                left: '48rpx',
                bottom: '334rpx',
                width: '450rpx',
                color: '#fff',
                fontSize: '26rpx',
                lineHeight: '40rpx',
                scalable: true,
                maxLines: 2,
            },
        };
        let scaleImgHeight = 0;
        try {
            const imgPositionData = await shareUtils.getImgPositionData(showImg);
            if (imgPositionData) {
                scaleImgHeight = imgPositionData.scaleHeight;
            }
        } catch (error) {
            reject(error);
            return;
        }

        let titleH = 0;
        const titlePositionData = await shareUtils.getTextPositionData({ ctx, view: titleView });
        if (titlePositionData) {
            titleH = shareUtils.toRpx(titlePositionData.height);
        }
        let subordinateTitleH = 0;
        const subordinateTitlePositionData = await shareUtils.getTextPositionData({
            ctx,
            view: subordinateTitleView,
        });

        if (subordinateTitlePositionData) {
            subordinateTitleH = shareUtils.toRpx(subordinateTitlePositionData.height);
        }
        const paddingBottom = 380; //从副标题往下的距离 rpx
        const titleDistance = 40; //主标题到副标题之间的距离 rpx
        titleView.css.bottom = `${paddingBottom + subordinateTitleH + titleDistance}rpx`;
        const posterHeight = Math.ceil(
            scaleImgHeight + (titleH * 2) / 3 + titleDistance + subordinateTitleH + paddingBottom
        );
        const distanceTop = Math.ceil(scaleImgHeight * 0.38); // 渐变色距离顶部的距离 rpx
        //渐变背景的高度
        const linearGradientHeight = Math.ceil(
            distanceTop + (titleH * 2) / 3 + titleDistance + subordinateTitleH + paddingBottom
        );
        const startLinear =
            Math.ceil((1 - distanceTop / linearGradientHeight).toFixed(2) * 100) + '%';
        // 宽度和高度必须 位置必须rpx才能生效,否则默认左上角 0 0 位置 渐变后面的百分数必须写

        const data = {
            width: '750rpx',
            height: `${posterHeight}rpx`,
            background: '#060419',
            views: [
                // 顶栏图片
                {
                    type: 'image',
                    url: showImg,
                    css: {
                        top: '0rpx',
                        left: '0rpx',
                        width: '750rpx',
                        mode: 'widthFix',
                        scalable: true,
                    },
                },
                //渐变矩形
                {
                    type: 'rect',
                    css: {
                        left: '0rpx',
                        bottom: '0rpx',
                        width: '750rpx',
                        height: `${linearGradientHeight}rpx`,
                        //从下向上的渐变
                        color: `linear-gradient(180deg, rgba(6, 4, 25, 1) 0%, rgba(7,4,26,1) ${startLinear}, transparent 100%)`,
                        scalable: true,
                    },
                },
                //标题
                titleView,
                // 副标题
                subordinateTitleView,
                //分割线
                {
                    type: 'rect',
                    css: {
                        left: '50rpx',
                        bottom: '250rpx',
                        width: '320rpx',
                        height: '2rpx',
                        color: 'rgba(196, 196, 196, 0.3)',
                    },
                },
                // 个人头像
                {
                    type: 'image',
                    url: (userInfo && userInfo.headPortraitUrl) || '',
                    css: {
                        bottom: '142rpx',
                        left: '50rpx',
                        width: '42rpx',
                        height: '42rpx',
                        scalable: true,
                        borderRadius: '4rpx',
                    },
                },
                //个人昵称
                {
                    type: 'text',
                    text: (userInfo && userInfo.nickName) || '',
                    css: {
                        left: '110rpx',
                        bottom: '142rpx',
                        color: '#fff',
                        fontSize: '28rpx',
                        align: 'left',
                        scalable: true,
                        lineHeight: '42rpx',
                    },
                },
                // 邀请您一起做客直播间
                {
                    type: 'text',
                    text: posterDescribe,
                    css: {
                        left: '50rpx',
                        bottom: '78rpx',
                        width: '440rpx',
                        color: 'rgba(255,255,255,0.5)',
                        fontSize: '28rpx',
                        lineHeight: '30rpx',
                        scalable: true,
                        maxLines: 1,
                    },
                },
                // 二维码
                {
                    type: 'image',
                    // url: codeImg,
                    url: qrCodeImg,
                    css: {
                        bottom: `50rpx`,
                        right: '48rpx',
                        backgroundColor: '#fff',
                        width: '160rpx',
                        height: '160rpx',
                        scalable: true,
                        borderRadius: '160rpx',
                    },
                },
            ],
        };
        resolve(data);
    });
};
module.exports.createPosterData = createPosterData;

引用计算文本高度的组件 js “share-utils“ :

//获取text类型的行高 行数等
const screenWidth = wx.getSystemInfoSync().screenWidth;
/*
    text:文案
    fontSize:字体大小 只支持rpx 数值类型
    maxLines: 文案最大行数  可不传 数值类型
    width: 文案宽度  可不传 只支持rpx 数值类型
    padding: 文案padding值为数组格式 只支持rpx值 可不传

*/
const getTextPositionData = ({ ctx, view }) => {
    let paddings = doPaddings(view.css.padding);
    const textArray = String(view.text).split('\n');
    // 处理多个连续的'\n'
    for (let i = 0; i < textArray.length; ++i) {
        if (textArray[i] === '') {
            textArray[i] = ' ';
        }
    }
    if (!view.css.fontSize) {
        view.css.fontSize = '20rpx';
    }
    const fontWeight = view.css.fontWeight || '400';
    const textStyle = view.css.textStyle || 'normal';
    ctx.font = `${textStyle} ${fontWeight} ${toPx(view.css.fontSize)}px "${
        view.css.fontFamily || 'sans-serif'
    }"`;
    // 计算行数
    let lines = 0;
    // let totalWidth = 0
    const linesArray = [];
    for (let i = 0; i < textArray.length; ++i) {
        const textLength = ctx.measureText(textArray[i]).width;
        const minWidth = toPx(view.css.fontSize) + paddings[1] + paddings[3];
        let partWidth = view.css.width
            ? toPx(view.css.width) - paddings[1] - paddings[3]
            : textLength;
        if (partWidth < minWidth) {
            partWidth = minWidth;
        }
        const calLines = Math.ceil(textLength / partWidth);
        // 取最长的作为 width
        // totalWidth = partWidth > totalWidth ? partWidth : totalWidth;
        lines += calLines;
        linesArray[i] = calLines;
    }
    lines = view.css.maxLines < lines ? view.css.maxLines : lines;
    const lineHeight = view.css.lineHeight ? toPx(view.css.lineHeight) : toPx(view.css.fontSize);
    const height = lineHeight * lines;
    return {
        lines, //行数
        height, //高度
    };
};
//计算rpx px转换用
const toPx = (rpx, int, factor = screenWidth / 750, pixelRatio = 1) => {
    rpx = rpx.replace('rpx', '');
    if (int) {
        return parseInt(rpx * factor * pixelRatio);
    }
    return rpx * factor * pixelRatio;
};
const toRpx = (px, int, factor = screenWidth / 750) => {
    if (int) {
        return parseInt(px / factor);
    }
    return px / factor;
};
const doPaddings = padding => {
    let pd = [0, 0, 0, 0];
    if (padding) {
        const pdg = padding.split(/\s+/);
        if (pdg.length === 1) {
            const x = toPx(pdg[0]);
            pd = [x, x, x, x];
        }
        if (pdg.length === 2) {
            const x = toPx(pdg[0]);
            const y = toPx(pdg[1]);
            pd = [x, y, x, y];
        }
        if (pdg.length === 3) {
            const x = toPx(pdg[0]);
            const y = toPx(pdg[1]);
            const z = toPx(pdg[2]);
            pd = [x, y, z, y];
        }
        if (pdg.length === 4) {
            const x = toPx(pdg[0]);
            const y = toPx(pdg[1]);
            const z = toPx(pdg[2]);
            const a = toPx(pdg[3]);
            pd = [x, y, z, a];
        }
    }
    return pd;
};
// 获取图片宽高以及750像素下的高度
export const getImgPositionData = img => {
    return new Promise((resolve, reject) => {
        if (img) {
            wx.getImageInfo({
                src: img,
                success(res) {
                    const { width, height } = res;
                    const scaleHeight = Math.ceil((750 * height) / width);
                    resolve({
                        width,
                        height,
                        scaleHeight,
                    });
                },
                fail() {
                    reject({
                        errMsg: '获取图片信息失败, 请重试',
                    });
                },
            });
        } else {
            // wx.hideLoading();
            reject({
                errMsg: '图片不存在',
            });
        }
    });
};
export default {
    getTextPositionData,
    getImgPositionData,
    toPx,
    toRpx,
};

注意:

提示:以下均为真机测试,实际情况可能会随着时间改变,请注意自测

  1. painter 高度和宽度必须给定

  2. painter 位置必须添加单位,否则默认左上角0 0,例如: top值不能直接写0,需要写0rpx
    微信小程序webview页面使用painter生成海报_第2张图片

  3. painter 渐变属性名为color 不支持to bottom格式
    微信小程序webview页面使用painter生成海报_第3张图片

  4. painter cnpm方式安装的版本非最新版本,demo里的版本为最新

  5. painter git提交代码时painter目录会被忽略,需要更改忽略文件配置或者直接上传该目录

  6. 手机预览及本地预览可以正确通过createSelectorQuery获取canvas但是真机调试会报错,上线后也正常
    微信小程序webview页面使用painter生成海报_第4张图片微信小程序webview页面使用painter生成海报_第5张图片

  7. webview下个别时候setData不生效,具体原因未知

你可能感兴趣的:(微信小程序,微信小程序,小程序)