关于Drawable的学习

本文已参与[新人创作礼]活动,一起开启掘金创作之路。

首先要搞清楚官方为什么要设计出Drawable,而且还允许我们进行自定义。

这个问题可以通过阅读View源码找到答案。View都有一个drawable属性。在draw(canvas)方法里面我们可以看到调用了drawable.draw(canvas)方法,可以看到将画布对象传了进去。谷哥官方也就是通过这种方式将绘制背景的操作单独提取了出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class View{
public void draw(Canvas canvas) {
drawBackground(canvas);
}
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
if (background == null)
return;
setBackgroundBounds();
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
}

所以单独设计一个Drawable的目的在我看来是为了让代码不杂揉在view里面。比如你要给view设置一个背景,改变view的形状。有了画布canvas我们就能干任何事情,drawable就是帮助我们在展示我们想要展示的东西之前帮我们做一些不是那么主要的事情。


上面讲了我对drawable的理解,现在就要实际使用一下他了。


在使用前有个我自认为很重要的知识点需要讲下,为什么在自定义Drawable里面调用invalidateSelf()方法没有效果出现?答:因为你的自定义Drawable没有继承Drawable.Callback接口,然后实现方法invalidateDrawable(Drawble who)。我们来看下View类。可以看到他继承了Drawable.Callback,然后实现了invalidateDrawable方法,在里面调用了invalidate()实现了页面刷新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class View implements Drawable.Callback{
@Override
public void invalidateDrawable(@NonNull Drawable drawable) {
if (verifyDrawable(drawable)) {
final Rect dirty = drawable.getDirtyBounds();
final int scrollX = mScrollX;
final int scrollY = mScrollY;

invalidate(dirty.left + scrollX, dirty.top + scrollY,
dirty.right + scrollX, dirty.bottom + scrollY);
rebuildOutline();
}
}
}

然后我们在View的setBackgroundDrawable方法找到他们给Drawable设置了回调:

1
2
3
void setBackgroundDrawable(Drawable background){
background.setCallback(this);
}

所以我们在写自定义Drawable的时候一定要继承Drawable.Callback接口,实现invalidateDrawable方法。套路如下:不然回调不到上层view方法从而调用不到invalidate方法。

1
2
3
4
5
6
7
@Override
public void invalidateDrawable(Drawable who) {
final Callback callback = getCallback();
if (callback != null) {
callback.invalidateDrawable(this);
}
}

关于图片的具体大小我们可以重写getIntrinsicWidth()和getIntrinsicHeight来设置图片大小值。默认是-1。


现在实现一个点击图片,图片渐变的效果。

绘制思路:对画布上面进行裁切,先绘制正常的图片,然后对画布进行下面裁切,绘制有蒙板的颜色。蒙板的颜色也就是设置colorFilter属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
override fun draw(canvas: Canvas) {
if (mDrawable == null) return
var rect = bounds
var curOffset = mPercent * rect.height()
canvas.save()
canvas.clipRect(rect.left.toFloat(),rect.top.toFloat(),rect.width().toFloat(),rect.height() - curOffset)
mDrawable.colorFilter = mDefaultColorFilter
mDrawable.draw(canvas)
canvas.restore()
canvas.save()
canvas.clipRect(rect.left.toFloat(), rect.height() - curOffset, rect.width().toFloat(), rect.height().toFloat());
mDrawable.colorFilter= mPercentColorFilter
mDrawable.draw(canvas)
canvas.restore()
}

总知关于drawable的自定义还需要多练习,我们现在知道了他其实就是帮我们在自定义view的时候把一些操作从view里面提取出来,防止view过于臃肿。基于这个思想你就可以自行改造了。

学习过程遇到的一个好的博客推荐


中途添加的知识点:

调用方法:view.setBackgroundResource(id)可能会触发requestlayout方法。在不知不觉的时候造成了性能浪费。

源码解析:

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
public void setBackgroundResource(@DrawableRes int resid) {
Drawable d = null;
if (resid != 0) {
d = mContext.getDrawable(resid);
}
setBackground(d);
}
public void setBackground(Drawable background) {
//noinspection deprecation
setBackgroundDrawable(background);
}

public void setBackgroundDrawable(Drawable background) {
if (background == mBackground) {
return;
}
boolean requestLayout = false;
if (background != null) {
if (mBackground == null
|| mBackground.getMinimumHeight() != background.getMinimumHeight()
|| mBackground.getMinimumWidth() != background.getMinimumWidth()) {
requestLayout = true;
}
mBackground = background;
background.setCallback(this);
if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {
mPrivateFlags &= ~PFLAG_SKIP_DRAW;
requestLayout = true;
}
} else {
mBackground = null
requestLayout = true;
}

if (requestLayout) {
requestLayout();
}
}

