微信图片气泡对话框实现

为了美化对话框,在对话框方方圆圆的对话框上加一个小尾巴,也就是气泡对话框。

文本气泡里装着文本,图片气泡里装着图片(😁 什么鬼),文本气泡大家应该都见过,这里就不演示了。先看例子 🌰🌰 图片气泡栗子

背景

很久以前为了实现气泡效果会考虑使用图片,现在更多的则是使用样式来实现,纯文本气泡对话框样式实现起来比较容易,网上教程很多。今天要说的是图片的气泡效果实现。下图为微信截图。此类气泡有个特点就是小尾巴(红圆圈)的背景是被图片填满的。

思路

实现思路分成两种:

  1. 遮罩掩盖
  2. 元素背景 inherit
  3. 类似于 overflow 裁剪

如果用第一种方式,遮罩的形状大概是如下图这样滴,但是要保证遮罩的颜色与背景颜色一致,灵活性上不够好。例如微信是支持聊天背景变换的,当变换了背景会导致和遮罩颜色不统一,遮罩就失效了。

第二种方法可以使用 CSS 较低成本的实现效果,例如生成一个伪元素,伪元素背景继承自父元素,会在一定程度上实现效果,但是在父子元素交接处会看出问题来。

如果用第三种方法,传统的 css 的 overflow 只支持规整的方形或圆形的 overflow,所以这么想的同学还是死了这条心吧。所以需要找个神奇的东东替代 overflow 的功能,并且可以任意形状 hidden。

实现方法

前几天在家闲来无事,研究了下 svg。我去,真是打开了一个新的大门啊。其中有一个元素叫 clipPath,可以实现路径的剪切。所以理论上任何形状都可以裁剪。实现气泡的话只需要画一个下图黑线框一样的图形,再把黑框之外的部分会 hidden 掉就可以了。不说了看代码吧。

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
//生成气泡类
function ImgBubble(opt) {
this.init(opt);
}

ImgBubble.prototype = {
constructor: ImgBubble,
opt: {
maxSize: 200
},
extend: extend,

init: function(options) {
this.options = this.extend({}, this.opt, options);
this.initImg();
var that = this;
this.on(
"load",
function() {
that.svgBubble = new SVGBubble({
src: that.options.content,
w: that.imgWidth,
h: that.imgHeight,
node: that.options.node,
type: that.options.type
});
}.bind(this)
);
},
//加载图片
initImg: function() {
var that = this;
this.img = new Image();
this.img.onload = function() {
var width = this.width;
var height = this.height;
var radio;
if (width >= height) {
radio = width / that.options.maxSize;
that.imgWidth = that.options.maxSize;
that.imgHeight = height / radio;
} else {
radio = height / that.options.maxSize;
that.imgHeight = that.options.maxSize;
that.imgWidth = width * radio;
}

that.trigger("load");
};

this.img.src = this.options.content;
}
};

//自定义事件
var Event = {
on: function(type, callback) {
this.event = this.event || {};
this.event[type] = this.event[type] || [];
this.event[type].push(callback);
},

trigger: function(type) {
var events = this.event[type];
var arg = Array.prototype.slice.call(arguments, 1);
var that = this;
events.forEach(function(e) {
if (typeof e === "function") {
e.apply(that, arg);
}
});
}
};

function extend(prev) {
var length = arguments.length,
next;
for (var i = 1; i < length; i++) {
next = arguments[i];
for (var k in next) {
if (next.hasOwnProperty(k)) {
prev[k] = next[k];
}
}
}
return prev;
}

extend(ImgBubble.prototype, Event);
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
//生成svg类
function SVGBubble(options) {
this.init(options);
}

SVGBubble.prototype = {
constructor: SVGBubble,
opt: {
rr: 10, //圆角半径
gap: 10, //要遮挡图片的宽度
h1: 40, //三角离最上的高度
tr: 10, //三角边
h: 133, //总高度
w: 200, //总宽度
D:
"M ${rr+gap} 0 Q ${gap} 0 ${gap} ${rr} L ${gap} ${h1} 0 ${h1+tr/2} ${gap} ${h1+tr} ${gap} ${h-rr} Q ${gap} ${h} ${gap+rr} ${h} L ${w-rr} ${h} Q ${w} ${h} ${w} ${h-rr} L ${w} ${rr} Q ${w} 0 ${w-rr} 0 L ${gap+rr} 0",
DD:
"M ${rr} 0 ${w-rr-gap} 0 Q ${w-gap} 0 ${w-gap} ${rr} L ${w-gap} ${h1} ${w} ${h1+tr/2} ${w-gap} ${h1+tr} ${w-gap} ${h-rr} Q ${w-gap} ${h} ${w-rr-gap} ${h} L ${rr} ${h} Q 0 ${h} 0 ${h-rr} L 0 ${rr} Q 0 0 ${rr} 0"
},
extend,
extend,
init: function(options) {
this.options = this.extend({}, this.opt, options);
this.options.id = options.id || this.id();
this.options.w = Math.round(this.options.w);
this.options.h = Math.round(this.options.h);
this.options.d = this.generateD();
this.node = this.options.node;
this.initNode();
},
html: (function() {
var reg = /\$\{([^\}]+)\}/g;
var html =
'<svg xmlns="http://www.w3.org/2000/svg">' +
"<defs>" +
'<clipPath id="${id}clip">' +
'<path d="${d}" class="${id}path" style="stroke: black; fill: none;" />' +
"</clipPath>" +
"</defs>" +
'<image xmlns:xlink="http://www.w3.org/1999/xlink" class="${id}-path-img" x="0" y="0" width="${w}" height="${h}" style="clip-path: url(#${id}clip);" >' +
"</image>" +
"</svg>";

return function(options) {
return html.replace(reg, function(a, b, c, d) {
return options[b];
});
};
})(),

//左右
getDTarget: function() {
return this.options.type == "left" ? this.options.D : this.options.DD;
},

generateD: function() {
var that = this;
var res = this.getDTarget().replace(/\$\{([^\}]+)\}/g, function(
a,
b,
c,
d
) {
var myFunction = new Function(
"w",
"rr",
"gap",
"h1",
"h",
"tr",
"return " + b
);
var res = myFunction(
that.options.w,
that.options.rr,
that.options.gap,
that.options.h1,
that.options.h,
that.options.tr
);
return res;
});
return res;
},

id: function() {
return +new Date();
},
initNode: function() {
var svgNodes = new DOMParser().parseFromString(
this.html(this.options),
"image/svg+xml"
);
var image = svgNodes.documentElement.childNodes[1];
image.setAttributeNS(
"http://www.w3.org/1999/xlink",
"href",
this.options.src
);
this.node.appendChild(
this.node.ownerDocument.importNode(svgNodes.documentElement, true)
);
}
};
1
2
3
4
5
6
7
8
var data = {
type: "left", //气泡方向
content: "./img/5243.jpg", //图片地址
node: document.querySelector(".svg-bubble") //要插入的目标节点
};

//实例化气泡类
new ImgBubble(data);

总结

用 svg 加脚本生成气泡有几个优点:

  1. 气泡大小可控
  2. 方向(左右)可控
  3. 气泡位置可细微调整

但是缺点也是有的啊,例如,目前还不能给气泡添加边框和阴影。这个后续再加吧。