使用Docker搭建VPN

若要申请VPS可使用这个DigtalOcean,会赠送你$10用于购买VPS。

通过SSH隧道翻墙

本地执行

1
ssh -D -N 7777 root@host

需要使用代理的地方,都设置proxy到 127.0.0.1:7777

浏览器可通过SwitchyOmega添加情景方式

代理协议使用SOCKS5,代理服务器地址127.0.0.1,代理端口7777

使用Docker

要说安装,最简洁明了方便当然是试用Docker

安装Docker, 可根据自己系统参考官网

1
apt install docker.io  # Ubuntu 16.04为例的

l2tp

1
docker pull fcojean/l2tp-ipsec-vpn-server

VERY IMPORTANT ! First, run this command on the Docker host to load the IPsec NETKEY kernel module:
sudo modprobe af_key

vim /etc/docker/vpn.env,添加内容

1
2
3
VPN_IPSEC_PSK=0819
VPN_USER_CREDENTIAL_LIST=[{"login":"skyler","password":"skyler"},{"login":"guest","password":"guest"}]
VPN_NETWORK_INTERFACE=eth0

启动服务

1
2
3
4
5
6
7
8
docker run \
--name l2tp-ipsec-vpn-server \
--env-file /etc/docker/vpn.env \
-p 500:500/udp \
-p 4500:4500/udp \
-v /lib/modules:/lib/modules:ro \
-d --privileged \
fcojean/l2tp-ipsec-vpn-server

check检测运行状态

1
docker exec -it l2tp-ipsec-vpn-server ipsec status

Shadowsocks

1
docker pull mritd/shadowsocks

启动,可修改18888端口及-m(加密方式)、-k(用户名)的参数

1
docker run -dt --name shadowsocks -p 18888:18888 mritd/shadowsocks -s "-s 0.0.0.0 -p 18888 -m rc4-md5 -k skyler --fast-open"

ss不使用Docker也很简单,查看Shadowsocks官网

附Terminal使用代理

vim ~/.zshrc或者~/.bashrc添加

1
2
3
4
 # proxy
alias setproxy="export ALL_PROXY=socks5://127.0.0.1:7777"
alias unsetproxy="unset ALL_PROXY"
alias ip="curl -i http://ip.cn"

1
2
3
4
source ~/.zshrc
setproxy # 设置代理
unsetproxy # 取消代理
ip # 可用此命令验证

Mac使用Iterm2的正确姿势

iTerm2设置光标跳动

Preferences > Profiles > Keys >

修改配置项

⌥← –> Send Escape Sequence + Esc+b

⌥→ –> Send Escape Sequence + Esc+f

自定义zsh提示命令

前提当然是试用zsh主题了
示例:

1
2
3
4
5
6
7
gmserver() {
ssh root@$1.$2.gengmei
}
_gmserver() {
_arguments '-s[sort output]' '1:first arg:(backend gaia ascle ascle-mock backend-mock flagship artemis)' ':next arg:(dev dev1 test pre hotfix hotfix1)'
}
compdef _gmserver gmserver

Python学习笔记

根据Soure安装指定版本Python

遇到问题,安装完之后没有Pip

需要执行

1
2
./configure --with-ensurepip=upgrade
make && make install

其中--with-ensurepip=upgrade 很重要

Python3 print中文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
In [1]: arr  = ['我是中文', '好的']

In [2]: print(arr)
['我是中文', '好的']

In [3]: import json

In [4]: astr = json.dumps(arr)

In [5]: print(astr)
["\u6211\u662f\u4e2d\u6587", "\u597d\u7684"]

In [6]: print(astr.encode('utf-8').decode('unicode-escape'))
["我是中文", "好的"]

In [7]: astr
Out[7]: '["\\u6211\\u662f\\u4e2d\\u6587", "\\u597d\\u7684"]'

git pre-commit.md

需要提前 pip install flake8

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env bash

if [ -n "$BYPASS" ]; then exit 0; fi

git diff --cached -- '*.py' | flake8 --max-line-length=119 --ignore=W391,E501,F401 --show-source --diff

if [ $? -gt 0 ]; then
echo
echo '--------'
echo 'Lint check failed.'
exit 1
fi

Django添加sql query log

在manage.py shell中