可以看到当我们设置的新图片和老图片的大小不相等的时候就会认为需要重新布局,所以解决办法就是我们可以手动设置图片的大小相等用来避免这个不必要出现的资源浪费。


新学到的知识:制作涟漪效果可以使用RippleDrawable.

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#FF0000" >

<item android:id="@android:id/mask"
android:drawable="@android:color/white" />

</ripple>

记录一次使用Glide不当导致的ui显示问题

手机需要显示一个长条的图片:

bg1@3x.png
Downsampler decodeFromWrappedStreams
手机:

手机 分辨率 density
oppo 1440 * 3216 640
小米 1440 * 3200 560

加载的图片:

  • 大小: 1125 * 147

**加载方式: **

1
2
3
4
5
6
7
<ImageView
android:id="@+id/ivFootBack2"
android:layout_width="match_parent"
android:layout_height="49dp"
/>

Glide.with(this@MainActivity).load(default).into(ivFootBack2)

效果:

手机 效果
oppo 手机有多宽图片就可以显示多宽
小米 图片宽度达不到铺满屏幕宽度,效果就是只显示了一断
错误:
captrue.png
正确:
captrue.png

你现在是不是也超级纳闷了,为什么会这样.

「Bitmap」

Mosquitto第一次使用

输入mosquitto -h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mosquitto version 2.0.11

mosquitto is an MQTT v5.0/v3.1.1/v3.1 broker.

Usage: mosquitto [-c config_file] [-d] [-h] [-p port]

-c : specify the broker config file.
-d : put the broker into the background after starting.
-h : display this help.
-p : start the broker listening on the specified port.
Not recommended in conjunction with the -c option.
-v : verbose mode - enable all logging types. This overrides
any logging options given in the config file.

See https://mosquitto.org/ for more information
  • -c表示指定mosquitto的配置文件(默认是:/etc/mosquitto/mosquitto.conf)
  • -d表示启动一个后台进程(也就是守护进程)来运行我们的服务器
  • -p指定端口号

启动服务器的方法:

1
mosquitto -c /etc/mosquitto/mosquitto.conf -p 1883

1
service mosquitto start

这两个命令是相同的。因为默认的配置文件就是/etc/mosquitto/mosquitto.conf,默认的端口号就是1883。

我们想看有没有启动成功可以执行:

1
2
3
la@lll:~$ ps -aux | grep mosquitto
mosquit+ 16064 0.0 0.0 15772 8120 ? Ss 15:05 0:00 /usr/sbin/mosquitto -c /etc/mosquitto/mosquitto.conf
la@lll:~$

杀进程 kill -9 16064。br>

如果我们的服务器没有启动执行命令mosquitto_sub -t "test"会报错:

1
2
3
la@lll:~$ mosquitto_sub -t "test"
Error: Connection refused
la@lll:~$

ActiveMq服务器的部署:

  1. 官方下载:https://activemq.apache.org
  2. 启动 ./activemq start
  3. 查看是否启动:
    1
    2
    3
    ps -ef|grep java |grep -v grep 
    netstat -anp|grep 61616
    lsof -i:61616
    启动后的网址:http://localhost:8161/

有的时候启动失败了,日至会保存在:apache-activemq-5.17.2/data/activemq.log。报错信息里面如果讲的是端口占用,那我们可以进入apache-activemq-5.17.2/conf/activemq.xml修改端口号。

Linux基础知识学习

出现post-installation脚本返回错误的问题。

可以手动进入目录文件:/var/lib/dpkg/status 。打开这个文件(sudo gedit /var/lib/dpkg/status)然后找到出现包问题的那行,全部删除掉。然后保存文件。然后在执行sudo apt autoremove方法可以发现问题解决。

