edong 竟然用 360 软件在服务器上修复问题??!!!

公司为了方便,买了 edong 的 windows server 系统的服务器。

今天发现服务器访问不了外网,于是报了官方客服。过了一个小时,发现状态是处理完成,之后我登录上去,看到官方竟然是在服务器上安装 360 来修复问题,而且在进行全盘扫描。

这水平真是惊到我了。

而且问题并没有解决,还是连不了外网。

这么大一个公司的技术水平真是无法恭维。

edong 服务器 外网 修复7 条回复 • 2017-05-24 09:44:49 +08:00
binjoo 1
binjoo 2017-05-22 11:50:45 +08:00
简单粗暴,没毛病。
why1 2
why1 2017-05-22 12:46:04 +08:00 via Android
能修复?
jiangzhuo 3
jiangzhuo 2017-05-22 13:03:41 +08:00
装的是 360 企业版还是家庭版
chinafeng 4
chinafeng 2017-05-22 13:15:26 +08:00
传统 IDC 的售后技术水平一般也就那样…
leitwolf 5
leitwolf 2017-05-22 14:03:35 +08:00
@jiangzhuo 普通的

*近消息是:服务器被入侵,没法弄,叫我们备份数据,重装系统。。。

没那功能去弄这些了。
leitwolf 6
leitwolf 2017-05-22 14:03:50 +08:00
没那功夫
QQ2171775959 7
QQ2171775959 2017-05-24 09:44:49 +08:00
这个属于正常现象吧,毕竟你才给别人花费了多少钱,得到的售后的水平跟消费也是一样成比例的,当然也不排除一些技术大神做售后良心发现好给你做好各种优化及安全策略。

R710, DL380 G7 这代的二手服务器当作公司的生产环境服务器可用性靠谱吗?

全新的服务器太贵了,2、3 万一台买新的估计只能买到双 E5-26xx+8 ~ 16G 内存+两三个 600G SAS 硬盘这个配置吧,某宝上二手买的戴尔 R710,惠普 DL380G7, 配的是 2 颗 E5640,一台 72G 内存( 4G x 18 条插槽配满),一台 144G 内存( 8G x 18 条满配),硬盘 6 ~ 8 块 300G SAS 做的 RAID1。放家里开了 3 个多月吧,配 4G 内存那台出过 3,4 次内存多比特校验错误,CentOS7 直接死机前面板 LED 报警,其它没有出现过问题,无空调散热,就放着。。
现在正好创业,托管到机房 2U 都是 1 付 1 万左右起吧,杭州上海这边,小成本创业只能想着拉条电信静态 IP 的宽带接着服务器,或者国外直接弄个 VPS,自己的业务用国内 VPS 月付还是太贵。
到时候硬盘都做 RAID1,内存其实也可以做 Mirroring (相当于 RAID1 ),内存容量少一半,但还是够用。就是不知道夏天家里小区会不会停电。。服务器和宽带放到办公室的话工业用电有点贵啊,一台服务器一个月至少 250 度电吧。
用二手服务器也好几年了,小用用效果还可以,哪位有经验的能科普下吗,为什么全新的服务器那么贵。。

20 条回复    2017-07-05 07:05:53 +08:00

lan894734188
    1

lan894734188   2017-05-15 18:40:09 +08:00 via Android

不水洗的话都好说
rssf
    2

rssf   2017-05-15 18:43:09 +08:00 via iPhone

你用企业宽带跑网站,咋看都像是天坑
a251922581
    3

a251922581   2017-05-15 18:46:12 +08:00

@rssf 80 端口用不到,不是做网站业务,就一个 MySQL 的数据库服务开着。。
rssf
    4

rssf   2017-05-15 18:47:52 +08:00 via iPhone

@a251922581 你会被被人扫到崩溃的
julyclyde
    5

julyclyde   2017-05-15 22:21:26 +08:00

内存硬盘全换掉,剩下的可以用
julyclyde
    6

julyclyde   2017-05-15 22:22:00 +08:00

全新服务器带售后服务啊
thinkxen
    7

thinkxen   2017-05-15 22:30:44 +08:00

我有个客人买了几台 DELL C1300,就换了新硬盘,放到我们机房,快三年了都没出过一次问题~~~
fzinfz
    8

fzinfz   2017-05-15 22:37:49 +08:00 via iPad

一个数据库服务配双路 E5?
sunzen
    9

sunzen   2017-05-15 23:27:53 +08:00 via Android

@fzinfz 这个不是很正常吗? 我们公司都拿 200w 刀片跑上数据库
crazycen
    10

crazycen   2017-05-16 00:01:29 +08:00 via iPhone

多备份还是没问题的,我的 lab 环境就 3 台 380 g7 和一台存储

johnny23
    11

johnny23   2017-05-16 07:52:03 +08:00 via iPhone

我有 14 年机房退役机器 大多是 e5 v5 系列的 有人收么
geekzu
    12

geekzu   2017-05-16 08:14:57 +08:00

主要买的是售后服务
c0878
    13

c0878   2017-05-16 09:32:32 +08:00

我们 office 机房夏天风扇 24 小时吹着 散热做好就行 防尘什么就算了 还有定期备份数据 *好配个 UPS 突然断电太伤了 能顶 15 分钟让你正常关机就行
QQ2171775959
    14

QQ2171775959   2017-05-16 09:36:40 +08:00

买到好一点的成色的话,还是蛮不错,性价比都是没有问题,像我们这边的话,一般都可以保质保量的,包换的。关键是要找对卖家,售后要跟得上的话,就没有多大的问题。
wdk23411
    15

wdk23411   2017-05-16 09:41:16 +08:00

14 年退役的 E5 V5 ?
qq1242245799
    16

qq1242245799   2017-05-16 15:54:43 +08:00

放家里你是在玩火,直接找机房托管啊,创业这个成本省不了
miclinux
    17

miclinux   2017-06-11 23:14:12 +08:00

品牌服务器用料足够,但是电子产品买新不买旧,主要是新制程带来的功耗降低。

二手的话建议考虑 DEll R730 这个级别的,估计 2011 接口还能再更新一代 CPU。

loveminds
    18

loveminds   2017-06-12 08:30:50 +08:00

没啥问题,别说 E5v1,5520/5620 这一代都还有很多公司还没退役
yw9381
    19

yw9381   2017-07-05 07:05:32 +08:00 via Android

不知道你是哪里询的价
同样 dell r730 e5-2620v4 64g 内存 3t sas 单电。raid 卡供货商报价也不过 2w5 左右。两三个月前才问的
yw9381
    20

yw9381   2017-07-05 07:05:53 +08:00 via Android

cpu 是两路了。

求助: Dell FX2 刀片服务器 CMC 黄灯

刚一通电的时候是绿灯,之后就黄灯
在模块 Bios 里无论怎么设置 CMC 的 ip 地址,无论是固定的还是 DHCP,都无法 ping 到 CMC
第 1 条附言 · 2017-06-13 17:26:22 +08:00
问题已经查明
黄灯是因为只插了一个电源
CMC 连不通是因为必须使用千兆网线和交换机,而且必须是很好的网线才行……
cmc 黄灯 fx2 DHCP6 条回复 • 2017-06-13 17:26:48 +08:00
fzinfz 1
fzinfz 2017-06-12 19:52:56 +08:00
把服务器网线换到其他设备试下能 ping ?搞不好路由器 /防火墙做了 IP 隔离啥的
日志提示什么?服务器报错的话应该可以找售后了
ecloud 2
ecloud 2017-06-12 21:23:54 +08:00
@fzinfz 网确定是好用的,CMC 的日志去哪里看? Dell 的 CMC 模块竟然用公的 com 口,我这里的线全是公头,刚在淘宝上买了个母头。估计只有进入控制台才能看到日志吧。明天打算联系售后了……
fzinfz 3
fzinfz 2017-06-13 00:34:04 +08:00
你设置的可能不是 CMC IP,而是 IDRAC IP。通过 COM 口改 DHCP 前默认 192.168.0.120