1
2
3
4
import logging
l = logging.getLogger('django.db.backends')
l.setLevel(logging.DEBUG)
l.addHandler(logging.StreamHandler())

在console中

settings.py中配置

1
2
3
4
5
6
7
8
9
10
11
LOGGING = {
...

'loggers': {
'django.db.backends': {
'handlers': ['console'], # Quiet by default!
'propagate': False,
'level': 'DEBUG',
},
}
}

Crontab定时任务表达式

格式

秒 分钟 小时 日 月 星期 年

1
2
3
4
5
6
7
8
字段名                 允许的值                        允许的特殊字符  
秒 0-59 , - * /
分 0-59 , - * /
小时 0-23 , - * /
日 1-31 , - * ? / L W C
月 1-12 or JAN-DEC , - * /
周几 1-7 or SUN-SAT , - * ? / L C #
年 (可选字段) empty, 1970-2099 , - * /

举例

1
2
3
4
5
6
7
8
9
*/5 * * * * ?  每隔5秒执行一次
0 */1 * * * ? 每隔1分钟执行一次
0 0 23 * * ? 每天23点执行一次
0 0 1 * * ? 每天凌晨1点执行一次:
0 0 1 1 * ? 每月1号凌晨1点执行一次
0 0 23 L * ? 每月最后一天23点执行一次
0 0 1 ? * L 每周星期天凌晨1点实行一次
0 26,29,33 * * * ? 在26分、29分、33分执行一次
0 0 0,13,18,21 * * ? 每天的0点、13点、18点、21点都执行一次

Month

一年中的几月:可以用0-11 或用字符串 “JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV and DEC” 表示

Day-of-Week

每周:数字1-7(1 = 星期日),或用字符口串“SUN, MON, TUE, WED, THU, FRI and SAT”

说明

1
2
3
4
5
6
7
8
* :代表整个时间段
“?”字符:表示不确定的值
“,”字符:指定数个值
“-”字符:指定一个值的范围
“/”字符:指定一个值的增加幅度。n/m表示从n开始,每次增加m
“L”字符:用在日表示一个月中的最后一天,用在周表示该月最后一个星期X
“W”字符:指定离给定日期最近的工作日(周一到周五)
“#”字符:表示该月第几个周X。6#3表示该月第3个周五

Linux命令awk

一、简介

awk是一种编程语言,用于在linux/unix下对文本和数据进行处理。数据可以来自标准输入、一个或多个文件,或其它命令的输出。它支持用户自定义函数和动态正则表达式等先进功能,是linux/unix下的一个强大编程工具。它在命令行中使用,但更多是作为脚本来使用。

awk的处理文本和数据的方式:它逐行扫描文件,从第一行到最后一行,寻找匹配的特定模式的行,并在这些行上进行你想要的操作。如果没有指定处理动作,则把匹配的行显示到标准输出(屏幕),如果没有指定模式,则所有被操作所指定的行都被处理。

awk分别代表其作者姓氏的第一个字母。因为它的作者是三个人,分别是Alfred Aho、Brian Kernighan、Peter Weinberger。

gawk是awk的GNU版本,它提供了Bell实验室和GNU的一些扩展。下面介绍的awk是以GUN的gawk为例的,在linux系统中已把awk链接到gawk,所以下面全部以awk进行介绍。

二、使用

awk [-F field-separator] 'commands' input-file(s)
其中,[-F域分隔符]是可选的,commands 是真正awk命令。 input-file(s) 是待处理的文件。
在awk中,文件的每一行中,由域分隔符分开的每一项称为一个域。通常,在不指名-F域分隔符的情况下,默认的域分隔符是空格。

三、模式

  1. 正则表达式:使用通配符的扩展集。
  2. 关系表达式:可以用下面运算符表中的关系运算符进行操作
  3. 可以是字符串或数字的比较,如$2>%1选择第二个字段比第一个字段长的行。
  4. 模式匹配表达式:用运算符~(匹配)和~!(不匹配)。
  5. 模式,模式:指定一个行的范围。该语法不能包括BEGIN和END模式。
  6. BEGIN:让用户指定在第一条输入记录被处理之前所发生的动作,通常可在这里设置全局变量。
  7. END:让用户在最后一条输入记录被读取之后发生的动作。

