3.x动态二维码使用示例

一灰灰blogQuickMediaqrcode-plugin约 2851 字大约 10 分钟

前面几篇介绍的二维码,输出都是静态图(png/jpg)或者矢量图(svg),本篇我们来玩点不一样的——动态二维码,也就是我们平时常见的 GIF 动图二维码:二维码本身一动不动,动的是背景图或者前置图。

1. 场景介绍

静态二维码看多了难免审美疲劳,在一些营销海报、公众号推文的场景下,一个会动的二维码往往更能吸引人扫码。在 quick-media 的 qrcode-plugin 中,实现动态二维码的思路非常朴素:

  • 二维码信息(也就是扫出来的内容)是固定的,每一帧都一样;
  • 动的是"背景图"或者"前置图":我们事先准备一张 gif 格式的背景图(或前置图),插件会读取 gif 的每一帧,将静态二维码与每一帧背景图进行合成;
  • 最后把所有合成后的帧,重新打包输出为一个 gif 文件。

对应的实现逻辑主要在两个地方,有兴趣看源码的小伙伴可以对照着读:

  • 3.x 版本:QrCodeGenV3#asGif() -> QrRenderFacade#renderAsGif()
  • 2.x 版本:QrCodeGenWrapper#asGif() -> QrCodeGenerateHelper#toGifImages()

以 2.x 的 QrCodeGenerateHelper#toGifImages 为例,核心流程就三步:

// 1. 先绘制一张静态二维码
BufferedImage qrCode = QrCodeRenderHelper.drawQrInfo(qrCodeConfig, bitMatrix);

// 2. 读取 gif 背景图的每一帧,将二维码叠加到每一帧背景图上,得到一组合成后的帧
List<ImmutablePair<BufferedImage, Integer>> bgList =
        QrCodeRenderHelper.drawGifBackground(qrCode, qrCodeConfig.getBgImgOptions());

// 3. 逐帧插入 logo,最后交由 GifHelper.saveGif 输出 gif 文件

注意:ImmutablePair<BufferedImage, Integer> 中的 Integer 是这一帧的延时时间(单位 ms),直接取自原始 gif 每一帧的延时,所以最终输出动图的播放节奏与原始 gif 保持一致。

2. 依赖引入

项目已发布到 maven 中央仓库,直接依赖即可,请注意使用最新的版本

maven 的使用姿势:

<!-- https://mvnrepository.com/artifact/com.github.liuyueyi.media/qrcode-plugin -->
<dependency>
    <groupId>com.github.liuyueyi.media</groupId>
    <artifactId>qrcode-plugin</artifactId>
    <version>3.0.1</version>
</dependency>

gradle 的使用姿势:

// https://mvnrepository.com/artifact/com.github.liuyueyi.media/qrcode-plugin
implementation 'com.github.liuyueyi.media:qrcode-plugin:3.0.0'

下面所有实例,都可以在源码的测试目录中直接获取:

测试类的基础配置与前面几篇保持一致:

public class BasicGenTest {
    protected String prefix = "/tmp";
    protected static final String msg = "http://weixin.qq.com/r/FS9waAPEg178rUcL93oH";

    @Before
    public void init() {
        if (OSUtil.isWinOS()) {
            prefix = "d://quick-media";
        }
    }
}

3. 背景图为动图

3.1 全覆盖模式(OVERRIDE)

最常见的姿势:找一张 gif 当背景图,二维码以一定透明度覆盖在整个背景图上。使用姿势与静态背景图几乎一样,只是资源用 setGif() 加载,输出类型为 gif:

/**
 * 背景作为动图的场景
 */
@Test
public void bgGif() throws Exception {
    boolean ans = QrCodeGenV3.of(msg).setSize(500)
            .newBgOptions()
            .setBg(new QrResource().setGif("gifs/bgOverride.gif"))
            .setOpacity(0.6f)
            .complete()
            // 指定输出gif格式二维码,默认保存为图片的时候,可以省略;
            // 一般建议具体指定生成二维码的类型更合适
            .setQrType(QrType.GIF)
            .asFile(prefix + "/gif_bg.gif");
    System.out.println(ans);
}