解决依赖 Ubuntu出现依赖关系问题 - 仍未被配置问题_Song的学习记录-CSDN博客_ubuntu依赖关系](https://blog.csdn.net/s306205127/article/details/78546484)")

进行dpkg安装的时候如果出现缺少依赖包的报错,那么就执行代码来自动安装缺少的包

sudo apt-get -f install 。 并且执行完这个方法,系统会自动帮我们完成安装过程,不需要再次使用dpkg进行安装。

如果apt的版本在1.1以上 可以执行使用apt安装 .deb文件。

sudo apt install ./包

安装软件包

1
2
3
下列软件包有为满足的依赖关系:
* : 依赖:python3-apt 但是它将不会被安装
E: 无法修正错误,因为您要求某些软件包保持现状,就是它们破坏了软件包间的依赖关系。

这个原因是因为python3他需要依赖包libpython3-stdlib 3.6.5。但是在我安装python之前已经安装了他的高版本libpython3-stdlib 3.7.7。所以就没法安装了。这就是包依赖问题。不能用apt-get -f install 来解决了。

需要用aptitude:

首先下载aptitude:sudo apt-get install apitude

sudo aptitude -f install

[解决依赖包版本不统一导致的问题](“Ubuntu解决依赖关系问题的正确姿势 - 简书 (jianshu.com)“)

安装Typora

  1. 进入官网typora.io。下载他的二进制文件

  2. 执行命令:tar -xzvf Typora.tar.gz

  3. 创建桌面图标 touch Typora.desktop

    填入内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    [Desktop Entry]
    Name=Typora
    GenericName=Editorubuntu / linux
    Comment=Typroa - a markdown editor
    Exec="Typora的绝对路径" %U
    Icon=图标的绝对路径
    Terminal=false
    Categories=Markdown;ux
    StartupNotify=false
    Type=Application
  4. 如果想在那个啥里面也可以看到他可以把Typora.desktop文件移动到 /usr/share/applications 里面。

Android字节码插桩

实现这个功能需要自定义Transform。这个Transform有一个很重要的点,它只会在所有java类变成class文件后才会执行。也因为这样,我们才可以对class文件进行插桩处理。

对字节码进行处理的开源工具这里使用的ASM。

1
2
3
4
5
implementation 'org.ow2.asm:asm:8.0.1'
implementation 'org.ow2.asm:asm-commons:8.0.1'
/或者
implementation gradleApi()
implementation 'com.android.tools.build:gradle:3.4.1'

—————–待续~

Java指Class的学习

[Class文件结构](“Class文件结构 - wade&luffy - 博客园 (cnblogs.com)“)

用二进制软件打开我们的class文件,可以看到这个class文件的二进制文件内容(在这个软件里面他是将二进制数据转化成了十六进制,这是为什么呢因为一个一个字节占用8位,那这8位数据我们可以拆成4和4,然后一个十六进制数用二进制表示要用4个数,所以正好两个十六进制就能表示一个字节,这样可以方便人们阅读)。他的前四个字节代表这个文件的魔术,用来描述这个文件格式信息,比如class文件的魔术是CAFEBABF。然后接着后4个字节代表当前jdk的版本信息。然后接着两位代表常量池常量数量。

img-0

0014对应的10进制就是20。也就代表当前的这个class文件的常量池里面有20个常量。这个我们通过javap命令可以证明:执行命令 javap -v **.class

img-0

这一看到有19个常量+一个特殊常量(就是0这个方便jvm虚拟机来索引用) = 20个常量。

常量池中存放着各种类型的常量,都有自己的类型,并且有自己的存储规范,比如:

  • 字符串常量:以01开头(占一个字节),接着用2个字节表示字符长度,接着就是字符内容。

    比如01 0002 6869 ;0002代表长度为2。所以往后数两位就是字符内容。

  • fieldref类型:以09开头,接着2个字节代表常量所在的class 在两位代表该常量名和类型

  • class类型:以07开头,接着两个字节代表指向utf-8的所引。

img-0img-0img-0
img-0

class文件里面哪些是常量?

上面这张图已经表示了全部的常量类型。一共有12种。

  1. 字面量:比较类似java语言层面的常量概念,比如文本字符串,被声明为final的常量值
  2. 符号引用:属于编译原理方面的概念,包括:
    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符

Esp8266

NodeMCU

是一款开源的物联网开发平台。链接 NodeMCU学习(二)–NodeMCU介绍及使用(一)_zwb_578209160的博客-CSDN博客_nodemcu](https://blog.csdn.net/zwb_578209160/article/details/113122094?ops_request_misc=%7B%22request%5Fid%22%3A%22164117591916781683975894%22%2C%22scm%22%3A%2220140713.130102334..%22%7D&request_id=164117591916781683975894&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_click~default-1-113122094.first_rank_v2_pc_rank_v29&utm_term=NodeMCU&spm=1018.2226.3001.4187))

img-0
img-0

使用教程:NodeMCU-AT篇,教你如何刷入AT模块 物联网NodeMCU-AT篇(一) 刷写AT固件_凌顺实验室-CSDN博客_at固件是什么](https://blog.csdn.net/ling3ye/article/details/80221132))

Android触摸事件

在阅读Android触摸事件处理的源码的时候看到了这么一个方法:event.getPointerIdBits()。这个方法是不对外暴露的。但是我们可以看到他的源码:

1
2
3
4
5
6
7
8
public final int getPointerIdBits() {
int idBits = 0;
final int pointerCount = nativeGetPointerCount(mNativePtr);
for (int i = 0; i < pointerCount; i++) {
idBits |= 1 << nativeGetPointerId(mNativePtr, i);
}
return idBits;
}

可以看到他先是调用了nativeGetPointerCount()这个C方法获取到当前手指头触摸在屏幕上的总数。想获取手指头触摸在屏幕的总数我们可以通过方法:event.getPointerCount()这是官方暴露给我们的方法,他的源码就是调用了方法nativeGetPointerCount()

1
2
3
public final int getPointerCount() {
return nativeGetPointerCount(mNativePtr);
}

继续回到方法getPointerIdBits,可以看到里面调用了方法nativeGetPointerId()。这个方法就是获取到指定位置的手指id。这个方法官方也提供了一个公开方法让我们调用event.getPointerId(i)

1
2
3
public final int getPointerId(int pointerIndex) {
return nativeGetPointerId(mNativePtr, pointerIndex);
}

所以这么一拆分,我们可以将方法getPointerIdBits拆成这么写:

1
2
3
4
5
6
7
8
public final int getPointerIdBits() {
int idBits = 0;
final int pointerCount = getPointerCount();
for (int i = 0; i < pointerCount; i++) {
idBits |= 1 << getPointerId(i);
}
return idBits;
}

这么一看我们似乎明白了getPointerIdBits()是干嘛的了。我们可以通过这个方法获取到是有哪些手指头放在屏幕上了。

这个最好我们自己试验下。getPointerId(index)他是获取当前这个index对应的手指id。比如我先大拇指按在屏幕上,这个时候他的getPointerId的值就是0。然后保持大拇指按着,在按下无名指这个时候这个无名指的getPointerId就是1。如果这个时候你抬起大拇指那么调用getPointerId得到的值是1。这个方法就是用来获取在屏幕上手指的id的。默认这个id从0开始。

一个触摸事件在ViewRootImpl产生,调用mView.dispatchPointerEvent(event)这个方法将事件分发给视图层。

1
2
3
4
5
6
7
8
// View.java
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
return dispatchTouchEvent(event);
} else {
return dispatchGenericMotionEvent(event);
}
}