四、常量

常量名 含义
$n 当前记录的第n个字段,字段间由FS分隔
$0 完整的输入记录
NF 浏览记录的域的个数
NR 已读的记录数
ARGC 命令行参数个数
ARGV 命令行参数排列
ENVIRON 支持队列中系统环境变量的使用
FILENAME awk浏览的文件名
FNR 浏览文件的记录数
FS 设置输入域分隔符,等价于命令行 -F选项
OFS 输出域分隔符
ORS 输出记录分隔符
RS 控制记录分隔符

五、常用函数

1. 求和
1
cat data | awk '{sum+=$1} END {print "Sum = ", sum}'
2. 求平均
1
cat data | awk '{sum+=$1} END {print "Average = ", sum/NR}'
3. 求最大值
1
cat data | awk 'BEGIN {max = 0} {if ($1>max) max=$1 fi} END {print "Max=", max}'
4. 求最小值(min的初始值设置一个超大数即可)
1
cat data | awk 'BEGIN {min = 1999999} {if ($1<min) min=$1 fi} END {print "Min=", min}'
5. 去重
1
awk -F':' '!a[$1]++'
6. 输入统计结果
1
cat data | awk 'info[$0]=++a[$12] {} END {print "TotalCount = ", NR; for(i in info){print i, info[i]} }'

还可以结合 grep/uniq(去重)/sort(排序)等命令一起使用

六、Demo

当前目录下有三个log文件如下
log1.txt

1
2
3
4
2016-11-29 17:10:41,967 GET /api/web/conversation skyler
2016-11-29 17:10:41,967 GET /api/web/conversation hu
2016-11-29 17:10:41,967 GET /api/web/conversation hu
2016-11-29 17:12:12,434 GET /api/web/tag -

log2.txt

1
2
3
2016-11-29 17:08:19,794 GET /api/web/account/user huleping
2016-11-29 17:10:41,967 GET /api/web/conversation skyler
2016-11-29 17:08:19,794 GET /api/web/account/user skyler

log3.txt

1
2
3
2016-11-29 17:12:23,462 GET /api/web/conversation skyler
2016-11-29 17:12:30,459 GET /api/web/activity huleping
2016-11-29 17:12:30,474 GET /api/web/conversation huleping

查找出匹配结果相同的结果,然后根据用户id去重统计个数,并输出总数

1
2
3
4
5
6
7
8
➜ for i in $(seq 1 3); do grep "/api/web/conversation" log${i}.txt; done | awk '!a[$5]++ END {print "TotalCount = ", NR; for(i in a){print i, a[i] | "sort -r -n -k2";} }'
2016-11-29 17:10:41,967 GET /api/web/conversation skyler
2016-11-29 17:10:41,967 GET /api/web/conversation hu
2016-11-29 17:12:30,474 GET /api/web/conversation huleping
TotalCount = 6
skyler 3
hu 2
huleping 1


seq 用于产生从某个数到另外一个数之间的所有整数
sort用法说明
n是按照数字大小排序,-r是以相反顺序,-k是指定需要爱排序的栏位,-t指定栏位分隔符为冒号

Android Studio查看项目所有依赖的lib

添加task

1
2
3
4
5
6
7
dependencies {
...
}
//显示所有依赖包的存储路径以及版本
task showLibInfo << {
configurations.compile.each { println it }
}

然后在项目目录下执行task

1
./gradlew showLibInfo

输出结果就可以看到项目所有依赖的lib信息了

清除项目所有pyc文件

清除python项目所有pyc文件
用python去写一个项目,难免有时候需要处理清除pyc文件,于是写了个脚本,

可以传入一个文件夹参数,表示需要清理的文件夹
默认取当前执行的文件夹路径

脚本具体如下:

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
#!/usr/bin/env python
# coding=utf-8
# Date:2011-03-26

import os
import fnmatch
import sys


def clearpyc(root, patterns='*', single_level=False, yield_folders=False):
"""
root: 需要遍历的目录
patterns: 需要查找的文件,以;为分割的字符串
single_level: 是否只遍历单层目录,默认为否
yield_folders: 是否包含目录本身,默认为否
"""
patterns = patterns.split(';')
for path, subdirs, files in os.walk(root):
if yield_folders:
files.extend(subdirs)
files.sort()
for name in files:
for pattern in patterns:
if fnmatch.fnmatch(name, pattern.strip()): # 去除pattern两端的空格
yield os.path.join(path, name)
if single_level:
break


