3.x动态二维码使用示例
前面几篇介绍的二维码,输出都是静态图(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()返回QrCodeV3OptionssetBg(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/4setFtX/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 / setBgY | FILL 模式下二维码在背景图上的绘制坐标(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 素材了。