首先映入眼帘的是方法dispatchTouchEvent,这里我直接先看ViewGroup的这个方法:

1
2
3
4
5
6
7
8
9
10
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
// 核心代码
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}

再看ViewGroup的dispatchTouchEvent方法的时候,首先所有的核心方法都被包括在了一个判断里面onFilterTouchEventForSecurity。这个方法意思就是执行一个安全策略,当不符合条件的时候,你可以屏蔽你的应用控件处理这个事件。这是一个公开方法,在自定义view的时候可以重写这个方法。默认这个方法是在当应用不可见的时候才返回false。

这个onInterceptTouchEvent方法的触发条件:

1
2
3
4
5
6
7
8
9
10
11
12
//ViewGroup.dispatchTouchEvent
// 当时down事件或者mFirstTouchTarget不为空
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
}

onInterceptTouchEvent方法返回true的时候,也就是拦截这个事件,交给自己来处理。

1
2
3
4
5
6
7
// 当onIterceptTouchEvent返回true。第一次的时候这个mFirstTouchEvent肯定是等于null的。所以。第一次
// mFirstTouchTarget == null 一定为true所以就会进入dispatchTransformedTouchEvent分发事件方法
//dispatchTransformedTouchEvent这个方法回去判断是调用谁的touchevent
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, cancel=canceled, child=null,
desiredPointerIdBits=TouchTarget.ALL_POINTER_IDS);
}
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
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
// 这个getPointerIdBits获取的结果是所有手指头对应的id数的总和。id数分别为0 1 2 3 ...
// 然后getPointerIdBits就将这些id向左进行了对应的id数目的偏移,然后将他们加了起来用来判断手指头的数量是否改变了并且也可以知道改变了谁。 这个上面讲了。
// 默认getPointerIdBits获取的值等于1 因为既然到了这里代表有一根手指触摸屏幕了那么他的id=0.然后1<<0 的结果是1. 所以getPointerIdBits的结果就是1.
final int oldPointerIdBits = event.getPointerIdBits();
// 默认desiredPointerIdBits = -1
// 所以默认第一次这么一与运算,这个 newPointerIdBits = 1
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

if (newPointerIdBits == 0) {
return false;
}

final MotionEvent transformedEvent;
// 默认第一次这是相等的 1==1
if (newPointerIdBits == oldPointerIdBits) {
// 在viewgroup里面如果onInterceptor为true那么child一定是null
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
// 进入view的dispatchTouchEvent
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);

handled = child.dispatchTouchEvent(event);

event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}

// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}

handled = child.dispatchTouchEvent(transformedEvent);
}

// Done.
transformedEvent.recycle();
return handled;
}

这里会进入到view的dispatchTouchEvent方法。然后会触发onTouchEvent方法。

哎整个android应用就是一个递归调用,递归啊,我最害怕的递归啊,整个触摸事件的传递就是一个递归调用的过程。当触发disPatchTouchEvent就是咱们递归的开始。这里我给你演示下三成view的情况。

img-0

首先1触发dispatchTouchEvent方法,当这次是一个按下的事件或者找到了事件传递链。那么就进入自己的事件拦截方法判断是不是要拦截事件。