动图效果建议直接运行上面的示例查看,gif 是会动的,静态截图展示不出来那个味道。

这里新增的几个配置点说明一下:

  • newBgOptions():进入背景图配置;配置完成后调用 complete() 返回 QrCodeV3Options
  • setBg(new QrResource().setGif(...)):指定 gif 背景资源
  • setOpacity(0.6f):OVERRIDE 模式下二维码的透明度,越小越透
  • setQrType(QrType.GIF):声明输出为 gif

3.2 填充模式(FILL)

二维码不是铺满整张背景图,而是嵌在背景图的某个区域。需要注意,使用 setImg() 加载 gif 时,文件名务必以 .gif 结尾;更推荐的姿势还是 setGif(),避免文件名后缀不对导致 gif 无法被正确识别:

@Test
public void bgGifFill() throws Exception {
    boolean ans = QrCodeGenV3.of(msg).setSize(400)
            // 说明:指定背景图为gif时,也可以直接使用 setImg, 但是这种方式请注意,文件名务必是 gif 结尾
            // 官方更推荐的是使用 setGif 来指定gif资源图,避免文件名后缀不对导致无法正确识别为gif
            .setBgResource(new QrResource().setImg("gifs/bgFill.gif"))
            .setBgStyle(BgStyle.FILL)
            .setBgX(20).setBgY(137)
            .asFile(prefix + "/gif_bg_fill.gif");
    System.out.println(ans);
}
  • setBgStyle(BgStyle.FILL):二维码填充在背景图指定位置
  • setBgX(20) / setBgY(137):二维码在背景图上的绘制坐标

3.3 穿透模式(PENETRATE)

穿透模式是背景图的颜色"透过"二维码显示,因此如果背景是一张颜色渐变的 gif,我们就能得到一个颜色不停变化的动态二维码,这个效果非常好看:

/**
 * 使用一张动态的颜色渐变的背景图,从而实现二维码颜色变化的场景
 *
 * @throws Exception
 */
@Test
public void bgGifPenetrate() throws Exception {
    boolean ans = QrCodeGenV3.of(msg).setSize(500)
            .setBgResource(new QrResource().setImg("gifs/bgPen.gif"))
            .setBgW(500).setBgH(500)
            .setBgStyle(BgStyle.PENETRATE)
            .asFile(prefix + "/gif_bg_penetrate.gif");
    System.out.println(ans);
}

穿透模式下不需要设置透明度,起作用的是 BgStyle.PENETRATE 这个枚举值,它的语义是:二维码主题色为透明,由背景图的颜色进行填充。

4. 前置图为动图

前置图位于二维码的最上层,把前置图换成 gif,常见玩法就是给二维码加一个"会动的 logo":

/**
 * 借助前置图实现一个gif的logo效果图
 *
 * @throws Exception
 */
@Test
public void ftGif() throws Exception {
    int size = 500;
    int ftSize = size / 4;
    boolean ans = QrCodeGenV3.of(msg)
            .setSize(size)
            .clearLogoArea(true)
            .setFtResource(new QrResource().setImg("gifs/preQie.gif"))
            .setFtW(ftSize).setFtH(ftSize)
            .setFtX((ftSize - size) / 2)
            .setFtY((ftSize - size) / 2)
            .asFile(prefix + "/gif_ft_logo.gif");
    System.out.println(ans);
}
  • clearLogoArea(true):清除前置图覆盖区域的二维码信息,避免 logo 底下还透出码元影响观感
  • setFtW / setFtH:前置图的宽高,这里是二维码的 1/4
  • setFtX / setFtY:二维码在前置图上的绘制坐标,注意前置图小于二维码时坐标可以是负数,这里通过 (ftSize - size) / 2 把前置图居中到二维码中心

再来看一个前置图大于二维码的玩法,前置图整图输出,二维码作为其中的一部分:

@Test
public void ftGifV2() throws Exception {
    boolean ans = QrCodeGenV3.of(msg)
            .setSize(500)
            .setPreColor(0xff73a7f5)
            .setFtResource(new QrResource().setImg("gifs/pre.gif"))
            .setFtX(-50)
            .setFtY(-120)
            .setFtFillColor(Color.WHITE)
            .asFile(prefix + "/gif_ft.gif");
    System.out.println(ans);
}
  • setFtFillColor(Color.WHITE):前置图为 png 存在透明区域时,指定透明区域的填充色,避免输出的 gif 中透明区域显示异常

5. 2.x 版本的动态二维码

如果还在使用 2.x 版本(QrCodeGenWrapper),同样支持动态二维码,姿势是:背景图传 gif 地址 + setPicType("gif"),输出文件名以 .gif 结尾:

public class AnimateQrCodeTest {

    @Test
    public void testGenAnimateColorCode() {
        String msg = "http://weixin.qq.com/r/FS9waAPEg178rUcL93oH";
        // 根据本地文件生成待logo的二维码, 重新着色位置探测图像
        try {
            String logo = "logo.jpg";
            String bg = "http://ww1.sinaimg.cn/large/8154e929gy1g8pq78mcgrg20dw0boaja.gif";
            boolean ans = QrCodeGenWrapper.of(msg)
                    .setW(500)
                    .setDrawPreColor(0xff002fa7) // 宝石蓝
                    .setDetectOutColor(0xff00ff00)
                    .setDetectInColor(0xffff0000)
                    .setPadding(1)
                    .setErrorCorrection(ErrorCorrectionLevel.H)
                    .setLogo(logo)
                    .setLogoBorder(true)
                    .setLogoBgColor(Color.LIGHT_GRAY)
                    .setLogoStyle(QrCodeOptions.LogoStyle.ROUND)
                    .setBgImg(bg)
                    .setBgOpacity(0.6f)
                    .setPicType("gif")
                    .asFile("/tmp/out.gif");
            System.out.println(ans);
        } catch (Exception e) {
            System.out.println("cmvreate qrcode error! e: " + e);
        }
    }
}

gif 背景图与图片码元(DrawStyle.IMAGE)也是可以混搭的,即二维码的码元本身用小图片渲染,再叠一个 gif 背景,这就是很多"花里胡哨"动态二维码的实现姿势:

@Test
public void gifQr4() throws IOException {
    String msg = "http://weixin.qq.com/r/FS9waAPEg178rUcL93oH";
    String bg = "http://ww1.sinaimg.cn/large/8154e929gy1g8w9jsxwtdg20pz08zwr8.gif";
    String logo = "logo.jpg";
    try {
        boolean ans = QrCodeGenWrapper.of(msg)
                .setW(500)
                .setDrawBgColor(ColorUtil.OPACITY)
                .setDrawStyle(QrCodeOptions.DrawStyle.IMAGE)
                .setDetectImg("jihe/PDP.png")
                .addImg(1, 1, "jihe/a.png")
                .addImg(3, 1, "jihe/b.png")
                .addImg(1, 3, "jihe/c.png")
                .addImg(3, 2, "jihe/e.png")
                .addImg(2, 3, "jihe/f.png")
                .addImg(2, 2, "jihe/g.png")
                .addImg(3, 4, "jihe/h.png")
                .setPadding(1)
                .setErrorCorrection(ErrorCorrectionLevel.H)
                .setLogo(logo)
                .setLogoBorder(true)
                .setLogoStyle(QrCodeOptions.LogoStyle.ROUND)
                .setLogoBgColor(0xfffefefe)
                .setLogoBorderBgColor(0xffc7c7c7)
                .setBgImg(bg)
                .setBgW(1870)
                .setBgH(646)
                .setBgStyle(QrCodeOptions.BgImgStyle.FILL)
                .setBgStartX(690)
                .setBgStartY(20)
                .setBgOpacity(0.9f)
                .setPicType("gif")
                .asFile("/tmp/gifQr4.gif");
    } catch (WriterException e) {
        e.printStackTrace();
    }
}