if __name__ == '__main__':
if 2 == len(sys.argv):
directory = os.path.join(os.getcwd(), sys.argv[1])
print("参数检查正确")
else:
directory = os.getcwd()
print("取当前目录")

print(directory)

for path in clearpyc(directory, '*.pyc'):
print(path)
os.remove(path)

为终端Terminal设置Shadowsocks代理

Shadowsocks是我们常用的代理工具,它使用socks5协议,而终端很多工具目前只支持http和https等协议,对socks5协议支持不够好,所以我们为终端设置shadowsocks的思路就是将socks协议转换成http协议,然后为终端设置即可。仔细想想也算是适配器模式的一种现实应用吧。

想要进行转换,需要借助工具,这里我们采用比较知名的polipo来实现。polipo是一个轻量级的缓存web代理程序。闲话休叙,让我们开始动手吧。

Shadowsocks的安装与使用参考链接,点击访问

以下我是在Mac上配置的方式,其他系统可模仿。

安装

1
brew install polipo  # brew没安装的同学自行安装

修改配置

设置每次登陆启动polipo

1
ln -sfv /usr/local/opt/polipo/*.plist ~/Library/LaunchAgents

修改文件/usr/local/opt/polipo/homebrew.mxcl.polipo.plist设置parentProxy
增加了 <string>socksParentProxy=localhost:1080</string>

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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>homebrew.mxcl.polipo</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>ProgramArguments</key>
<array>
<string>/usr/local/opt/polipo/bin/polipo</string>
<string>socksParentProxy=localhost:1080</string>
</array>
<!-- Set `ulimit -n 20480`. The default OS X limit is 256, that's
not enough for Polipo (displays 'too many files open' errors).
It seems like you have no reason to lower this limit
(and unlikely will want to raise it). -->
<key>SoftResourceLimits</key>
<dict>
<key>NumberOfFiles</key>
<integer>20480</integer>
</dict>
</dict>
</plist>

启动

1
2
launchctl unload ~/Library/LaunchAgents/homebrew.mxcl.polipo.plist
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.polipo.plis

验证

两个语句执行的结果,显示ip地址不一样

1
2
curl ip.gs
http_proxy=http://localhost:8123 curl ip.gs

使用

编辑~/.bash_profile设置别名, 编辑之后执行source ~/.bash_profile

1
alias use_ss="export http_proxy=http://localhost:8123 && export https_proxy=http://localhost:8123"

执行命令use_ss就可以使用了, 根据之下命令执行结果可验证

1
2
3
curl ip.gs
use_ss
curl ip.gs

本文编写参考

Android自定义小红点BadgeView

想全局统一个小红点样式,总是改了这个忘了其他的,而且小红点格式各样,总是满足不了自己的需求,所以心血来潮自己自定义View onDraw了一个。

可前往查看GitHub源码.

效果就是这样….

BadgeView_preview.gif

一定要记得在attrs.xml 项目中添加

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
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="BadgeView">
<attr name="iconSrc" format="reference"/>
<attr name="iconWidth" format="dimension"/>
<attr name="iconHeight" format="dimension"/>
<!--若是icon是正方形的,可直接设置这个参数-->
<attr name="iconSize" format="dimension"/>

<attr name="text" format="string"/>
<attr name="textSize" format="dimension"/>
<attr name="textColor" format="color"/>

<attr name="badgeNum" format="integer"/>
<!--是否显示数字, 为false时只显示小红点; 没有数字时,小红点的大小通过badgeSize设置-->
<attr name="showNum" format="boolean"/>
<attr name="badgeBackgroundColor" format="color"/>
<!--限制设置小红点的大小不能超过数字显示模式(代码中也做了限制); 显示在文字模式大小的左下角;-->
<!-- 不显示数字时, 小红点的大小, 不包括边线-->
<attr name="badgeRedSize" format="dimension"/>
<attr name="badgeNumSize" format="dimension"/>
<attr name="badgeNumColor" format="color"/>
<!--若小红点有边缘线,加上边缘线-->
<attr name="badgeBorderColor" format="color"/>
<attr name="badgeBorderWidth" format="dimension"/>
<!--badge相对于主体右上角的相对位置, 重叠的部分的大小; 可以设置负值-->
<!--默认是( badgeHeight/2 ), 正好覆盖一个角-->
<attr name="badgeBottom" format="dimension"/>
<attr name="badgeLeft" format="dimension"/>
<!-- 有些设计要求未读前面加"+", (至少我们设计师这么设计) 显示成 +1/+34/+99-->
<attr name="badgeNumPre" format="string"/>
</declare-styleable>
</resources>

主要代码如下:

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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
public class BadgeView extends View {

////可设置部分 start///////////////////////////////////////////////
// 主体部分的设置 icon
private int iconSrc;
private float iconWidth;
private float iconHeight;
// 没有icon 就是文字描述了; icon的优先级比text高
private String text;
private int textColor;
private float textSize;

// 未读数; 在显示的时候 未读数默认显示形式9/23/99+
private int badgeNum;
private int badgeBackgroundColor;
private int badgeNumColor;
private float badgeNumSize;
// 是否显示数字, 默认显示小红点
private boolean showNum;
// 不显示数字时, 小红点的大小, 不包括边线
private float badgeRedSize;
// 边线, 有些小红点外边有白边, 若是设置了宽度,则会添加边线; 边线算在Badge整个的大小当中
private float badgeBorderWidth;
private int badgeBorderColor;
// 有些设计要求未读前面加"+", (至少我们设计师这么设计) 显示成 +1/+34/+99
private String badgeNumPre;
// badge的左下角 相对于 text/icon 右上角的相对位置,
// 默认是( badgeHeight/2 ), 正好覆盖一个角
private float badgeBottom;
private float badgeLeft;
// 是否自己设置了
private boolean hasBadgeBottomAttr;
private boolean hasBadgeLeftAttr;

// view设置的padding
private float viewPaddingLeft;
private float viewPaddingTop;
private float viewPaddingRight;
private float viewPaddingBootom;
////可设置部分 end///////////////////////////////////////////////

// 小红点真实大小 比 文本 的margin(不包括白边)
private static final int BADGE_TEXT_MARGIN_LEFT = 10;
private static final int BADGE_TEXT_MARGIN_TOP = 6;
private static final int BADGE_TEXT_MARGIN_RIGHT = 10;
private static final int BADGE_TEXT_MARGIN_BOOTOM = 6;

// 可以设置padding
private static final int VIEW_PADDING = 0;

////以下是辅助变量///////////////////////////////////////////////
// 整个View的真实大小
private float viewHeight;
private float viewWidth;
// 内容所占的大小, 内容居中
private float viewMinHeight;
private float viewMinWidth;
// 小红点有向右突出部分,为保证主体部分水平居中, 需要设置两边的margin
private float mainMarginHorizontal;
// 小红点有向上突出部分,就算没有未读数,也需要预留出位置, 设置Top即可
private float mainMarginTop;
// 描述文字或者icon的宽高
private float mainWidth;
private float mainHeight;
// badge的整体宽高
private float badgeHeight;
private float badgeWidth;
// badgeNum/小红点 的真实宽高
private float badgeNumHeight;
private float badgeNumWidth;
// icon
private Bitmap iconBitmap;
// 未读数显示的文案; 未读数默认显示形式9/23/99+
private String showUneadText;

// 画笔
private Paint contentPaint;
private TextPaint textPaint;
private TextPaint badgeNumPaint;

public BadgeView(Context context) {
this(context, null);
}

public BadgeView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}

public BadgeView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}

private void init(Context context, AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.BadgeView);

iconSrc = array.getResourceId(R.styleable.BadgeView_iconSrc, 0);
float iconSize = array.getDimension(R.styleable.BadgeView_iconSize, dip2px(30));
iconWidth = array.getDimension(R.styleable.BadgeView_iconWidth, iconSize);
iconHeight = array.getDimension(R.styleable.BadgeView_iconHeight, iconSize);

text = array.getString(R.styleable.BadgeView_text);
if (TextUtils.isEmpty(text)) {
text = "Hello World";
}
textColor = array.getColor(R.styleable.BadgeView_textColor, Color.BLACK);
textSize = array.getDimension(R.styleable.BadgeView_textSize, sp2px(16));

badgeNum = array.getInteger(R.styleable.BadgeView_badgeNum, 0);
badgeBackgroundColor = array.getColor(R.styleable.BadgeView_badgeBackgroundColor, Color.rgb(0xFF, 0x76, 0x90));
badgeNumColor = array.getColor(R.styleable.BadgeView_badgeNumColor, Color.WHITE);
badgeNumSize = array.getDimension(R.styleable.BadgeView_badgeNumSize, sp2px(10));
badgeNumSize = array.getDimension(R.styleable.BadgeView_badgeNumSize, sp2px(10));
showNum = array.getBoolean(R.styleable.BadgeView_showNum, true);
badgeRedSize = array.getDimension(R.styleable.BadgeView_badgeRedSize, dip2px(8));
badgeBorderColor = array.getColor(R.styleable.BadgeView_badgeBorderColor, Color.WHITE);
badgeBorderWidth = array.getDimension(R.styleable.BadgeView_badgeBorderWidth, 0);
if (badgeBorderWidth < 0) {
badgeBorderWidth = 0;
}
badgeNumPre = array.getString(R.styleable.BadgeView_badgeNumPre);

// 初始化badgeNum的画笔
badgeNumPaint = new TextPaint();
badgeNumPaint.setAntiAlias(true);
badgeNumPaint.setColor(badgeNumColor);
badgeNumPaint.setTextSize(badgeNumSize);
badgeNumPaint.setTextAlign(Paint.Align.CENTER);
// 计算 未读数的高度
String minBadge = getUnreadText(0);
Rect minBadgeRect = new Rect();
badgeNumPaint.getTextBounds(minBadge, 0, minBadge.length(), minBadgeRect);
// 计算badge的高度
badgeNumHeight = minBadgeRect.height();
badgeHeight = badgeNumHeight + BADGE_TEXT_MARGIN_TOP + BADGE_TEXT_MARGIN_BOOTOM + badgeBorderWidth * 2;
// 限制设置小红点的大小不能超过数字显示模式; 显示在文字模式大小的左下角
if (badgeRedSize > badgeNumHeight + BADGE_TEXT_MARGIN_TOP + BADGE_TEXT_MARGIN_BOOTOM) {
badgeRedSize = badgeNumHeight + BADGE_TEXT_MARGIN_TOP + BADGE_TEXT_MARGIN_BOOTOM;
}
// 获取位置
hasBadgeBottomAttr = array.hasValue(R.styleable.BadgeView_badgeBottom);
hasBadgeLeftAttr = array.hasValue(R.styleable.BadgeView_badgeLeft);
badgeBottom = array.getDimension(R.styleable.BadgeView_badgeBottom, 0);
badgeLeft = array.getDimension(R.styleable.BadgeView_badgeLeft, 0);
//关闭清空TypedArray
array.recycle();

// 初始化主体文字描述的画笔
textPaint = new TextPaint();
textPaint.setAntiAlias(true);
textPaint.setColor(textColor);
textPaint.setTextSize(textSize);
textPaint.setTextAlign(Paint.Align.CENTER);

contentPaint = new Paint();
contentPaint.setAntiAlias(true);
}

public void setBadgeNum(int badgeNum) {
this.badgeNum = badgeNum;
}

public void setShowNum(boolean isShow) {
this.showNum = isShow;
}

public void setIconSrc(int res) {
this.iconSrc = res;
}

public void setBadgeLocation(float bottom, float left) {
this.badgeBottom = bottom;
this.badgeLeft = left;
hasBadgeBottomAttr = true;
hasBadgeLeftAttr = true;
}

/**
* 重新计算绘制这个View
*/
public void redraw() {
// 需要重新计算高宽,所以用这个
requestLayout();
// invalidate();
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

if (viewWidth != viewMinWidth || viewHeight != viewMinHeight) {
canvas.save();
// 若是设置的高宽大于所需要的高宽, 对画布进行操作
float paddingLeft = viewPaddingLeft + (viewWidth - viewPaddingLeft - viewPaddingRight - viewMinWidth) / 2;
float paddingTop = viewPaddingTop + (viewHeight -viewPaddingTop -viewPaddingBootom - viewMinHeight) / 2;
// 移动布局, 改变原点
canvas.translate(paddingLeft, paddingTop);
}

onDrawContent(canvas);

if (viewWidth != viewMinWidth || viewHeight != viewMinHeight) {
canvas.restore();
}

}