如果不拦截事件,则遍历全部孩子,这里1的孩子只有2。然后调用孩子2的dispatchTouchEvent方法如果孩子2这个方法返回true。那么就代表找到了这个事件传递链。

整个事件传递其实只需要看两个方法dispatchTouchEventdispatchTransformedTouchEvent

dispatchTouchEvent他的返回值代表这次事件传递是否找到了消费者。找到了消费者就可以为我们的mFirstTouchTarget设置上值了。最终去处理这个事件的视图的视图他的mFirstTouchTarget的值是null。

现在在回来看下ViewGroup的dispatchTouchEvent:

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
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
if (newTouchTarget == null && childrenCount != 0) {
for (int i = childrenCount - 1; i >= 0; i--) {
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
}
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
return handled;
}

嘿嘿被我这么一删,就可以很清晰的看到整个执行流程了。

首选判断如果是按下事件或者mFirstTouchTarget!=null 那么就执行onInterceptTouchEvent方法判断是否拦截事件。

然后不拦截的话,然后这个事件是一个按下事件,就去遍历所有的孩子执行方法dispatchTransformedTouchEvent里面去调用了孩子的dispatchTouchEvent方法。直到某个dispatchTouchevent方法返回了true。返回true才代表找到了消费这个事件的人。然后我们的mFirstTouchTarget就有值了。然后接着是滑动事件。最先收到事件的当然也是最外层的父容易。然后由于他的mFirstTouchTarget值不为空所以还是会执行他自己的onInterceptTouchEvent判断是否拦截事件。由于这次不是按压事件了。所以直接按着mFirstTouchTarget的调用链执行dispatchTransformedTouchEvent。

我们再来回顾下父视图什么时候会执行onInterceptTouchEvent方法:

1
2
3
4
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
intercepted = onInterceptTouchEvent(ev);
}

我们可以看到在点击事件的时候,是一定会走拦截方法的。然后还有一个情况就是当mFirstTouchTarget不等于null的时候。那什么时候mFirstTouchTarget不等于null呢?答:在触发点击事件的时候,这个事件在某个子视图的onTouchEvent的down事件返回true或者自己的onTouchEvent在处理down事件的时候返回了true。这样子我们的整整一个事件调用链的mFirstTouchTarget的值就不会为null。既然不会为null了,那么接下来的滑动事件抬起事件,在onInterceptTouchevent里面就能接收到了。为什么我会产生对这个的深入理解,那是因为我在看NestedScrollView的源码的时候,发现NestedScrollView只重写了onInterceptTouchEvent方法:

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
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE) && mIsBeingDragged) {
return true;
}
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE: {
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
break;
}
final int pointerIndex = ev.findPointerIndex(activePointerId);
if (pointerIndex == -1) {
break;
}

final int y = (int) ev.getY(pointerIndex);
final int yDiff = Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop
&& (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) {
mIsBeingDragged = true;
mLastMotionY = y;
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
mNestedYOffset = 0;
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
break;
}

case MotionEvent.ACTION_DOWN: {
final int y = (int) ev.getY();
if (!inChild((int) ev.getX(), y)) {
mIsBeingDragged = false;
recycleVelocityTracker();
break;
}
mLastMotionY = y;
mActivePointerId = ev.getPointerId(0);

initOrResetVelocityTracker();
mVelocityTracker.addMovement(ev);
mScroller.computeScrollOffset();
mIsBeingDragged = !mScroller.isFinished();
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
break;
}

case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
recycleVelocityTracker();
if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) {
ViewCompat.postInvalidateOnAnimation(this);
}
stopNestedScroll(ViewCompat.TYPE_TOUCH);
break;
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
}
/*
* 我们唯一只想拦截拖动事件
*/
return mIsBeingDragged;
}

可以看到在onInterceptTouchEvent里面只在拖动事件的时候返回true。那问题来了。如果NestedScrollView的所有孩子都没有对down事件进行返回true的处理的话,那么在NestedScrollView的onInterceptTouchEvent不是只能接收到down事件了,关于move up事件都不会到这里来了。这方面NestedScrollView当然考虑到了,我们可以看NestedScrollView的onTouchEvent事件,他直接返回了一个true。不管孩子他们对事件处不处理,反正到了我这里我全部是要处理的。

1
2
3
4
5
6
7
8
9
10
public boolean onTouchEvent(MotionEvent ev) {
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
if (getChildCount() == 0) {
return false;
}
}
}
return true
}

他这么一处理,NestedScrollview就再也不怕他的onInterceptTouchEvent不会执行了。

那现在来分析下NestedScrollView是如何实现滑动的,并且解决嵌套滑动的问题的呢。

关于嵌套滑动,谷歌官方提供了一组类供我们使用。

  • NestedScrollingChildHelper
  • NestedScrollingParentHelper