http://www.dell.com/support/manuals/us/en/19/dell-cmc-v1.30-poweredge-fx2/CMCFX2FX2s13UG-v1/Checklist-to-set-up-chassis?guid=GUID-767EC114-FE22-477E-AD20-E3356DD53395&lang=en-us

The default CMC network configuration is Static with the CMC IP address 192.168.0.120. If you want to change the network configuration to DHCP, connect a serial cable to serial port on the CMC.

那个灯闪?确定是绿灯不是蓝灯?绿转黄 /橙可能电源问题,应该不太可能。

http://www.dell.com/support/manuals/us/en/19/poweredge-fx2/Servers_TSG/Power-supply-unit-indicator-codes?guid=GUID-CE5CD45A-2D9D-48E1-A675-6F618DB1EDD0&lang=en-us

多年没碰刀片了,差不多忘光了,建议仅供参考。
deamwork 4
deamwork 2017-06-13 08:33:02 +08:00 via Android
拔插一下 CMC,估计 CMC 坏
然后再看看灯号,如果还是黄色的估计就是 CMC 坏了
在保的话丢售后吧
ecloud 5
ecloud 2017-06-13 10:11:57 +08:00 via iPhone
@fzinfz 我确定以及肯定修改的是 CMC 的 ip
我的确只插了一个电源,如果说因为我少插了一个电源就不让用 CMC 的话,这是不是太矫情了……
ecloud 6
ecloud 2017-06-13 17:26:48 +08:00
@deamwork 问题已经查明
黄灯是因为只插了一个电源
CMC 连不通是因为必须使用千兆网线和交换机,而且必须是很好的网线才行……

是否有免费的 Windows Server 服务器流量统计工具?只要网卡流量

是否有免费的 Windows Server 服务器流量统计工具?只要网卡流量

之前在用宝塔流量统计,但昨天的都看不到(感觉是软件的 Bug,否则是有那样的项目的)

流量 Server Windows 网卡7 条回复 • 2017-08-19 20:01:45 +08:00
wevsty 1
wevsty 2017-06-11 23:06:47 +08:00
开始-控制面板-管理工具-性能
系统自带的这个东西就能统计了
orzfly 2
orzfly 2017-06-12 02:41:55 +08:00 via Android
https://codebox.net/pages/bitmeteros
maleigebi 3
maleigebi 2017-06-12 08:22:24 +08:00
GlassWire?
www.glasswire.com
wxlg1117 4
wxlg1117 2017-08-18 15:01:00 +08:00
推荐 networx,有月周日小时程序
https://www.softperfect.com/products/networx/scr_stat_daily.png
wkl17 5
wkl17 2017-08-19 15:17:11 +08:00
@wxlg1117 感谢分享,这就试试。貌似现在这种软件 免费的很少。以前用 PJ 版的 DuMeter,但是记得会经常崩溃,也很久没使用了
wkl17 6
wkl17 2017-08-19 15:20:26 +08:00
@orzfly 下载试试,感谢。

@maleigebi 这个貌似是收费的?看到有 3 个价格的
wxlg1117 7
wxlg1117 2017-08-19 20:01:45 +08:00 via Android
@wkl17 记着用这个软件 5.5.5 啊,*新的 6.0 以上的也已经收费了

一直困扰设计师多年的Android 单位 dp

轻松理解Android开发单位DP ,让设计与开发高度匹配,整合了网上各大资料汇总一个通俗易懂的。

一直困扰设计师多年的Android 单位 dp

设计这么多年了,相信很多设计师,一直被DP搞得晕头转向的,因为我们设计常用单位是PX ,设计的时候我们用这个单位,但是开发人员则用dp,如何保证设计的产品效果图,文字能让开发人员,准确还原,这里我们就不得不提sp

%title插图%num

Android 为什么引入dp?

是因为这个世界上存在着很多不同屏幕密度的手机,屏幕密度是什么?就是ppi,就是单位长度里的像素数量。

想象一下,如果这些手机的尺寸一样,屏幕密度相差很大,那么是不是说一个手机水平方向上像素很少,另一个手机水平方向上像素很多?那我们画同样pix数量的时候,它显示的长度不就会不一样了?

比如下图中的两个手机,同时设置2px长度的Button,在屏幕密度较高的手机里就会显示的比较小。

而同时设置的2dp长度的Button,在两个手机上显示的大小是一样的。

%title插图%num

Android常用单位:

1.屏幕尺寸in(英寸):指实际的物理尺寸,为屏幕对角线的测量。一般讲的大小单位都是英寸,比如iPhone5S的屏幕尺寸是4英寸。

%title插图%num

2.像素(pixel):屏幕上的点,基本原色素及其灰度的基本编码。

%title插图%num

3.分辨率(Resolution):是指屏幕上垂直方向和水平方向上的像素个数。(比如iPhone5S的分辨率是1136*640)

%title插图%num

4.dpi:dot per inch,就是每英寸的像素数,也叫做屏幕密度。这个值越大,屏幕就越清晰。

%title插图%num

5. dp=dip : device independent pixels(设备独立像素):一种基于屏幕密度的抽象单位。在每英寸160点的显示器上,1dp = 1px。

6. sp:Scale-independent pixels,它是安卓的字体单位,以160PPI屏幕为标准,当字体大小为 100%时,1sp=1px。

对ppi 的理解:

ppi= Pixels per inch,每英寸上的像素数数目,即 “像素密度“

屏幕尺寸in(英寸):指实际的物理尺寸,为屏幕对角线的测量。

而ppi的运算方式是:

PPI = √(长度像素数² + 宽度像素数²) / 屏幕对角线英寸数

举个简单的列子,iphone5的ppi是多少?ppi=√(1136px²+ 640px²)/4 in=326ppi(视网膜Retina屏)

为解决Android设备碎片化,引入一个概念dp,一种基于屏幕密度的抽象单位。

Android把屏幕密度分为了四个广义的大小:

低(120ppi)、中(160ppi)、高(240ppi)和超高(320ppi)

dp是(设备独立像素)不同设备有不同的显示效果,这个和设备硬件有关是虚拟像素,在不同的像素密度的设备上会自动适配。

在每英寸160ppi的显示器上,1dp= 1px。

关于dp与px 单位换算:

dp=dip:device independent pixels(设备独立像素)不同设备有不同的显示效果,这个和设备硬件有关,一般我们为了支持WVGA、HVGA和QVGA推荐使用这个,不依赖像素。

dp是一种与密度无关的像素单位,在每英寸160点的屏幕上,1dp=1px

dp和px的换算公式 :dp*ppi/160 =px

比如,在一个240ppi的屏幕里,比如1dp x 240/160 =1.5px。 即 1dp=1.5px,

