2019独角兽企业重金招聘Python工程师标准>>>
最近公司一个项目需要用到emoji表情,在使用的过程中出现了一些问题。一同事问我说服务器传输过来的是emoji表情在我们这边显示成了编码\ud83d\ude04。我说是转义问题,但是他折腾了半天硬是没搞懂,我叫他把代码发过来,然而服务器却在内网,无法链接到服务器。于是乎我就写了这篇文章。
emoji的起源
表情符号,来自日语词汇“絵文字”(假名为“えもじ”,读音即emoji),是一套起源于日本的12x12像素表情符号,由栗田穣崇(Shigetaka Kurit)创作,最早在日本网络及手机用户中流行。 自苹果公司发布的iOS 5输入法中加入了emoji后,这种表情符号开始席卷全球,目前emoji已被大多数现代计算机系统所兼容的Unicode编码采纳,普遍应用于各种手机短信和社交网络中。----来自互动百科
上面说到“emoji已被大多数现代计算机系统所兼容的Unicode编码采纳,普遍应用于各种手机短信和社交网络中。”,换句话来说就是还有少数的计算机系统和手机的emoji表情采用的不是Unicode编码。查阅资料得知在最初日本的三大电信公司NTT DoCoMo、au/KDDI和Softbank,实现的方式是以下几种
- NTT DoCoMo的i-mode系统电话系统中,绘文字的尺寸是12x12 像素,在传送时,一个图形有2个字节。Unicode编码为E63E到E757
- au/KDDI的emoji体系则是通过特别的IMG标签实现
- Softbank的emoji是用SI/SO escape sequence所编码的。
还有一点就是,在不同的操作系统上emoji表情也可能会不一样,例如?
编码 | Apple | Microsoft | Samsung | LG | |
---|---|---|---|---|---|
U+1F600 | |||||
编码 | HTC | Mozilla | Emoji One | ||
U+1F600 | |||||
编码 | emojidex | ||||
U+1F600 |
emoji的Unicode
通常查看的文档和资料中emoji的unicode编码都是Unicode编码呈现的,例如?的编码是:U+1F604
?的编码表:
表情 | Unicode | UTF-16 | UTF8 | SB Unicode |
---|---|---|---|---|
? | U+1F604 | 0xD83D 0xDE04 | 0xF0 0x9F 0x98 0x84 | E415 |
更多的编码请查询:http://punchdrunker.github.io/iOSEmoji/table_html/index.html
iOS开发中的emoji
在iOS系统中emoji是以编码的形式存在(在系统中应该有个编码值和图片的关系表),在输入emoji编码的时候系统会根据编码找到对应的图片进行显示。在iOS中的所有能输入文字的原生控件都支持emoji表情的显示,也就是说如果你使用的是Unicode可以在不做任何转换的前提下显示出来,当然前提是你的emoji编码是正确的。
在开发中输入emoji: control + command + Space 就会弹出emoji窗口
macEmoji.png
如果需要输入编码的话如下:
//在这里以?表情为例,?的Unicode编码为U+1F604,UTF-16编码为:\ud83d\ude04
NSString * emojiUnicode = @"\U0001F604";
NSLog(@"emojiUnicode:%@",emojiUnicode);
//如果直接输入\ud83d\ude04会报错,加了转义后不会报错,但是会输出字符串\ud83d\ude04,而不是?
NSString * emojiUTF16 = @"\\ud83d\\ude04";
NSLog(@"emojiUTF16:%@",emojiUTF16);
//转换
emojiUTF16 = [NSString stringWithCString:[emojiUTF16 cStringUsingEncoding:NSUTF8StringEncoding] encoding:NSNonLossyASCIIStringEncoding];
NSLog(@"emojiUnicode2:%@",emojiUTF16);
运行输出:
emojiUnicode:?
emojiUnicode1:\ud83d\ude04
emojiUnicode2:?
回到我同事那个问题,服务器传输过来的值如果是\ud83d\ude04
那么在用label或者其他系统控件显示时出现的应该是?
表情,但是他说出现的是\ud83d\ude04
,所以服务器传输过来的值应该是\\ud83d\\ude04
,那问题来了,难道每次都需要对字符串进行转换操作么?显然不应该,让服务端直接返回\ud83d\ude04
编码是比较好的选择。
iOS的emoji键盘开发
iOS本身是有自带键盘的,但是为了让用户有更好的体验往往会添加个emoji键盘。emoji键盘的样式很多种,在这里我们就以QQ的emoji键盘为例如图:
keyboard.png
从上图来看,去掉那些跟emoji关系不大的成分,就只剩下中间那块是需要我们做的,先分析一个键盘需要的功能:
- 1.显示
- 2.删除
- 3.输出
观察上图并结合功能发现UICollectionView满足我们的需求,当然UIScrollView也能实现,但是为了方便在这里选用的是UICollectionView。
我准备了一个emoji的plist文件,用来导入emoji排序和分组导入工程
emojisPlist.png
然后处理成[[Srting]]格式。(在这里使用的是Swift语言,因为最近闲着无聊用Swift仿QQ,所以直接用swift写了)
数据准备好了后就直接开始码界面。代码如下:
HHEmojiKeyboard.swift
let HHEmojiKeyboard_Column:CGFloat = 7
// MARK: - HHEmojiKeyboardDelegate 协议
protocol HHEmojiKeyboardDelegate:NSObjectProtocol {
/**
点击选择表情
- parameter emojiKeyboard: emoji键盘
- parameter emoji: emoji表情
*/
func emojiKeyboard(emojiKeyboard:HHEmojiKeyboard,didSelectEmoji emoji:String)
/**
点击删除按钮
- parameter emojiKeyboard: emoji键盘
*/
func emojiKeyboardDidSelectDelete(emojiKeyboard:HHEmojiKeyboard)
/**
翻页
- parameter emojiKeyboard: emoji键盘
- parameter pageIndex: 页面的下标从0开始
*/
func emojiKeyboard(emojiKeyboard:HHEmojiKeyboard,scrollDidTo pageIndex:Int)
}
class HHEmojiKeyboard: UICollectionView,UICollectionViewDelegate,UICollectionViewDataSource,UIScrollViewDelegate {
/// 数据源
var dataArr:[[String]]!
/// 是否显示删除按钮
var delete:Bool = false
/// 协议
weak var emojiKeyboardDelegate:HHEmojiKeyboardDelegate?
/// 每页emoji表情的总数,这里加上了删除按钮
var pageEmojiCount:Int!{
get{
/// 行数
let line = self.frame.size.height / (self.frame.width / HHEmojiKeyboard_Column)
/// 一页的表情数
let pageEmojiCount = Int(line) * Int(HHEmojiKeyboard_Column)
return pageEmojiCount
}
}
/**
构造方法
- parameter frame: 位置大小
- parameter layout: 布局
- parameter arr: [String]数据,需要处理
- parameter delete: 是否显示删除按钮
- returns: HHEmojiKeyboard实例
*/
init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout ,stringArr arr:[String]!,isShowDelete delete:Bool) {
super.init(frame: frame, collectionViewLayout: layout)
self.registerClass(HHEmojiKeyboardCell.self, forCellWithReuseIdentifier: "HHEmojiKeyboardCell")
self.registerClass(HHImageKeyboardCell.self, forCellWithReuseIdentifier: "HHImageKeyboardCell")
self.pagingEnabled = true
self.dataSource = self
self.delegate = self
self.showsHorizontalScrollIndicator = false
self.delete = delete
self.grouping(arr)
}
/**
构造方法
- parameter frame: 位置大小
- parameter layout: 布局
- parameter arr: 分组数据
- parameter delete: 是否显示删除按钮
- returns: HHEmojiKeyboard实例
*/
init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout ,groupingArr arr:[[String]]!,isShowDelete delete:Bool){
super.init(frame: frame, collectionViewLayout: layout)
self.registerClass(HHEmojiKeyboardCell.self, forCellWithReuseIdentifier: "HHEmojiKeyboardCell")
self.registerClass(HHImageKeyboardCell.self, forCellWithReuseIdentifier: "HHImageKeyboardCell")
self.pagingEnabled = true
self.dataSource = self
self.delegate = self
self.showsHorizontalScrollIndicator = false
self.delete = delete
self.dataArr = arr
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - UICollectionViewDataSource
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int{
return self.pageEmojiCount
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell{
if indexPath.row == self.pageEmojiCount - 1 && self.delete{
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("HHImageKeyboardCell", forIndexPath: indexPath) as! HHImageKeyboardCell
cell.imgView.image = UIImage(named: "aio_face_delete")
return cell
}else{
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("HHEmojiKeyboardCell", forIndexPath: indexPath) as! HHEmojiKeyboardCell
if self.dataArr[indexPath.section].count > indexPath.row {
cell.emojiLabel.text = self.dataArr[indexPath.section][indexPath.row]
}else{
cell.emojiLabel.text = ""
}
return cell
}
}
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int{
return self.dataArr.count
}
// MARK: - UICollectionViewDelegate
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath){
if let delegate = self.emojiKeyboardDelegate {
if let cell = collectionView.cellForItemAtIndexPath(indexPath){
if cell.isKindOfClass(HHEmojiKeyboardCell){
if let content = (cell as! HHEmojiKeyboardCell).emojiLabel.text {
if content.characters.count > 0{
delegate.emojiKeyboard(self, didSelectEmoji: content)
}
}
}else if cell.isKindOfClass(HHImageKeyboardCell){
delegate.emojiKeyboardDidSelectDelete(self)
}
}
}
}
// MARK: - UICollectionViewDelegateFlowLayout
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize{
return CGSizeMake(self.frame.size.width / HHEmojiKeyboard_Column, self.frame.size.width / HHEmojiKeyboard_Column)
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets{
return UIEdgeInsetsZero
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat{
return 0
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat{
return 0
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize{
return CGSizeZero
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize{
return CGSizeZero
}
// MARK: - UIScrollViewDelegate
func scrollViewDidScroll(scrollView: UIScrollView) {
var page = Int(scrollView.contentOffset.x / scrollView.frame.size.width)
if Int(scrollView.contentOffset.x)%Int(scrollView.frame.size.width) > 0 {
page += 1
}
if let delegate = self.emojiKeyboardDelegate {
delegate.emojiKeyboard(self, scrollDidTo: page)
}
}
// MARK: - 分组
func grouping(arr:[String]!){
var pageEmojiCount = self.pageEmojiCount
if self.delete {
pageEmojiCount = pageEmojiCount - 1
}
var pageNumber:Int = arr.count / pageEmojiCount
if arr.count%pageEmojiCount > 0 {
pageNumber += 1
}
self.dataArr = []
var emojis:[String] = []
for i in 0..= arr.count {
break
}
emojis.append(arr[i*pageEmojiCount+j])
}
self.dataArr.append(emojis)
}
}
}
HHEmojiKeyboardCell.swift
class HHEmojiKeyboardCell: UICollectionViewCell {
/// emoji表情标签
var emojiLabel:UILabel!
// MARK: - 父类方法
override init(frame: CGRect) {
super.init(frame: frame)
self.initUI()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.initUI()
}
override func updateConstraints() {
super.updateConstraints()
let size = self.frame.size
self.emojiLabel.font = UIFont.systemFontOfSize(size.width/2)
}
// MARK: - 初始化
/**
初始化UI
*/
func initUI(){
self.emojiLabel = UILabel()
self.emojiLabel.textAlignment = .Center
self.emojiLabel.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(self.emojiLabel)
let vflH = "H:|-0-[emojiLabel]-0-|"
let vflV = "V:|-0-[emojiLabel]-0-|"
let hLayout = NSLayoutConstraint.constraintsWithVisualFormat(vflH, options: NSLayoutFormatOptions.DirectionLeadingToTrailing, metrics: nil, views: ["emojiLabel" : self.emojiLabel])
let vLayout = NSLayoutConstraint.constraintsWithVisualFormat(vflV, options: NSLayoutFormatOptions.DirectionLeadingToTrailing, metrics: nil, views: ["emojiLabel" : self.emojiLabel])
self.addConstraints(hLayout)
self.addConstraints(vLayout)
}
}
HHImageKeyboardCell.swift
class HHImageKeyboardCell: UICollectionViewCell {
/// emoji表情标签
var imgView:UIImageView!
// MARK: - 父类方法
override init(frame: CGRect) {
super.init(frame: frame)
self.initUI()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.initUI()
}
// MARK: - 初始化
/**
初始化UI
*/
func initUI(){
self.imgView = UIImageView()
self.imgView.contentMode = UIViewContentMode.Center
self.imgView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(self.imgView)
let vflH = "H:|-0-[imgView]-0-|"
let vflV = "V:|-0-[imgView]-0-|"
let hLayout = NSLayoutConstraint.constraintsWithVisualFormat(vflH, options: NSLayoutFormatOptions.DirectionLeadingToTrailing, metrics: nil, views: ["imgView" : self.imgView])
let vLayout = NSLayoutConstraint.constraintsWithVisualFormat(vflV, options: NSLayoutFormatOptions.DirectionLeadingToTrailing, metrics: nil, views: ["imgView" : self.imgView])
self.addConstraints(hLayout)
self.addConstraints(vLayout)
}
}
HHEmojiManage.swift
import Foundation
class HHEmojiManage:NSObject {
class func getEmojiAll() -> NSDictionary{
if let path = NSBundle.mainBundle().pathForResource("EmojisList", ofType: "plist"){
if let dic = NSDictionary(contentsOfFile: path as String){
return dic
}
}
return NSDictionary()
}
}
ViewController.swift
class ViewController: UIViewController ,HHEmojiKeyboardDelegate{
var keyboard:HHEmojiKeyboard!
var textView:UITextView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let dic = HHEmojiManage.getEmojiAll()
var emojiArr:[String] = []
for arr in dic.allValues {
emojiArr = arr as! [String]
break
}
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .Horizontal
layout.sectionInset = UIEdgeInsetsZero
let width:CGFloat = self.view.frame.size.width
let frame = CGRectMake(0, self.view.frame.size.height - width*3/7, width, width*3/7)
self.keyboard = HHEmojiKeyboard(frame: frame, collectionViewLayout: layout, stringArr: emojiArr, isShowDelete: true)
self.keyboard.emojiKeyboardDelegate = self
self.keyboard.backgroundColor = UIColor.whiteColor()
self.view.addSubview(self.keyboard)
let textFiledFrame = CGRectMake(0, CGRectGetMinY(self.keyboard.frame) - 30, width, 30)
self.textView = UITextView(frame: textFiledFrame)
self.textView.backgroundColor = UIColor.grayColor()
self.view.addSubview(self.textView)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - HHEmojiKeyboardDelegate
func emojiKeyboard(emojiKeyboard:HHEmojiKeyboard,didSelectEmoji emoji:String){
self.textView.text?.appendContentsOf(emoji)
}
func emojiKeyboardDidSelectDelete(emojiKeyboard:HHEmojiKeyboard){
self.textView.deleteBackward()
}
func emojiKeyboard(emojiKeyboard: HHEmojiKeyboard, scrollDidTo pageIndex: Int) {
print(pageIndex)
}
}
运行效果如下:
run1.png
看上去好像没什么问题,但是当我翻到最后一页的时候发现效果不对。如图:
run2.png
所以问题来了,如何得到我们想要的效果。将emoji表情和删除图标换成cell的下标,得到下图:
emoji表情和删除图标换成cell的下标
发现UICollectionView竟然是竖着排的。我能想到的解决的方法有两种。
- 1.让UICollectionView横着排
-
2.挪动数据的位置,无数据的位置由长度为0的字符串补上
第一种的实现方法也简单,重写UICollectionViewFlowLayout的
func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]?
方法
代码如下:
HHCollectionViewFlowLayout.swift
import UIKit
class HHCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
if let attributes = super.layoutAttributesForElementsInRect(rect){
let attArr = NSArray(array: attributes, copyItems: true) as! [UICollectionViewLayoutAttributes]
for att in attArr {
let size = att.size
let x = CGFloat(att.indexPath.row%Int(HHEmojiKeyboard_Column))*size.width+CGFloat(att.indexPath.section) * self.collectionView!.frame.size.width
let y = CGFloat(att.indexPath.row/Int(HHEmojiKeyboard_Column))*size.height
att.frame.origin.x = x
att.frame.origin.y = y
}
return attArr
}
return nil
}
}
将ViewController中的UICollectionViewFlowLayout换成HHCollectionViewFlowLayout
运行效果如下:
run3.png
问题解决。
第二种的实现方法,则是挪动数据的位置,将数据看成矩阵,需要做的就是
将矩阵1
Matrix.png
转换成矩阵2
Matrix2.png
观察发现规律:
原来位置->目标位置 | 原来位置->目标位置 | 原来位置->目标位置 |
---|---|---|
0 -> 0 | 7 -> 1 | 14 -> 2 |
1 -> 3 | 8 -> 4 | 15 -> 5 |
2 -> 6 | 9 -> 7 | 16 -> 8 |
3 -> 9 | 10 -> 10 | 17 -> 11 |
4 -> 12 | 11 -> 13 | 18 -> 14 |
5 -> 15 | 12 -> 16 | 19 -> 17 |
6 ->18 | 13 -> 19 | 20 -> 20 |
从0开始,每次在前面的基础上加上3,如果加3后的值超出了最大值,择选取未使用过的最小值。而3则是行数。这样的每次计算前还得计算出行数,挺麻烦的。
将矩阵倒过来,由矩阵2转成矩阵1,观察发现规律:
原来位置->目标位置 | 原来位置->目标位置 | 原来位置->目标位置 | 原来位置->目标位置 | 原来位置->目标位置 | 原来位置->目标位置 | 原来位置->目标位置 |
---|---|---|---|---|---|---|
0 -> 0 | 3 -> 1 | 6 -> 2 | 9 -> 3 | 12 -> 4 | 15 -> 5 | 18 -> 6 |
1 -> 7 | 4 -> 8 | 7 -> 9 | 10 -> 10 | 13 -> 11 | 16 -> 12 | 19 -> 13 |
2 -> 14 | 5 -> 15 | 8 -> 16 | 11 -> 17 | 14 -> 18 | 17 -> 19 | 20 -> 20 |
从0开始,每次在前面的基础上加上7,如果加7后的值超出了最大值,择选取未使用过的最小值。7是列数这就方便很多了。具体代码如下:
/**
矩阵转换
- parameter arr: 原来矩阵
- parameter column: 列数
- returns: 装置矩阵
*/
func matrixTransformation(arr:[String],column:Int) -> [String] {
var matrix:[String] = []
var min = 1
var preNum = -column
for _ in 0..= arr.count {
num = min
min += 1
}
matrix.append(arr[num])
preNum = num
}
return matrix
}
运行后效果达到我们的预期。
附上Dome一份:https://github.com/MRCaoHH/HHEmojiKeyboard
参考资料:
[1] http://emojipedia.org
[2] http://www.baike.com/wiki/emoji
[3] https://zh.wikipedia.org/wiki/繪文字
[4] http://www.unicode.org/emoji/charts/full-emoji-list.html#1f600
[5] http://punchdrunker.github.io/iOSEmoji/table_html/index.html