/**
* 绘制整个内容
* @param canvas
*/
private void onDrawContent(Canvas canvas) {
if (iconSrc != 0) {
// 画icon
canvas.drawBitmap(iconBitmap, mainMarginHorizontal + (mainWidth - iconWidth) / 2, mainMarginTop + (mainHeight - iconHeight) / 2, contentPaint);
} else {
// 写text, 文字是居中的
canvas.drawText(text, viewMinWidth / 2, viewMinHeight, textPaint);
}

if (badgeNum > 0) {
canvas.save();
// 移动布局, 改变原点
canvas.translate(viewMinWidth - badgeWidth, 0);

oDrawBadge(canvas);

canvas.restore();
}
}

private void oDrawBadge(Canvas canvas) {
// 若有小红点有边缘线, 画边缘线
if (badgeBorderWidth > 0) {
contentPaint.setStyle(Paint.Style.STROKE);
contentPaint.setColor(badgeBorderColor);
contentPaint.setStrokeWidth(badgeBorderWidth);
if (!showNum) {
// 不显示数字
canvas.drawCircle(badgeWidth / 2, badgeHeight - badgeRedSize / 2 - badgeBorderWidth, badgeRedSize / 2, contentPaint);
} else if (badgeWidth == badgeHeight) {
// 显示是字符串长度为1时, 为正圆
canvas.drawCircle(badgeWidth / 2, badgeHeight / 2, badgeWidth / 2, contentPaint);
} else {
// 椭圆
Path borderPath = new Path();
borderPath.addArc(new RectF(0, 0, badgeHeight, badgeHeight), 90, 180);
borderPath.lineTo(badgeWidth - badgeHeight / 2, 0);
borderPath.addArc(new RectF(badgeWidth - badgeHeight, 0, badgeWidth, badgeHeight), 270, 180);
borderPath.lineTo(badgeHeight / 2, badgeHeight);
canvas.drawPath(borderPath, contentPaint);
}
}

contentPaint.setColor(badgeBackgroundColor);
contentPaint.setStyle(Paint.Style.FILL);
if (showNum) {
// 绘制红色背景图
Path path = new Path();
path.addArc(new RectF(badgeBorderWidth, badgeBorderWidth, badgeHeight - badgeBorderWidth, badgeHeight - badgeBorderWidth), 90, 180);
path.lineTo(badgeWidth - badgeHeight / 2 + badgeBorderWidth, badgeBorderWidth);
path.addArc(new RectF(badgeWidth - badgeHeight + badgeBorderWidth, badgeBorderWidth, badgeWidth - badgeBorderWidth, badgeHeight - badgeBorderWidth), 270, 180);
path.lineTo(badgeHeight / 2 - badgeBorderWidth, badgeHeight - badgeBorderWidth);
canvas.drawPath(path, contentPaint);
// 写上数字
canvas.drawText(showUneadText, badgeWidth / 2, badgeHeight - BADGE_TEXT_MARGIN_BOOTOM - badgeBorderWidth, badgeNumPaint);

} else {
// 画实心圆
canvas.drawCircle(badgeRedSize / 2 + badgeBorderWidth, badgeHeight - badgeRedSize / 2 - badgeBorderWidth, badgeRedSize / 2, contentPaint);
}
}