mdpi(160ppi)  1dp = 1px ;  hdpi  (240ppi)1dp=1.5px;

Xhdpi(320ppi )1dp=2px;    xxhdpi(480ppi )1dp=3px;

%title插图%num

对于设计人员而已,我们要把dp转化为px,  只是需要利用公式:

px=dp*ppi/160

mdpi: 1.0 (baseline)    1dp=px

hdpi: 1.5              1dp=1.5px

xhdpi: 2.0              1dp=2px

xxhdpi3.0                     1dp=3px

把dp 转化为px  公式为:

dp乘以每个密度下面的系数比值

常见手机分辨率:

4:3

VGA       640*180(video graphics array)

QVGA    320*240(Quarter VGA)

HVGA    480*320(half-size VGA)

SVGA    800*600(Supper VGA)

5:3

WVGA  800*480(WideVGA)

 

16:9

FWVGA  854*480(full wide VGA)

HD     1920*1080(high definition)

QHD      960*540

720p     1280*720 标清

1080P     1920*1080  超清

Android 字体单位:

sp:Scale-independentpixels,它是安卓的字体单位,以160PPI屏幕为标准,当字体大小为 100%时,1sp=1px。

Android系统允许用户自定义文字尺寸大小(小、正常、大、超大等等),当文字尺寸是“正常”时1sp=1dp=0.00625英寸,而当文字尺寸是“大”或“超大”时,1sp>1dp=0.00625英寸。类似我们在windows里调整字体尺寸以后的效果——窗口大小不变,只有文字大小改变。

默认系统字体   英文:Roboto;中文:DroidSans Fallback  与微软雅黑很像

%title插图%num

Android L 常用字体边距规格对照表:

%title插图%num

AndroidL 常用字体边距规格对照表:参考资料:

%title插图%num

 

你真的懂Handler.postDelayed()的原理吗?

阅读之前先问大家一个问题:Handler.postDelayed()是先delay一定的时间,然后再放入messageQueue中,还是先直接放入MessageQueue中,然后在里面wait delay的时间?为什么?如果你不答不上来的话,那么此文值得你看看。
原文:

使用handler发送消息时有两种方式,post(Runnable r)post(Runnable r, long delayMillis)都是将指定Runnable(包装成PostMessage)加入到MessageQueue中,然后Looper不断从MessageQueue中读取Message进行处理。

然而我在使用的时候就一直有一个疑问,类似Looper这种「轮询」的工作方式,如果在每次读取时判断时间,是无论如何都会有误差的。但是在测试中发现Delay的误差并没有大于我使用System.out.println(System.currentTimeMillis())所产生的误差,几乎可以忽略不计,那么Android是怎么做到的呢?

Handler.postDelayed()的调用路径

一步一步跟一下Handler.postDelayed()的调用路径:

  1. Handler.postDelayed(Runnable r, long delayMillis)
  2. Handler.sendMessageDelayed(getPostMessage(r), delayMillis)
  3. Handler.sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis)
  4. Handler.enqueueMessage(queue, msg, uptimeMillis)
  5. MessageQueue.enqueueMessage(msg, uptimeMillis)