然后还有一组接口:

  • NestedScrollingParent
  • NestedScrollingParent2
  • NestedScrollingParent3
  • NestedScrollingChild
  • NestedScrollingChild2
  • NestedScrollingChild3

他的实现原理就是,父类继承接口NestedScrollingParent。子类继承接口NestedScrollingChild。在滑动的时候,子类主动告诉父类子类想要消耗多少距离,消耗不了的距离告诉父类,从而实现嵌套滑动的功能。使用规则就是在down时间的时候调用ChildHelper.startNestedScroll(axes,type)。然后再滑动的时候调用ChildHelper.dispatchNestedPreScrollView()。然后在UP事件调用ChildHelper.stopNestedScroll()

Android状态栏源码解析

    在Android系统中,有关系统状态栏有关得代码在/framework/base/packages/SystemUI中。Android系统启动的时候,会通过Java反射的方式启动。其启动SystemUI的SystemUIService服务。SystemUISercice是Service的一个子类,同时该类中通过反射机制同时启动了好几个和系统UI相关的服务。这些服务都是继承自SystemUI类。在SystemUIServiceonCreate里面,创建其他的服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SystemUIService extends Service {
@Override
public void onCreate() {
super.onCreate();
// Start all of SystemUI
((SystemUIApplication) getApplication()).startServicesIfNeeded();
}
}

// SystemUIApplication.java
public void startServicesIfNeeded() {
String[] names = SystemUIFactory.getInstance().getSystemUIServiceComponents(getResources());
startServicesIfNeeded("StartServices", names);
}
// SystemUiFactory.java
public String[] getSystemUIServiceComponents(Resources resources) {
return resources.getStringArray(R.array.config_systemUIServiceComponents);
}

可以看到需要开启的系统服务他们放到了array资源文件里面了,这里我们可以通过这个网站在线查看系统源码:

img-0

(可以看到系统启动了这么多的系统ui服务)

在Android系统中,进程可以分为两类系统进程应用进程

  • 系统进程:(如:init,zygote,sysetm_server进程)这些都是一直存活的,常驻内存中。系统进程如果出问题了,那你的手机就没法使用了。
  • 应用进程:指应用程序运行的进程。电话,短信,微信。

进程间的运行是独立的,一个进程奔溃异常退出并不会影响到其他进程的正常运行。

app_process:这是一个可执行程序,用来启动zygotesystem_server进程。

ActivityManager:负责Android全部四大组件的管理,并且还掌握了所有应用程序进程的创建和进程的优先级管理。

init进程是一切的开始,init进程里面配置了需要继续启动哪些系统进程。这其中有两个特别重要的进程zygotesystem_server

  • zygote:所有的进程都是通过zygote进程fork出来的子进程。因此zygote进程是所有应用进程的父进程。
  • system_server:这个进程正如他的名字一样,这是一个系统服务器。Framework层的几乎所有服务都位于这个进程中。其中包括管理四大组件的ActivityManagerService

zygote这个进程启动的时候会接着启动ZygoteInitRuntimeInit

zygoteInit是一个java代码。所有的应用进程都是通过发送数据到这个套接字上,然后由zytote进程创建。

                     在这个类的main方法里面执行了一个preload方法:这是Android启动最耗时间的两件事,preloadCLassess将framework.jar里的preloaded-classes定义的所有class load到内存里,preloaded-classes 编译Android后可以在framework/base下找到。而preloadResources 将系统的Resource(不是在用户apk里定义的resource)load到内存。

1
2
3
4
static void preload() {
preloadClasses();
preloadResources();
}

system_server:这个进程启动了大量的服务系统服务,比如:

  1. 网络管理:NetworkManagementService
  2. 窗口管理:WindowManagerService
  3. 震动管理:VibratorService
  4. 输入管理:InputManagerService
  5. 活动管理:ActivityMangerService

system_server进程的类是SystemServer这个java类。关于一个Service的启动需要知道的步骤是:

  1. 初始化Serivice对象,获得IBinder对象。
  2. 启动后台线程,并进入Loop等待
  3. 将自己注册到Service Manager,让其他进程通过名字可以获得远程调用必须的IBinder的对象。

SystemServer的main方法:

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
//=========SystemServer.java=========
public static void main(String[] args) {
new SystemServer().run();
}
private void run() {
......
//创建消息Looper
Looper.prepareMainLooper();
// 加载动态库libandroid_servers.so,初始化native服务
System.loadLibrary("android_servers");
......
//初始化系统context
createSystemContext();
//创建SystemServiceManager
mSystemServiceManager = new SystemServiceManager(mSystemContext);
......
//启动引导服务,如AMS等
startBootstrapServices();
//启动核心服务
startCoreServices();
//启动其它服务,如WMS,SystemUI等
//创建的服务对象都保存在了 ServiceManager里面了。
startOtherServices();
....
}