private void intParams() {
// 初始化主体的一些数据
if (iconSrc != 0) {
mainHeight = iconHeight;
mainWidth = iconWidth;
if (iconBitmap == null) {
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), iconSrc);
// 缩放图片
int width = bitmap.getWidth();
int height = bitmap.getHeight();
// 保证icon的scaleType="fitCenter"
// 获取图片的长边
float length = width > height ? width : height;
// 获取外框的最小边
float size = iconWidth > iconHeight ? iconHeight : iconWidth;
// 让图片按照长边进行缩放
float scale = size / length;
Matrix matrix = new Matrix();
matrix.postScale(scale, scale);
iconBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true);
}
// 因为icon是fitCenter, 所以有真实大小
iconWidth = iconBitmap.getWidth();
iconHeight = iconBitmap.getHeight();
} else {
// 字符描述文字的大小
Rect descRect = new Rect();
textPaint.getTextBounds(text, 0, text.length(), descRect);
mainWidth = descRect.width();
mainHeight = descRect.height();
}

// 初始化Badge的数据
if (showNum) {
showUneadText = getUnreadText(badgeNum);
Rect badgeRect = new Rect();
badgeNumPaint.getTextBounds(showUneadText, 0, showUneadText.length(), badgeRect);

badgeNumWidth = badgeRect.width();

if (showUneadText.length() == 1) {
// 当长度为1的时候,显示正圆
badgeWidth = badgeHeight;
} else {
badgeWidth = badgeNumWidth + BADGE_TEXT_MARGIN_LEFT + BADGE_TEXT_MARGIN_RIGHT + badgeBorderWidth * 2;
}
} else {
badgeWidth = badgeRedSize + badgeBorderWidth * 2;
}
// badgeHeight在构造方法中初始化了, 全部使用数字模式的高度