*后发现Handler没有自己处理Delay,而是交给了MessageQueue处理,我们继续跟进去看看MessageQueue又做了什么:

  1. msg.markInUse();
  2. msg.when = when;
  3. Message p = mMessages;
  4. boolean needWake;
  5. if (p == null || when == 0 || when < p.when) {
  6. // New head, wake up the event queue if blocked.
  7. msg.next = p;
  8. mMessages = msg;
  9. needWake = mBlocked;
  10. else {
  11. }

MessageQueue中组织Message的结构就是一个简单的单向链表,只保存了链表头部的引用(果然只是个Queue啊)。在enqueueMessage()的时候把应该执行的时间(上面Hanlder调用路径的第三步延迟已经加上了现有时间,所以叫when)设置到msg里面,并没有进行处理……WTF?

继续跟进去看看Looper是怎么读取MessageQueue的,在loop()方法内:

  1. for (;;) {
  2. Message msg = queue.next(); // might block
  3. if (msg == null) {
  4. // No message indicates that the message queue is quitting.
  5. return;
  6. }
  7. }

原来调用的是MessageQueue.next(),还贴心地注释了这个方法可能会阻塞,点进去看看:

  1. for (;;) {
  2. if (nextPollTimeoutMillis != 0) {
  3. Binder.flushPendingCommands();
  4. }
  5. nativePollOnce(ptr, nextPollTimeoutMillis);
  6. synchronized (this) {
  7. // Try to retrieve the next message. Return if found.
  8. final long now = SystemClock.uptimeMillis();
  9. Message prevMsg null;
  10. Message msg = mMessages;
  11. if (msg != null && msg.target == null) {
  12. // Stalled by a barrier. Find the next asynchronous message in the queue.
  13. do {
  14. prevMsg = msg;
  15. msg = msg.next;
  16. while (msg != null && !msg.isAsynchronous());
  17. }
  18. if (msg != null) {
  19. if (now < msg.when) {
  20. // Next message is not ready. Set a timeout to wake up when it is ready.
  21. nextPollTimeoutMillis = (int) Math.min(msg.when – now, Integer.MAX_VALUE);
  22. else {
  23. // Got a message.
  24. mBlocked false;
  25. if (prevMsg != null) {
  26. prevMsg.next = msg.next;
  27. else {
  28. mMessages = msg.next;
  29. }
  30. msg.next = null;
  31. if (DEBUG) Log.v(TAG, “Returning message: ” + msg);
  32. msg.markInUse();
  33. return msg;
  34. }
  35. else {
  36. // No more messages.
  37. nextPollTimeoutMillis 1;
  38. }
  39. }
  40. }

可以看到,在这个方法内,如果头部的这个Message是有延迟而且延迟时间没到的(now < msg.when),会计算一下时间(保存为变量nextPollTimeoutMillis),然后在循环开始的时候判断如果这个Message有延迟,就调用nativePollOnce(ptr, nextPollTimeoutMillis)进行阻塞。nativePollOnce()的作用类似与object.wait(),只不过是使用了Native的方法对这个线程精确时间的唤醒。

精确延时的问题到这里就算是基本解决了,不过我又产生了一个新的疑问:如果Message会阻塞MessageQueue的话,那么先postDelay10秒一个Runnable A,消息队列会一直阻塞,然后我再post一个Runnable B,B岂不是会等A执行完了再执行?正常使用时显然不是这样的,那么问题出在哪呢?

再来一步一步顺一下Looper、Handler、MessageQueue的调用执行逻辑,重新看到MessageQueue.enqueueMessage()的时候发现,似乎刚才遗漏了什么东西:

  1. msg.markInUse();
  2. msg.when = when;
  3. Message p = mMessages;
  4. boolean needWake;
  5. if (p == null || when == 0 || when < p.when) {
  6. // New head, wake up the event queue if blocked.
  7. msg.next = p;
  8. mMessages = msg;
  9. needWake = mBlocked;
  10. else {
  11. }
  12. // We can assume mPtr != 0 because mQuitting is false.
  13. if (needWake) {
  14. nativeWake(mPtr);
  15. }

这个needWake变量和nativeWake()方法似乎是唤醒线程啊?继续看看mBlocked是什么:

  1. Message next() {
  2. for (;;) {
  3. if (msg != null) {
  4. else {
  5. // Got a message.
  6. mBlocked false;
  7. }
  8. }
  9. if (pendingIdleHandlerCount <= 0) {
  10. // No idle handlers to run. Loop and wait some more.
  11. mBlocked true;
  12. continue;
  13. }
  14. }

就是这里了,在next()方法内部,如果有阻塞(没有消息了或者只有Delay的消息),会把mBlocked这个变量标记为true,在下一个Message进队时会判断这个message的位置,如果在队首就会调用nativeWake()方法唤醒线程!

现在整个调用流程就比较清晰了,以刚刚的问题为例:

  1. postDelay()一个10秒钟的Runnable A、消息进队,MessageQueue调用nativePollOnce()阻塞,Looper阻塞;
  2. 紧接着post()一个Runnable B、消息进队,判断现在A时间还没到、正在阻塞,把B插入消息队列的头部(A的前面),然后调用nativeWake()方法唤醒线程;
  3. MessageQueue.next()方法被唤醒后,重新开始读取消息链表,*个消息B无延时,直接返回给Looper;
  4. Looper处理完这个消息再次调用next()方法,MessageQueue继续读取消息链表,第二个消息A还没到时间,计算一下剩余时间(假如还剩9秒)继续调用nativePollOnce()阻塞;
  5. 直到阻塞时间到或者下一次有Message进队;

这样,基本上就能保证Handler.postDelayed()发布的消息能在相对精确的时间被传递给Looper进行处理而又不会阻塞队列了。

 

 

另外,这里在阅读原文的基础上添加一点思考内容:
MessageQueue会根据post delay的时间排序放入到链表中,链表头的时间小,尾部时间*大。因此能保证时间Delay*长的不会block住时间短的。当每次post message的时候会进入到MessageQueue的next()方法,会根据其delay时间和链表头的比较,如果更短则,放入链表头,并且看时间是否有delay,如果有,则block,等待时间到来唤醒执行,否则将唤醒立即执行。
所以handler.postDelay并不是先等待一定的时间再放入到MessageQueue中,而是直接进入MessageQueue,以MessageQueue的时间顺序排列和唤醒的方式结合实现的。使用后者的方式,我认为是集中式的统一管理了所有message,而如果像前者的话,有多少个delay message,则需要起多少个定时器。前者由于有了排序,而且保存的每个message的执行时间,因此只需一个定时器按顺序next即可。

解析 ViewTreeObserver 源码(二)

继上篇内容,本文介绍 ViewTreeObserver 的使用,以及体会其所涉及的观察者模式,期间会附带回顾一些基础知识。*后,我们简单聊一下 Android 的消息传递,附高清示意图,轻松捋清整个传递过程!

在开始下篇之前,有必要回顾一下上篇《解析 ViewTreeObserver 源码,体会观察者模式、Android消息传递(上)》提及的 ViewTreeObserver 的概念:

ViewTreeObserver 是被用来注册监听视图树的观察者,在视图树发生全局改变时将收到通知。这种全局事件包括但不限于:整个视图树的布局发生改变、在视图开始绘制之前、视图触摸模式改变时…

还没有看上篇,或者对上篇已经没印象的,建议先去看一下。

本篇内容较多,为节省篇幅,直接接着上篇继续讲。

#1. 一览 ViewTreeObserver 的大纲

先通过这部分来对类的构成进行粗略的认知,这样才能自如的应对后面的内容。本部分建议大家参考源码去看,这样会更直观、更容易理解,我参考的源码是 Android 6.0 的 SDK(api 23)。

查看类的大纲发现,该类看着挺复杂,但概括起来看就很简单了,下面我们按类别来一个个拿下。(windows 下 AS 查看类大纲的默认快捷键是 Ctrl + F12,大纲模式下还支持搜索以快速定位)
1.1 类的接口

ViewTreeObserver 通过接口回调的方式实现观察者模式,当接收到通知后,通过接口的回调方法告知程序相应的事件发生了。在 ViewTreeObserver 中,包含了 11 个接口,对应着11中观察事件,如下图:

这里写图片描述
这里写图片描述
1.2 类的方法

介绍完接口,下面总结一下 ViewTreeObserver 类的方法,大概分为以下四种类型。

添加监听:addOnXxxListener(OnXxxListener)
移除监听:removeOnXxxListener(OnXxxListener)
分发事件:dispatchOnXxx()
其他方法:checkIsAlive()、isAlive()方法等

“其他方法”在上篇差不多提过了,现在我们着重看前三类方法,下面简称 add、remove 和 dispatch 方法。

查看类可知,对于前面那张图所展示的每一个接口,都有与其对应的 add、remove、dispatch 方法。举个例子吧,以 OnGlobalLayoutListener(全局布局监听) 为例,那么与其对应的三类方法就是:

addOnGlobalLayoutListener(OnGlobalLayoutListener listener);
removeOnGlobalLayoutListener(OnGlobalLayoutListener victim);
dispatchOnGlobalLayout();

这么说,一共有11个接口,那么与之对应的 add、remove、dispatch 方法也就分别有11个,没错,我们通过大纲查看时就是这样。这个大家自行去类中查看,或者根据上面举的例子类推一下,我就不再贴代码了。

下面补充一点与方法的使用相关的内容:

虽说 ViewTreeObserver 包含这么多方法,但是系统并没有对我们开放所有的API。我们可以验证一下,在程序代码中先通过 getViewTreeObserver() 获取 View 的 ViewTreeObserver 对象,然后使用该对象分别调用这几类方法,分别模糊匹配 add、remove 和 dispatch,然后查看IDE的智能提示。

先看看调用 add 和 remove 方法:
这里写图片描述

这里写图片描述

如图所示,add 和 remove 方法只分别只有8个,并没有11个。其中remove中*后一个方法removeGloableOnLayoutListener已经过时了,在 API 16 取代它的方法是removeOnGloableLayoutListener。查看removeGloableOnLayoutListener方法可知,其直接调用了removeOnGloableLayoutListener方法,功能上没区别。区别在于名字,肯定是初期方法命名不合理,后来想改,但又不能直接修改或删除。所以,在一开始就设计好一些规范,并在开发过程中按照代码规范开发,是有多重要…

既然都是8个,那各自少掉的3个呢?进 ViewTreeObserver类一看,发现不让外部调用的是与OnWindowShownListener、OnComputeInternalInsetsListener、OnEnterAnimationCompleteListener接口对应的add、remove方法,这几个方法之所以在程序中无法访问,是因为被添加了 @hide标签,这是什么?

@hide 意味着被其标记的方法、类或者变量,在自动生成文档时,将不会出现在API文档中对开发者开放,但是系统可以调用,这就解释了为什么我们只能访问其中8个方法了。其中有些要求对版本有要求,例如添加或移除 OnWindowAttachListener,需要 API 18 以上,而我们一版在开发时会选择*低适配 Android 4.0,也即是 API 为 14,这样一来就无法使用。

其实,可以通过反射访问被 @hide 标记的域。但是不建议这么做,因为 Google 在添加该标记时解释道:

We are not yet ready to commit to this API and support it,so @hide。

既然没有准备好提交这个API并支持他,也就意味着 Google 可能会随时修改这些方法(虽然可能性很小),所以出于保险还是不要通过反射使用的好(个人观点)。

再来看看 dispatch 方法可用的有哪些:
这里写图片描述
喔,居然只有3个!查看 ViewTreeObserver 类,发现其余8个不可访问的方法没有声明修饰符,那就是默认的 default 类型。我们知道,default 修饰的方法只能在同一包内可见,ViewTreeObserver.java 在 android.view 包下,我们在程序中显然无法访问。

#2. 接口和方法的作用

为了保持内容的连贯和思路的清晰,在上一节只是介绍了 ViewTreeObserver 类的构成,并没有解释具体的作用。下面趁热打铁,看一下各自的作用。此处仍以 OnGlobalLayoutListener(全局布局监听) 接口对应的三个方法为例,其他接口的原理都一样,不再赘述。
2.1 OnGlobalLayoutListener 接口:

这里写图片描述

注释很精确的概括了其作用:当全局布局状态,或者视图树的子view可见性发生改变时,将调用该回调接口。

该接口包含了一个回调方法 onGlobalLayout(),我们在程序中就是通过覆写该方法,实现自己的逻辑,具体使用将在实战部分介绍。

##2.2 addOnGlobalLayoutListener 和 removeOnGlobalLayoutListener 方法

还是将这俩好基友放在一块介绍,我直接简称 add 和 remove 了。

在程序中,通过 add 方法添加一个对 view 布局发生改变的监听,传入 OnLayoutGlobalListener 接口对象,覆写接口的 onGlobalLayout() 方法,系统会将我们传入的 OnLayoutGlobalListener 存在集合中。
这里写图片描述

当通过 add 监听之后,我们需要在适当的时候通过 remove 方法移除该监听,防止多次调用。通常在覆写的 onGlobalLayout() 时方法中调用 remove 方法移除监听。
这里写图片描述
##2.3 dispatchOnGlobalLayout 方法

dispatch 方法一般都由系统调用,我们不需要去关心。在 dispatchOnGlobalLayout 方法中,会遍历存放 OnLayoutGlobalListener 对象的集合,然后调用 OnLayoutGlobalListener 对象的 onGlobalLayout() 方法,通知程序该事件发生了。
这里写图片描述

[注:上述代码中存放 OnGlobalLayoutListener 的集合 CopyOnWriteArray,值得了解一下,会让你受益匪浅。本打算讲的,但限于篇幅只好作罢,感兴趣的可以上网了解一下]
3.使用姿势(实战)

到目前为止,我们对 ViewTreeObserver 的认识仍停留在概念级别,终于等到了实战环节,验收自己学习成果的时刻到了。

##3.1 使用流程

我们还是先以 OnGlobalLayoutListener 为例介绍一下标准使用流程,这里需要结合上篇所学内容。

通过 View 对象的 getViewTreeObserver() 获取 ViewTreeObserver 对象。
检测 observer 是否可用。不可用的话再获取一次
定义 OnGlobalLayoutListener 接口对象 listener,并覆写 onGlobalLayout() 回调方法。如果只监听一次,记得在方法*后调用 observer.removeOnGlobalLayoutListener() 移除监听,避免重复调用。
observer.addOnGlobalLayoutListener(listener) ,至此完成对该 View 对象的全局布局监听。

附上一张不完整的流程图,使用在线工具 ProcessOn 画的,挺好用的,推荐给大家:
这里写图片描述

##3.2 实际使用

上面只是标准使用流程,实际开发中我们不会这么多约束,下面看两个实际的例子。值得注意的是,我们一直所说的 View,实际上指的是 View 及其子类,比如 LinearLayout、ImageView、TextView等。

① 在 onCreate() 中获取 View 的高度

在开发中,我们有时需要在 onCreate() 方法中拿到一个view(任何View的子类)的宽高,这时候我们直接通过 getWidth() 和 getHeight() 方法获取的值均为 0,因为真正的值要在 view 完成 onLayout() 之后才可以返回。这时,我们就可以借助 OnGlobalLayoutListener 监听 view 的布局改变,当 view 布局发生改变且完成 onLayout() 后,就会调用 dispatchOnGlobal() 通知我们,接下来就会走到回调方法 onGlobalLayout() 中去。

view.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
//1. do sth you want
width = view.getWidth();
height = view.getHeight;
Log.d(“OnGlobalLayoutListener”, “width:” + width + “,height:” + height);

//2. remove listener
// api 小于 16
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN){
//使用过时方法
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
// api >= 16
else {
//使用替换方法
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
}
});

代码已经写得很清楚了,下面再补充两点:

因为每次都是通过 getViewTreeObserver() 直接获取 View 当前的observer,所以就没再使用 isAlive() 判断。

在介绍 remove 方法时,提到 removeGlobalOnLayoutListener() 方法已经过时,取而代之的是 removeOnGlobalLayoutListener() 方法。后者是在 JELLY_BEAN 版本才引入的,对应的 api 是 16。由于我当前程序的 minSdkVersion 为 14,所以需要根据实际版本号分开处理。其实,在本例中,是不需要分开处理的,我们直接调用已过时的 removeGlobalOnLayoutListener() 方法即可,因为在前面分析过,二者仅仅是名字上的差别。但我之所以这么做,就是为了演示如何判断版本号并据此选择对应的方案。毕竟有些方法系统只提供了高版本的实现,之前的版本就没有对应的方法,此时我们就必须自己实现在低版本上的功能了。
除了 OnGlobalLayoutListener,我们还可以借助 OnPreDrawListener 实现上述功能。同时,OnPreDrawListener 还可以帮助我们实现 View 初始化加载时的动画效果。下面再举个例子,供大家参考以熟悉api,实际的开发中需要灵活运用。

② View 的初始化加载动画

直接上代码,在 onCreate() 方法中:
这里写图片描述

添加属性动画:
这里写图片描述

*终效果:
这里写图片描述

———————

解析 ViewTreeObserver 源码(一)

主要内容:ViewTreeObserver 是被用来注册监听视图树的观察者,在视图树发生全局改变时将收到通知。本文从 ViewTreeObserver 源码出发,带你剖析 ViewTreeObserver 的设计及使用,并间接体会观察者模式、Android消息传递机制在其中的使用。

这两天看代码看到了 ViewTreeObserver ,之前有接触过,但一直不太其到底在表达什么。这次既然又碰到了,那就干脆研究一下吧。于是我开始以关键字“ViewTreeObserver” Google 相关内容,结果发现找到的内容都差不多,更有甚者通篇直接翻译api,惊呆了!真的一点都不夸张,不相信的可以自己搜索试试看…

本系列分为上下两篇,本篇较为基础,主要介绍 ViewTreeObserver 的基础信息,以及说一下观察者模式,至于消息传递以及实战等更多内容将在下篇介绍。
1. 分开理解 ViewTreeObserver

ViewTreeObserver,拆开来理解就是 ViewTree 和 Observer。

ViewTree 就是我们常说的视图树,在Android中,所有视图都是由View及View的子类构成,ViewGroup 也是View的子类,它可以装载View或者ViewGroup,这样,一层层嵌套,就有了视图树的概念。给一张图感受一下:

视图树

视图树

Observer 即观察者的意思,其对应的观察者模式是软件设计模式的一种,在许多编程语言中被广泛运用。其核心在于:一个目标对象,我们称之为被观察者(Observable),管理着所有依附于它的观察者(Observers),当被观察者发生某种改变时(称为事件),被观察者调用对该事件感兴趣的观察者所提供的方法,主动通知观察者。

总结起来就是三要素:观察者,被观察者,事件,图示如下,图画的有点虚。

观察者和被观察者

观察者和被观察者

至于在 ViewTreeObserver 中,观察者模式是如何工作的,需要留到后面再说了,因为涉及到 View 的 measure、layout 和 draw 等过程。等不及的可以自己看一下,给点提示:看 ViewRootImpl 类。
2. ViewTreeObserver 概念

分别介绍 ViewTree 和 Observer 之后,我们就不难理解 ViewTreeObserver 的概念了。在这里,ViewTree 即为被观察者(也可以是单个View),ViewTreeObserver 就是观察者,ViewTreeObserver 对 ViewTree 注册监听(观察它),当 ViewTree 发生某种变化时,将调用 ViewTreeObserver 的相关方法,通知其这一改变。我们要做的就是覆写这一方法,添加自己的逻辑。

来看一下官方文档的注释说明:

ViewTreeObserver 是被用来注册监听视图树的观察者,在视图树发生全局改变时将收到通知。这种全局事件包括但不限于:整个视图树的布局发生改变、在视图开始绘制之前、视图触摸模式改变时…
3. 获取 ViewTreeObserver 对象

了解了 ViewTreeObserver 之后,接下来说说如何获取 ViewTreeObserver 对象。我想大多数人会想到 new ViewTreeObserver(),因为这个我们*擅长了。

但是,ViewTreeObserver 的构造方法明确声明了:This constructor should not be called。也就是我们没法调用,当我们尝试这么做时,IDE 会提示:

‘ViewTreeObserver()’ is not public in ‘android.view.ViewTreeObserver’.Cannot be accessed from outside package

根据 ViewTreeObserver 类的注释可知,我们只能通过 View 的 getViewTreeObserver() 方法获取 ViewTreeObserver 对象,那么我们看一下 getViewTreeObserver() 方法的源码:

getViewTreeObserver

getViewTreeObserver

先介绍一下方法中的 AttachInfo,官方文档如此注释说明:

A set of information given to a view when it is attached to its parent

意思是:当这个 View 被附着到父窗口时,将会获得这一组信息。说的好抽象啊,还是自己这组信息都有啥吧。

AttachInfo 是 View 类的内部类,找到这个类,浏览一遍,发现其含了一个 ViewTreeObserver 对象,以及该 View 所处的窗口(Window)、设备(Display)、根视图(rootView)等信息,信息量还是蛮大的,感兴趣的可以自己了解下,我们目前只关心这个 ViewTreeObserver 对象。

下面分析方法的逻辑。这个方法首先判断 View 所持有的 mAttachInfo ,当 mAttachInfo 不为空时,直接返回 mAttachInfo 中的 ViewTreeObserver 对象;否则去判断 View 类的 ViewTreeObserver 对象 mFloatingTreeObserver,若 mFloatingTreeObserver 为空,则创建该对象,并返回。

对于 mFloatingTreeObserver,官方注释为:

Special tree observer used when mAttachInfo is null.

很明显,这是一个特殊的视图树观察者对象,只有当 mAttachInfo 为空时才会被使用。

至此,我们成功的获取了 ViewTreeObserver 对象。
4. 确保 ViewTreeObserver 可用

在前面,我们成功获取了 ViewTreeObserver 对象,当我们使用该 对象调用 addOnXxxListener 方法监听 View 的某个状态时,该方法总是首先调用 checkIsAlive() 方法,检测 View 的 ViewTreeObserver 对象是否存活(可用)。

为什么要先去检测呢?官方文档给出的解释是:通过 View 的 getViewTreeObserver() 方法返回的 ViewTreeObserver ,在 View 的生命周期中不能保证始终有效。

既然我如此,那么通过源码我们看一下 checkIsAlive() 都做了什么:

checkIsAlive

checkIsAlive

逻辑还是很简单的:如果 View 的 ViewTreeObserver 对象不可用,将抛出 IllegalStateException (非法状态异常),并提示我们 重新调用 View 的 getViewTreeObserver() 方法获取对象。

但是,我们能不能在调用 addOnXxxListener 之前,能否检测当前 View 的 ViewTreeObserver 对象是否可用呢,总不能每次等异常了才发现要去重新获取把?答案是肯定的!

ViewTreeObserver 为我们提供了 isAlive() 方法,逻辑很简单,就一句代码“return mAlive”,mAlive 就是在 checkIsAlive() 方法中所判断的变量。该变量标记当前 View 的 ViewTreeObserver 对象是否可用。

*后,翻译官方文档的注释,来总结如何确保 ViewTreeObserver 可用:

当 ViewTreeObserver 不可用时,调用 isAlive() 方法以外的任何其他方法,都将抛出异常。所以,如果 View 对 ViewTreeObserver 持有长时间引用,那么其应该在调用 ViewTreeObserver 对象的任何其他方法之前,确保通过 isAlive() 的返回值检查 ViewTreeObserver 的状态是否可用。
5. 结尾

至此,有关 ViewTreeObserver 的基础只是介绍就到这儿了,相信你一定会觉得没看过瘾,因为本篇只是介绍了 ViewTreeObserver 的概念、如何获取View实例,以及如何避免 ViewTreeObserver 对象为空,我也觉的说的有点简单了。但是,当你发现这些网上都没有,需要靠自己读源码和注释去了解时,就不是那么简单了。

下篇预告:ViewTreeObserver 的 API 介绍、观察者模式是如何起作用的、View 涉及的消息传递机制 以及 ViewTreeObserver 的使用等,统统放出来。请耐心等候,这期间不妨自己去看看源码。

*后,感谢你耐心看完,看完之后有任何问题,欢迎随时交流。

Android CollapsingToolbarLayout使用介绍

我非常喜欢Material Design里折叠工具栏的效果,bilibili Android客户端视频详情页就是采用的这种设计。这篇文章的第二部分我们就通过简单的模仿bilibili视频详情页的实现来了解下CollapsingToolbarLayout的使用。文章的第三部分介绍了CollapsingToolbarLayout与TabLayout的组合使用。

有基础的朋友可以直接跳过*部分。

一、相关基础属性介绍

Android studio中有一个Activity模板叫ScrollingActivity,它实现的就是简单的可折叠工具栏,我们将此模板添加到项目中。

%title插图%num
ScrollingActivity.gif

ScrollingActivity的布局代码如下

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">

<android.support.design.widget.AppBarLayout
    android:id="@+id/app_bar"
    android:layout_width="match_parent"
    android:layout_height="@dimen/app_bar_height"
    android:fitsSystemWindows="true"
    android:theme="@style/AppTheme.AppBarOverlay">

    <android.support.design.widget.CollapsingToolbarLayout
        android:id="@+id/toolbar_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true"
        app:contentScrim="?attr/colorPrimary"
        app:layout_scrollFlags="scroll|exitUntilCollapsed">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            app:layout_collapseMode="pin"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

    </android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.NestedScrollView                    
        android:layout_width="match_parent"        
        android:layout_height="match_parent"              
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        >    
    <TextView        
            android:layout_width="wrap_content"        
            android:layout_height="wrap_content"        
            android:layout_margin="@dimen/text_margin"         
            android:text="@string/large_text" />     
</android.support.v4.widget.NestedScrollView>

<android.support.design.widget.FloatingActionButton
    android:id="@+id/fab"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="@dimen/fab_margin"
    android:src="@android:drawable/ic_dialog_email"
    app:layout_anchor="@id/app_bar"
    app:layout_anchorGravity="bottom|end" />

</android.support.design.widget.CoordinatorLayout>

AppBarLayout是一种支持响应滚动手势的app bar布局(比如工具栏滚出或滚入屏幕),CollapsingToolbarLayout则是专门用来实现子布局内不同元素响应滚动细节的布局。

与AppBarLayout组合的滚动布局(Recyclerview、NestedScrollView等)需要设置app:layout_behavior=”@string/appbar_scrolling_view_behavior”(上面代码中NestedScrollView控件所设置的)。没有设置的话,AppBarLayout将不会响应滚动布局的滚动事件。

CollapsingToolbarLayout和ScrollView一起使用会有滑动bug,注意要使用NestedScrollView来替代ScrollView。

AppBarLayout的子布局有5种滚动标识(就是上面代码CollapsingToolbarLayout中配置的app:layout_scrollFlags属性):

  1. scroll:将此布局和滚动时间关联。这个标识要设置在其他标识之前,没有这个标识则布局不会滚动且其他标识设置无效。
  2. enterAlways:任何向下滚动操作都会使此布局可见。这个标识通常被称为“快速返回”模式。
  3. enterAlwaysCollapsed:假设你定义了一个*小高度(minHeight)同时enterAlways也定义了,那么view将在到达这个*小高度的时候开始显示,并且从这个时候开始慢慢展开,当滚动到顶部的时候展开完。
  4. exitUntilCollapsed:当你定义了一个minHeight,此布局将在滚动到达这个*小高度的时候折叠。
  5. snap:当一个滚动事件结束,如果视图是部分可见的,那么它将被滚动到收缩或展开。例如,如果视图只有底部25%显示,它将折叠。相反,如果它的底部75%可见,那么它将完全展开。

CollapsingToolbarLayout可以通过app:contentScrim设置折叠时工具栏布局的颜色,通过app:statusBarScrim设置折叠时状态栏的颜色。默认contentScrim是colorPrimary的色值,statusBarScrim是colorPrimaryDark的色值。这个后面会用到。

CollapsingToolbarLayout的子布局有3种折叠模式(Toolbar中设置的app:layout_collapseMode)

  1. off:这个是默认属性,布局将正常显示,没有折叠的行为。
  2. pin:CollapsingToolbarLayout折叠后,此布局将固定在顶部。
  3. parallax:CollapsingToolbarLayout折叠时,此布局也会有视差折叠效果。

当CollapsingToolbarLayout的子布局设置了parallax模式时,我们还可以通过app:layout_collapseParallaxMultiplier设置视差滚动因子,值为:0~1。

FloatingActionButton这个控件通过app:layout_anchor这个设置锚定在了AppBarLayout下方。FloatingActionButton源码中有一个Behavior方法,当AppBarLayout收缩时,FloatingActionButton就会跟着做出相应变化。关于CoordinatorLayout和Behavior,我下一篇文章会和大家一起学习。

这一堆属性看着有点烦,大家可以新建一个ScrollingActivity模板去实验一下玩玩。

二、模仿bilibili客户端视频详情页

我们先对原界面分析一下。

%title插图%num
哔哩哔哩Android客户端视频详情页.gif

界面初始,CollapsingToolbarLayout是展开状态,显示的是视频封面。我们向上滚动界面,CollapsingToolbarLayout收缩。当AppBarLayout完全折叠的时候视频av号隐藏,显示出来一个小电视图标和“立即播放”,点击则使AppBarLayout完全展开,CollapsingToolbarLayout子布局由ImageView切换为视频弹幕播放器。

额…弹幕播放器…

B站很早就开源了一个弹幕引擎,还起了个狂拽酷炫吊炸天的名字叫“烈焰弹幕使 ”(一看就是二次元程序猿们的作品→_→),源码在github上,项目名叫DanmakuFlameMaster。

来我们先看修改完成的布局。

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">

<android.support.design.widget.AppBarLayout
    android:id="@+id/app_bar"
    android:layout_width="match_parent"
    android:layout_height="@dimen/app_bar_height"
    android:fitsSystemWindows="true"
    android:theme="@style/AppTheme.AppBarOverlay">

    <android.support.design.widget.CollapsingToolbarLayout
        android:id="@+id/toolbar_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:contentScrim="?attr/colorPrimary"
        app:statusBarScrim="@android:color/transparent"
        app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">

        <!--封面图片-->
        <ImageView
            android:id="@+id/imageview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="centerCrop"
            android:src="@drawable/diqiu"
            app:layout_collapseMode="parallax"
            app:layout_collapseParallaxMultiplier="0.7"
            android:fitsSystemWindows="true"/>

        <!--视频及弹幕控件-->
        <FrameLayout
            android:id="@+id/video_danmu"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_collapseMode="parallax"
            app:layout_collapseParallaxMultiplier="0.7"
            android:fitsSystemWindows="true"
            android:visibility="gone">
            <VideoView
                android:id="@+id/videoview"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />

            <!--哔哩哔哩开源的弹幕控件-->
            <master.flame.danmaku.ui.widget.DanmakuView
                android:id="@+id/danmaku"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />
        </FrameLayout>

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            app:layout_collapseMode="pin"
            app:popupTheme="@style/AppTheme.PopupOverlay" >

            <!--自定义带图片的立即播放按钮-->
            <android.support.v7.widget.ButtonBarLayout
                android:id="@+id/playButton"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"
                android:visibility="gone">
                <ImageView
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:layout_gravity="center_horizontal"
                    android:src="@mipmap/ic_play_circle_filled_white_48dp"/>

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:textColor="#ffffff"
                    android:text="立即播放"
                    android:layout_gravity="center_vertical"
                   />
            </android.support.v7.widget.ButtonBarLayout>

        </android.support.v7.widget.Toolbar>
    </android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>

<include layout="@layout/content_scrolling" />

<android.support.design.widget.FloatingActionButton
    android:id="@+id/fab"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="@dimen/fab_margin"
    android:src="@mipmap/ic_play_circle_filled_white_48dp"
    app:layout_anchor="@id/app_bar"
    app:layout_anchorGravity="bottom|end" />

</android.support.design.widget.CoordinatorLayout>

我把colorPrimary的色值修改成了B站的“少女粉”,播放的图标是从网上找的。

<color name="colorPrimary">#FA7199</color>

因为我们要实现沉浸式状态栏,所以就需要先把整个activity设置成状态栏透明模式。然后在布局文件中,把CollapsingToolbarLayout里要实现沉浸式的控件设置上android:fitsSystemWindows=”true”,如果没有设置,则子布局会位于状态栏下方,未延伸至状态栏。

布局并不算复杂,接下来先实现无弹幕播放时的功能,。

我们需要监听CollapsingToolbarLayout的折叠、展开状态。唉我去,官方并没有提供现成的方法(⊙_⊙?)。

查看源码,可以看到CollapsingToolbarLayout是通过实现AppBarLayout的OnOffsetChangedListener接口,根据AppBarLayout的偏移来实现子布局和title的视差移动以及ContentScrim和StatusBarScrim的显示。那么我们也可以通过调用AppBarLayout的addOnOffsetChangedListener方法监听AppBarLayout的位移,判断CollapsingToolbarLayout的状态。

先写一个枚举定义出CollapsingToolbarLayout展开、折叠、中间,这三种状态。

 private CollapsingToolbarLayoutState state;

 private enum CollapsingToolbarLayoutState {
    EXPANDED,
    COLLAPSED,
    INTERNEDIATE
}

接下来对AppBarLayout进行监听,判断CollapsingToolbarLayout的状态并实现相应的逻辑。

为了让大家对状态看着更直观,我在修改状态值的时候把title一起进行了修改。

使用CollapsingToolbarLayout的时候要注意,在完成CollapsingToolbarLayout设置之后再调用Toolbar的setTitle()等方法将没有效果,我们需要改为调用CollapsingToolbarLayout的setTitle()等方法来对工具栏进行修改。(具体原因各位亲去看下CollapsingToolbarLayout源码就知道了 ( ˙-˙ ) )

    AppBarLayout  app_bar=(AppBarLayout)findViewById(R.id.app_bar);
    app_bar.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
        @Override
        public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {

            if (verticalOffset == 0) {
                if (state != CollapsingToolbarLayoutState.EXPANDED) {
                    state = CollapsingToolbarLayoutState.EXPANDED;//修改状态标记为展开
                    collapsingToolbarLayout.setTitle("EXPANDED");//设置title为EXPANDED
                }
            } else if (Math.abs(verticalOffset) >= appBarLayout.getTotalScrollRange()) {
                if (state != CollapsingToolbarLayoutState.COLLAPSED) {
                    collapsingToolbarLayout.setTitle("");//设置title不显示
                    playButton.setVisibility(View.VISIBLE);//隐藏播放按钮
                    state = CollapsingToolbarLayoutState.COLLAPSED;//修改状态标记为折叠
                }
            } else {
                if (state != CollapsingToolbarLayoutState.INTERNEDIATE) {
                    if(state == CollapsingToolbarLayoutState.COLLAPSED){
                        playButton.setVisibility(View.GONE);//由折叠变为中间状态时隐藏播放按钮
                    }
                    collapsingToolbarLayout.setTitle("INTERNEDIATE");//设置title为INTERNEDIATE
                    state = CollapsingToolbarLayoutState.INTERNEDIATE;//修改状态标记为中间
                }
            }
        }
    });

然后对播放按钮设置监听,点击则调用AppBarLayout的setExpanded(true)方法使工具栏展开。

%title插图%num
CollapsingToolbarLayout状态监听演示.gif

哔哩哔哩客户端的title是固定不动的,可以调用CollapsingToolbarLayout的setTitleEnabled(false)方法实现。

视频播放时,调用 NestedScrollView的setNestedScrollingEnabled(false)方法可以使AppBarLayout不响应滚动事件。

细心的朋友可能发现了哔哩哔哩客户端为了避免视频封面图片颜色过浅影响状态栏信息的显示,加了一个渐变的不透明层。

实现渐变遮罩层很简单。先在res/drawable文件夹下新建了一个名为gradient的xml文件,其中代码如下:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">

    <gradient
        android:startColor="#33000000"
        android:endColor="#00000000"
        android:angle="270" />

</shape>

shape节点中,可以通过android:shape来设置形状,默认是矩形。gradient节点中angle的值270是从上到下,0是从左到右,90是从下到上。起始颜色#33000000是20%不透明度的黑色,#00000000表示全透明。

然后在CollapsingToolbarLayout里的ImageView代码下面加上一个自定义view,背景设置为上面的渐变效果。

<View
   android:layout_width="match_parent"
   android:layout_height="40dp"
   android:background="@drawable/gradient"
   android:fitsSystemWindows="true"
/>    

一般状态栏的高度大概在20dp左右,我为了让渐变效果比较自然,并且不过多影响图(mei)片(zi),把高度设置成了40dp。(状态栏能看清了,妹子脸也没黑,挺好 (๑• . •๑) )

%title插图%num
有无渐变遮罩层的对比.jpg

我省略了弹幕播放的相关实现,接下来只要在播放按钮监听中写出封面图片的隐藏、视频和弹幕弹幕控件的显示初始化及播放逻辑,在AppBarLayout的三种状态监听中根据是否视频在播放写出其他相应逻辑就好了,感兴趣的朋友可以下载哔哩哔哩的“烈焰弹幕使”源码DanmakuFlameMaster玩玩。

B站点击追番或投硬币后会出现一个类似Snackbar的提示控件,可以通过我上一篇文章没时间解释了,快使用Snackbar!——Android Snackbar花式使用指南来实现,欢迎感兴趣的朋友去看看。

%title插图%num
模仿哔哩哔哩视频详情页.gif

真的不是我懒得上代码了,真的…(基友:赶紧的,开黑了。 我:等等我,马上来!\(≧▽≦)/)

三.CollapsingToolbarLayout与TabLayout

CollapsingToolbarLayout与TabLayout组合使用的效果也不错。

%title插图%num
CollapsingToolbarLayout与TabLayout.gif

来看下CollapsingToolbarLayout里的代码

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">

<android.support.design.widget.AppBarLayout
    android:id="@+id/app_bar"
    android:layout_width="match_parent"
    android:layout_height="250dp"
    android:fitsSystemWindows="true"
    android:theme="@style/AppTheme.AppBarOverlay">

    <android.support.design.widget.CollapsingToolbarLayout
        android:id="@+id/toolbar_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:titleEnabled="false"
        android:fitsSystemWindows="true"
        app:contentScrim="@color/colorPrimary"
        app:statusBarScrim="@android:color/transparent"
        app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
        <ImageView
            android:id="@+id/imageview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="centerCrop"
            android:adjustViewBounds="true"
            app:layout_collapseMode="parallax"
            app:layout_collapseParallaxMultiplier="0.7"
            android:fitsSystemWindows="true"
            android:src="@drawable/girl2"/>
        <View
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:background="@drawable/gradient"
            android:fitsSystemWindows="true" />
        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="96dp"
            android:minHeight="?attr/actionBarSize"
            android:gravity="top"
            app:layout_collapseMode="pin"
            app:title="hello"
            app:popupTheme="@style/AppTheme.PopupOverlay"
            app:titleMarginTop="15dp"
            />
        <android.support.design.widget.TabLayout
            android:id="@+id/tablayout"
            android:layout_width="match_parent"
            android:layout_height="45dp"
            android:layout_gravity="bottom" />
    </android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>


<android.support.v4.view.ViewPager
    android:id="@+id/viewpage"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layout_behavior="@string/appbar_scrolling_view_behavior">
</android.support.v4.view.ViewPager>

  </android.support.design.widget.CoordinatorLayout>

TabLayout没有设置app:layout_collapseMode,在CollapsingToolbarLayout收缩时就不会消失。

CollapsingToolbarLayout收缩时的高度是Toolbar的高度,所以我们需要把Toolbar的高度增加,给TabLayout留出位置,这样收缩后TabLayout就不会和Toolbar重叠。

Toolbar的高度增加,title会相应下移。android:gravity=”top”方法使Toolbar的title位于Toolbar的上方,然后通过app:titleMarginTop调整下title距顶部高度,这样Toolbar就和原来显示的一样了。


CollapsingToolbarLayout还可以和Palette搭配使用,但是我感觉在实际使用中有些坑,因为CollapsingToolbarLayout中的图片不确定,Palette从图片中获取到的色彩很可能不是你想要的。

感兴趣的朋友可以自己查下Palette的用法。

就是这些。 []~( ̄▽ ̄)~*