可以看到创建了SystemServiceManager。这个类就是创建所有服务的方法。都是通过SystemServiceManager.startService方法进行的创建:

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
private void startBootstrapServices(@NonNull TimingsTraceAndSlog t) {
mSystemServiceManager.startService(FileIntegrityService.class);
mSystemServiceManager.startService(DeviceIdentifiersPolicyService.class);
mSystemServiceManager.startService(UriGrantsManagerService.Lifecycle.class);
ActivityTaskManagerService atm = mSystemServiceManager.startService(
ActivityTaskManagerService.Lifecycle.class).getService();
mActivityManagerService = ActivityManagerService.Lifecycle.startService(
mSystemServiceManager, atm);
mActivityManagerService.setSystemServiceManager(mSystemServiceManager);
mActivityManagerService.setInstaller(installer);
mWindowManagerGlobalLock = atm.getGlobalLock();
mDataLoaderManagerService = mSystemServiceManager.startService(
DataLoaderManagerService.class);
mIncrementalServiceHandle = startIncrementalService();
mPowerManagerService = mSystemServiceManager.startService(PowerManagerService.class);
mSystemServiceManager.startService(ThermalManagerService.class);
mActivityManagerService.initPowerManagement();
mSystemServiceManager.startService(RecoverySystemService.Lifecycle.class);
RescueParty.registerHealthObserver(mSystemContext);
PackageWatchdog.getInstance(mSystemContext).noteBoot();
mSystemServiceManager.startService(LightsService.class);
if (SystemProperties.getBoolean("config.enable_sidekick_graphics", false)) {
mSystemServiceManager.startService(WEAR_SIDEKICK_SERVICE_CLASS);
}
mDisplayManagerService = mSystemServiceManager.startService(DisplayManagerService.class);
mSystemServiceManager.startBootPhase(t, SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
mPackageManager = mSystemContext.getPackageManager();
mSystemServiceManager.startService(UserManagerService.LifeCycle.class);
AttributeCache.init(mSystemContext);
mActivityManagerService.setSystemProcess();
watchdog.init(mSystemContext, mActivityManagerService);
mDisplayManagerService.setupSchedulerPolicies();
mSystemServiceManager.startService(new OverlayManagerService(mSystemContext));
mSystemServiceManager.startService(new SensorPrivacyService(mSystemContext));
}

可以看到ActivityManagerService被创建了。

现在来看下startOtherServices(),从名字我们知道这是在创建其他服务。其他都有那些呢。我们打开Context这个类就知道了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Context{
public static final String POWER_SERVICE = "power";

public static final String RECOVERY_SERVICE = "recovery";
@SystemApi
public static final String SYSTEM_UPDATE_SERVICE = "system_update";

/**
* Use with {@link #getSystemService(String)} to retrieve a
* {@link android.view.WindowManager} for accessing the system's window
* manager.
*
* @see #getSystemService(String)
* @see android.view.WindowManager
*/
public static final String WINDOW_SERVICE = "window";
public static final String LAYOUT_INFLATER_SERVICE = "layout_inflater";
public static final String ACCOUNT_SERVICE = "account";
public static final String ACTIVITY_TASK_SERVICE = "activity_task";
public static final String ALARM_SERVICE = "alarm";
public static final String KEYGUARD_SERVICE = "keyguard";
}
// 等等。。。。。。。这里只列举了很少的一部分。

这里我们拿WindowManagerService这个服务来看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// SystemServer.java 
private void startOtherServices(@NonNull TimingsTraceAndSlog t) {

WindowManagerService wm = null;
wm = WindowManagerService.main(context, inputManager, !mFirstBoot, mOnlyCore,
new PhoneWindowManager(), mActivityManagerService.mActivityTaskManager);
ServiceManager.addService(Context.WINDOW_SERVICE, wm,false,
DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PROTO);

InputManagerService inputManager = null;
inputManager = new InputManagerService(context);
ServiceManager.addService(Context.INPUT_SERVICE, inputManager,
false, DUMP_FLAG_PRIORITY_CRITICAL);
}

我们可以看到创建的WindowManagerService对象被保存在了ServiceManager里面。然后我们就可以通过ServiceManager.getService(name:String)来获取置顶的服务就行了。所以以后你在阅读源码时看到。。。ServiceManager.getService的时候不知道这个获取的服务对象到底是什么时候。你可以进入类SystemServer里面看下,在调用ServiceManager.addService方法添加了什么服务进去。

kotlin之伴生对象

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
package demo
class Person() {
private var gender: Boolean = true
constructor(name: String, gender: Boolean) : this() {
println("constructor")
}
companion object {
val instance by lazy {
Person("yzq",false)
}
init {
println("companion init 1")
}
init {
println("companion init 2")
}
}
init {
println("Person init 2,gender:${gender}")
}
init {
println("Person init 1")
}
}
/** 输出结果
*companion init 1
*companion init 2
*Person init 2,gender:true
*Person init 1
*constructor
*/

可以看到伴生对象里面的init先执行了。我们看下反编译成Java长什么样,通过java文件我们可以很清楚的就知道为什么输出效果是上面那样的了。可以看到在对象里面,init代码块会被编译到构造函数里面。

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
public final class Person {
private boolean gender;
private static final Lazy instance$delegate;
public static final Person.Companion Companion = new Person.Companion((DefaultConstructorMarker)null);
public Person() {
this.gender = true;
String var1 = "Person init 2,gender:" + this.gender;
boolean var2 = false;
System.out.println(var1);
var1 = "Person init 1";
var2 = false;
System.out.println(var1);
}

public Person(@NotNull String name, boolean gender) {
Intrinsics.checkNotNullParameter(name, "name");
this();
String var3 = "constructor";
boolean var4 = false;
System.out.println(var3);
}

static {
instance$delegate = LazyKt.lazy((Function0)null.INSTANCE);
String var0 = "companion init 1";
boolean var1 = false;
System.out.println(var0);
var0 = "companion init 2";
var1 = false;
System.out.println(var0);
}

public static final class Companion {
@NotNull
public final Person getInstance() {
Lazy var1 = Person.instance$delegate;
Person.Companion var2 = Person.Companion;
Object var3 = null;
boolean var4 = false;
return (Person)var1.getValue();
}

private Companion() {
}

public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}

现在换种写法,instance不用懒加载的方式创建,而是直接new。

1
2
// 就这么一点改变
val instance = Person("yzq",false)

反编译成java文件:

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
public final class Person {
private boolean gender;
@NotNull
private static final Person instance = new Person("yzq", false);
@NotNull
public static final Person.Companion Companion = new Person.Companion((DefaultConstructorMarker)null);
public Person() {
this.gender = true;
String var1 = "Person init 2,gender:" + this.gender;
boolean var2 = false;
System.out.println(var1);
var1 = "Person init 1";
var2 = false;
System.out.println(var1);
}

public Person(@NotNull String name, boolean gender) {
Intrinsics.checkNotNullParameter(name, "name");
this();
String var3 = "constructor";
boolean var4 = false;
System.out.println(var3);
}

static {
String var0 = "companion init 1";
boolean var1 = false;
System.out.println(var0);
var0 = "companion init 2";
var1 = false;
System.out.println(var0);
}
public static final class Companion {
@NotNull
public final Person getInstance() {
return Person.instance;
}

private Companion() {
}

// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
/*
*Person init 2,gender:true
*Person init 1
*constructor
*companion init 1
*companion init 2
*/

可以发现执行顺序变了。

我们可以发现在kotlin里面通过companion object修饰的代码块会被生成为静态变量。欧了。

还有一个好的案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class DS {
var namedsa = ""
constructor(name:String){
this.namedsa = name
}
init {
println("我说${namedsa}")
}
companion object{
fun dsa():DS{
var s = DS("污泥")
return s
}
}
}
fun main() {
DS.dsa()
}

他的输出结果是:我说

是不是很奇怪,污泥怎么不见了?现在来编译成java代码:

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
public final class DS {
private String namedsa;
public static final DS.Companion Companion = new DS.Companion((DefaultConstructorMarker)null);

@NotNull
public final String getNamedsa() {
return this.namedsa;
}

public final void setNamedsa(@NotNull String var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
this.namedsa = var1;
}

public DS(@NotNull String name) {
Intrinsics.checkNotNullParameter(name, "name");
super();
this.namedsa = "";
String var2 = "我说" + this.namedsa;
boolean var3 = false;
System.out.println(var2);
this.namedsa = name;
}
public static final class Companion {
@NotNull
public final DS dsa() {
return new DS("污泥");
}

private Companion() {
}

// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}

我们需要注意的知识点是:init的代码会插入到构造函数里面。他是优先于构造函数里面的任何代码。

好了,现在我在换中写法。构造函数的方式换下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class DS(name:String) {
init {
println("我说${name}")
}
companion object{
fun dsa():DS{
var s = DS("污泥")
return s
}
}
}
fun main() {
DS.dsa()
}

在猜猜,输出结果是啥?

结果是:我说污泥。

来看下java代码:

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
public final class DS {
public static final DS.Companion Companion = new DS.Companion((DefaultConstructorMarker)null);

public DS(@NotNull String name) {
Intrinsics.checkNotNullParameter(name, "name");
super();
String var2 = "我说" + name;
boolean var3 = false;
System.out.println(var2);
}
public static final class Companion {
@NotNull
public final DS dsa() {
DS s = new DS("污泥");
return s;
}

private Companion() {
}

public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}

可以看到这样写的话,输出的name直接走的是构造函数里面的name,比起之前的写法,少了一个this.namedsa的赋值操作,所以就出现了两个不同的输出情况。