// Badge位置设置的范围做一个限制
if (!hasBadgeLeftAttr || badgeLeft > mainWidth) {
badgeLeft = getBadgeDefaultLocation();
}
if (!hasBadgeBottomAttr || badgeBottom > mainHeight) {
badgeBottom = getBadgeDefaultLocation();
}

// 计算整体内容的大小
mainMarginHorizontal = badgeWidth - badgeLeft;
mainMarginTop = badgeHeight - badgeBottom;
viewMinWidth = mainWidth + mainMarginHorizontal * 2;
viewMinHeight = mainHeight + mainMarginTop;
}

/**
* 获取默认的位置
* @return
*/
private float getBadgeDefaultLocation() {
// 文字的时候默认往上些, 盖住文字了
return iconSrc != 0 ? (showNum ? badgeHeight / 2 : badgeRedSize / 2 + badgeBorderWidth) : badgeRedSize / 2 + badgeBorderWidth - 3;
}

/**
* 构造未读数显示的文本
* 1) 未读数默认显示形式9/23/99+
* 2) 有些设计要求未读前面加"+", (至少我们设计师这么设计) 显示成 +1/+34/+99, 取配置badgeNumPre
* @param unread
* @return
*/
private String getUnreadText(int unread) {
String text = String.valueOf(unread);
if (TextUtils.isEmpty(badgeNumPre)) {
if (unread > 99) {
text = "99+";
}
} else {
if (unread > 99) {
text = badgeNumPre + "99";
} else if (unread >= 0) {
text = badgeNumPre + unread;
}
}
return text;
}