注意上面这个 gifQr4 的几个点:

  • gif 背景图的实际尺寸是 1870x646,二维码只填充在其中的一个区域(setBgStartX(690)),所以必须显式设置 setBgW / setBgH,并使用 FILL 模式
  • addImg(row, col, img) 表示用图片替换 MxN 大小的码元区域,如 addImg(3, 4, "jihe/h.png") 表示用一张图替换 3x4 的码元块
  • setDrawBgColor(ColorUtil.OPACITY) 将背景色设置为透明,避免二维码自身的白色背景遮挡住 gif 背景图

6. 参数小结

把本篇新出现的参数汇总一下,方便速查:

参数说明
setQrType(QrType.GIF)3.x 声明输出 gif 格式;保存文件时若文件名以 .gif 结尾可省略,但建议显式指定
setGif(String)QrResource 中加载 gif 资源的方式,官方推荐优先于 setImg
newBgOptions().setBg(...)3.x 配置背景图,支持 QrResource 传入 gif
setOpacity(float)OVERRIDE 模式下二维码透明度,0~1,越小越透
setBgStyle(BgStyle)背景图样式:OVERRIDE 全覆盖 / FILL 填充指定区域 / PENETRATE 穿透
setBgW / setBgH背景图宽高,gif 背景与二维码大小不一致时需要设置
setBgX / setBgYFILL 模式下二维码在背景图上的绘制坐标(2.x 对应 setBgStartX / setBgStartY)
setFtResource(new QrResource().setGif(...))前置图为 gif,实现动态 logo 效果
setFtW / setFtH前置图宽高
setFtX / setFtY二维码在前置图上的绘制坐标,可为负数
setFtFillColor(Color)前置图透明区域的填充色
clearLogoArea(boolean)清除前置图覆盖区域的二维码信息
setPicType("gif")2.x 声明输出 gif
setBgImg(String)2.x 设置背景图,传 gif 地址即可
setBgOpacity(float)2.x 的二维码透明度
addImg(row, col, img)2.x 用图片替换 MxN 大小的码元区域

7. 注意点

实际使用动态二维码时,有几个容易踩坑的地方,提前说明:

  • 输出类型必须是 gif:二维码内容本身是静态的,动的是背景图/前置图,最终输出必须走 gif 合成流程。如果只调用 asImg() 拿到的是第一帧的静态图;保存文件时文件名建议以 .gif 结尾,或者显式 setQrType(QrType.GIF)
  • 优先使用 setGif 加载 gif 资源:通过 setImg() 加载时,插件是依赖文件名后缀识别 gif 的,文件名不带 .gif 后缀时会被当成普通图片处理(只取一帧),动图效果会失效
  • 前置图 gif 与背景图 gif 同时存在时,只有背景 gif 生效:源码中对此有明确注释(QrCodeGenerateHelper#toGifImages),目前不支持两者同时为 gif 的场景,若同时设置,保存的将是背景 gif
  • 帧数越多生成越慢:gif 的每一帧都要重新合成一次二维码,帧数很大的 gif 生成耗时会明显上升,制作背景素材时建议控制一下帧数与尺寸
  • 容错级别建议用 H:动态二维码因为带了背景、logo 等干扰信息,扫码识别难度高于纯二维码,建议显式设置 setErrorCorrection(ErrorCorrectionLevel.H)
  • 生成后务必扫码验证:样式越复杂的动态二维码,可扫性越难保证,上线前请务必用手机实测

8. 小结

本篇主要介绍了动态二维码的生成姿势:借助 gif 格式的背景图/前置图,让静态二维码"动起来"。3.x 版本通过 setQrType(QrType.GIF) + QrResource#setGif 即可实现,2.x 版本则是 gif 背景图 + setPicType("gif") 的组合。

如果对输出样式有更高的要求,gif 背景图 + 图片码元(DrawStyle.IMAGE)+ logo 的组合基本可以覆盖市面上常见的动态二维码玩法,剩下的就靠美术同学提供足够好看的 gif 素材了。

Loading...