private int dip2px(int dpValue) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, getResources().getDisplayMetrics());
}

private int sp2px(int spValue) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spValue, getResources().getDisplayMetrics());
}


/**
* android-自定义View解决wrap_content无效的问题
* see https://my.oschina.net/ccqy66/blog/616662
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 计算高宽
intParams();
viewPaddingLeft = getPaddingLeft();
viewPaddingTop = getPaddingTop();
viewPaddingRight = getPaddingRight();
viewPaddingBootom = getPaddingBottom();

int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

//Measure Width
if (widthMode == MeasureSpec.EXACTLY) {
// 设置的大小不能比内容还小
viewWidth = widthSize < viewMinWidth ? viewMinWidth : widthSize;
} else {
viewWidth = viewMinWidth;
}
viewWidth += viewPaddingLeft + viewPaddingRight;

//Measure Height
if (heightMode == MeasureSpec.EXACTLY) {
// 设置的大小不能比内容还小
viewHeight = heightSize < viewMinHeight ? viewMinHeight : heightSize;
} else {
viewHeight = viewMinHeight;
}
if (viewHeight < viewMinHeight + VIEW_PADDING * 2) {
viewHeight = viewMinHeight + VIEW_PADDING * 2;
}
viewHeight += viewPaddingTop + viewPaddingBootom;

//MUST CALL THIS
setMeasuredDimension((int) Math.ceil(viewWidth), (int) Math.ceil(viewHeight));
}
}