From e7e53faa0f46d43c53fec3e9bb5feafbfa97665f Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Mon, 10 Oct 2016 23:48:37 +0800
Subject: [PATCH 001/160] Update
---
CustomView/Advance/[03]Canvas_Convert.md | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/CustomView/Advance/[03]Canvas_Convert.md b/CustomView/Advance/[03]Canvas_Convert.md
index 13f253ba..2e1c8206 100644
--- a/CustomView/Advance/[03]Canvas_Convert.md
+++ b/CustomView/Advance/[03]Canvas_Convert.md
@@ -61,7 +61,7 @@
canvas.drawCircle(0,0,100,mPaint);
```
-
+
我们首先将坐标系移动一段距离绘制一个圆形,之后再移动一段距离绘制一个圆形,两次移动是可叠加的。
@@ -105,7 +105,7 @@
```
(为了更加直观,我添加了一个坐标系,可以比较明显的看出,缩放中心就是坐标原点)
-
+
接下来我们使用第二种方法让缩放中心位置稍微改变一下,如下:
``` java
@@ -124,7 +124,7 @@
```
(图中用箭头指示的就是缩放中心。)
-
+
前面两个示例缩放的数值都是正数,按照表格中的说明,**当缩放比例为负数的时候会根据缩放中心轴进行翻转**,下面我们就来实验一下:
@@ -194,7 +194,7 @@
}
```
-
+
*****
#### ⑶旋转(rotate)
@@ -222,7 +222,7 @@
canvas.drawRect(rect,mPaint);
```
-
+
改变旋转中心位置:
``` java
@@ -240,7 +240,7 @@
canvas.drawRect(rect,mPaint);
```
-
+
好吧,旋转也是可叠加的
``` java
@@ -262,7 +262,7 @@
canvas.rotate(10);
}
```
-
+
*****
#### ⑷错切(skew)
@@ -299,7 +299,7 @@ Y = sy * x + y
mPaint.setColor(Color.BLUE); // 绘制蓝色矩形
canvas.drawRect(rect,mPaint);
```
-
+
如你所想,错切也是可叠加的,不过请注意,调用次序不同绘制结果也会不同
``` java
@@ -318,7 +318,7 @@ Y = sy * x + y
canvas.drawRect(rect,mPaint);
```
-
+
*****
#### ⑸快照(save)和回滚(restore)
@@ -440,7 +440,7 @@ public int saveLayerAlpha (float left, float top, float right, float bottom, int
## About Me
### 作者微博: @GcsSloop
-
+
******
## 四.参考资料
From 804b4287d083a0c4377d32a469f731e336e85348 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Tue, 11 Oct 2016 20:30:32 +0800
Subject: [PATCH 002/160] Update
---
CustomView/Advance/[99]DrawText.md | 279 -----------------------------
1 file changed, 279 deletions(-)
delete mode 100644 CustomView/Advance/[99]DrawText.md
diff --git a/CustomView/Advance/[99]DrawText.md b/CustomView/Advance/[99]DrawText.md
deleted file mode 100644
index fe9982a1..00000000
--- a/CustomView/Advance/[99]DrawText.md
+++ /dev/null
@@ -1,279 +0,0 @@
-# drawText之坐标、居中、绘制多行
-
-本文用于讲解Canvas中关于DrawText相关的一些常见问题,如坐标,居中,和绘制多行。
-
-之前由于个人的疏忽以及对问题的想当然,没有进行验证,在 [【安卓自定义View进阶 - 图片文字】](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B4%5DCanvas_PictureText.md) 这篇文章中出现了一个错误,有不少眼睛雪亮的网友都发现了该错误并给予了反馈,非常感谢这些网友的帮助。
-
-
-
-## 错误原因
-
-这个错误是drawText方法中坐标的一个问题,就一般的绘图而言,坐标一般都是指定左上角,然而drawText默认并不是左上角,甚至不是大家测试后认为的左下角,而是一个**由Paint中Align决定的对齐基准线**,该API注释原文如下:
-
-> Draw the text, with origin at (x,y), using the specified paint. **The origin is interpreted based on the Align setting in the paint.**
-
-在默认情况下基线如下图所示:
-
-> PS:基线(BaseLine)有两条,是用户指定的坐标确定的,在默认情况下,基线X在字符串左侧,基线y在字符串下方(但并不是最低的部分)。
-
-
-
-## 分析这样设计的原因
-
-**Q: 为何基线y要放到文字下面,而不是上面?**
-
-> A : 既然采用这种对齐方式,必然有其缘由,至少不是为了坑我这种小白。据本人猜测可能有以下原因:
-* 1.符合人类书写习惯,不论是汉字还是英文或是其他语言,我们在书写对齐的时候都是以下面为基准线的,而不是上面,(**我们默认的基准线类似于四线格中的第三条线**)。
-
-* 2.字的特点,字的显示不仅有有中文汉字,还有一些特殊字符,并且大部分是根据下面对齐的,如果把对齐的基线放到上面并使其显示整齐,设计太麻烦,如下:
-
-* 3.字体的特点,我们都知道,字有很多的字体风格,不同字体风格的大小是不同的,如果以以上面为基准线而让其底部对齐,这设计难度简直不敢想象:
-
-**综上所述,基线y放到下面不仅符合人的书写习惯,而且更加便于设计。**
-
-## drawText从入门到懵逼
-
-虽然之前在 [图片文字](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B4%5DCanvas_PictureText.md) 这篇文章中已经简单的了解部分关于文字的方法,但Paint中关于文字的方法还有很多,本文会了解一些我们比较关心的一些内容,例如绘制居中的文本,多行文本等,在此之前我们先了解一下Paint中与文本相关的内部类或者枚举:
-
-名称 | 类型 | 主要作用
----------------|:------:|------------------------------------------------------
-Align | 枚举 | 指定基准线与文本的相对关系(包含 左对齐 右对齐 居中)
-Style | 枚举 | 设置样式,但不仅仅是为文本服务(包含 描边 填充 描边加填充)
-FontMetrics | 内部类 | 描述给定的文本大小,字体,间距等各种度量值(度量结果类型为float)
-FontMetricsInt | 内部类 | 作用同上,但度量结果返回值是int型的
-
-### Align
-
-Align中文意思是对齐,其作用正式如此,我们使用过 Word 的人都知道文本有 *左对齐、居中、右对齐、两端对齐、分散对齐* 五种模式,Align作用就是设置文本的对齐模式,但是并没有这么多,仅有 **左对齐、居中、右对齐** 三种模式,如下:
-
-> 吐槽:就因为没有两端对齐和分散对齐,导致长文本,尤其是中英混合的长文本在手机上显示效果奇差,相信做阅读类软件的程序员深有体会,最终还要自定义View解决。
-
-枚举类型 | 作用
----------|-------------------------------------------------------------
-LEFT | 左对齐 基线X在文本左侧,基线y在文本下方,文本出现在给定坐标的右侧,是默认的对齐方式
-RIGHT | 右对齐 基线x在文本右侧,基线y在文本下方,文本出现在给定坐标的左侧
-CENTER | 居中对齐 基线x在文本中间,基线y在文本下方,文本出现在给定坐标的两侧
-
-Align对应的方法如下:
-
-``` java
- public Paint.Align getTextAlign () // 获取对齐方式
-
- public void setTextAlign (Paint.Align align) // 设置对齐方式
-```
-
-在实际运用中基线与模式之间的关系则如下图所示:
-
-
-
-> PS 该图片是截屏加工所得,其中红色的先是各自的基准线。注意汉字有一部分是在基准线下面的。
-
-其核心代码如下:
-
-``` java
- // 平移画布 - 如有疑问请参考画布操作这篇文章
- canvas.translate(mCenterX,0); // mCenterX 是屏幕宽度的一半,在onSizeChanged中获取
-
- // 设置字体颜色
- mPaint.setColor(Color.rgb(0x06,0xaf,0xcd));
-
- // 在不同模式下绘制文字
- mPaint.setTextAlign(Paint.Align.LEFT);
- canvas.drawText("左对齐", 0, 200, mPaint);
-
- mPaint.setTextAlign(Paint.Align.CENTER);
- canvas.drawText("居中对齐", 0, 400, mPaint);
-
- mPaint.setTextAlign(Paint.Align.RIGHT);
- canvas.drawText("右对齐", 0, 600, mPaint);
-```
-
-### Style
-
-Style的意思是样式,这个在 [Canvas之绘制基本形状](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B2%5DCanvas_BasicGraphics.md) 这篇文章中讲过,它有三种状态:
-
-枚举类型 | 作用
-----------------|----------------
-FILL | 填充
-STROKE | 描边
-FILL_AND_STROKE | 填充加描边
-
-Style 对应的方法如下:
-
-``` java
-public Paint.Style getStyle () // 获取当然样式
-
-public void setStyle (Paint.Style style) // 设置样式
-```
-
-效果如下:
-
-
-
-核心代码:
-
-``` java
- // 平移画布 - 如有疑问请参考画布操作这篇文章
- canvas.translate(mCenterX,0); // mCenterX 是屏幕宽度的一半,在onSizeChanged中获取
-
- mPaint.setTextAlign(Paint.Align.CENTER);
- mPaint.setColor(Color.rgb(0x06,0xaf,0xcd));
-
- // 设置不同的样式
-
- // 填充
- mPaint.setStyle(Paint.Style.FILL);
- canvas.drawText("GcsSloop 中文", 0, 200, mPaint);
-
- // 描边
- mPaint.setStyle(Paint.Style.STROKE);
- canvas.drawText("GcsSloop 中文", 0, 400, mPaint);
-
- // 描边加填充
- mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
- canvas.drawText("GcsSloop 中文", 0, 600, mPaint);
-```
-
-### FontMetrics
-
-FontMetrics 是 Paint 的一个内部类,根据名字我们可以大致知道这是一个描述**字体规格**相关的类,关于这个类的描述原文是这样的:
-
-> Class that describes the various metrics for a font at a given text size. Remember, Y values increase going down, so those values will be positive, and values that measure distances going up will be negative. This class is returned by getFontMetrics().
-
-翻译一下:
-
-> 简单来说,FontMetrics包含了当前文本相关的各项参数,你可以通过 Paint中 _getFontMetrics()_ 这个方法来获取到这个类。
-(另外,原文中还特地强调了一下关于坐标正负的问题。)
-
-##### FontMetrics包含了以下几个数值:
-
-> 如果没有特殊说明,一下文章中“基线”默认代表“基线Y”。
-
-名称 | 正负| 释义
---------|:---:|--------------------------------------------------------
-top | - | 在指定字形和大小的情况下,字符最高点与基线之间的距离
-ascent | - | 单个字符的最高点与基线距离的推荐值(不包括字母上面的符号)
-descent | + | 单个字符的最低点与基线距离的推荐值
-bottom | + | 在指定字形和大小的情况下,字符最低点与基线之间的距离
-leading | + | 行间距,当前行bottom与下一行top之间的距离的推荐值 (通常为0,因为top与ascent,bottom与leading之间的距离足够作为行间距了)
-
-
-
-看了上面啰啰嗦嗦讲了一堆,你可能会产生一些疑问,这里我简单解释一下:
-
-0、 FontMetrics到底有什么用?
-
-> FontMetrics 是字体规格,比较精确的测量出文字相关的各种数据,在很多地方都是用得到的,比较常见的就是我们的音乐播放器中的歌词显示,需要实时的变字体位置,这里就需要比较精确是数值来进行计算以确保不会出现两行文字重叠等问题。
-
-1、为什么表格中最高点距离基线的值是负值,而最低点反而是正值?
-
-> 这是因为手机屏幕坐标系的特殊性,在数学中常见的坐标系y轴是向上的,而对于手机而言,y轴默认是向下的,所以数值越小越靠近屏幕顶端,即最高点的数值比基线小,最高点减去基线结果自然为负数。 更多参考 [坐标系](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Base/%5B1%5DCoordinateSystem.md) 这篇文章。
-
-2、为什么表格中很多都是推荐值?
-
-> 这是因为字的规格不仅受字大小的影响,而且受字体风格的影响,不同的字体风格差别很大,其结果可能会有偏差,故大部分都是推荐值。
-
-3、 具体绘制的文本不同会影响 FontMetrics 中的数值吗?
-
-> 不会, **FontMetrics中的数值受字体大小(FontSize) 和 字体风格(Typeface) 影响**, 而不会因为具体绘制的内容不同而改变,给Paint设置完字体大小和字体风格后就能获取到正确的FontMetrics。
-
-**获取FontMetrics的方法**
-``` java
- Paint.FontMetrics mMetrics = mPaint.getFontMetrics();
-
- Log.e("TAG", "top=" + mMetrics.top);
- Log.e("TAG", "ascent=" + mMetrics.ascent);
- Log.e("TAG", "descent=" + mMetrics.descent);
- Log.e("TAG", "bottom=" + mMetrics.bottom);
- Log.e("TAG", "leading=" + mMetrics.leading);
-```
-结果:
-```
-05-15 21:18:13.315 13112-13112/com.gcssloop.canvas E/TAG: top=-152.98828
-05-15 21:18:13.315 13112-13112/com.gcssloop.canvas E/TAG: ascent=-129.88281
-05-15 21:18:13.315 13112-13112/com.gcssloop.canvas E/TAG: descent=34.179688
-05-15 21:18:13.315 13112-13112/com.gcssloop.canvas E/TAG: bottom=37.939453
-05-15 21:18:13.315 13112-13112/com.gcssloop.canvas E/TAG: leading=0.0
-```
-
-**由于FontMetrics中的 ascent 与 descent 比较常用,所以可以直接通过Paint获取:**
-``` java
- Log.e("TAG", "P.ascent=" + mPaint.ascent());
- Log.e("TAG", "P.descent=" + mPaint.descent());
-```
-结果,可以看到与通过FontMetrics获得的结果相同。
-```
-05-15 21:24:18.950 13112-13112/com.gcssloop.canvas E/TAG: P.ascent=-129.88281
-05-15 21:24:18.955 13112-13112/com.gcssloop.canvas E/TAG: P.descent=34.179688
-```
-
-
-## 文字居中
-
-对于绘制居中的文本来说,我们可以封装一个方法用中心点作为绘制坐标,在绘制的时候转换为实际坐标。
-
-根据前面的知识可知,想让文字水平居中很容易,只需要设置 TextAlign 为 CENTER,那么基线X(BaseLineX)自然就是这个字符串的中心了。
-
-而让文字垂直居中则有些麻烦了,因为基线Y(BaseLineY)既不是顶部,底部,也不是中心,所以文本居中最难的内容就是计算BaseLineY的位置。
-
-我们已知:
-
-> 1. 中心位置坐标,centerX, centerY
-2. 文本高度:height = descent-ascent
-3. descent的坐标:descentY = centerY + 1/2height
-4. baseLineY坐标:baseLineY = descentY-descent
-
-通过上面内容可以推算出如下公式:
-
-> baseLineY = centerY - 1/2(ascent + descent)
-
-**根据公式我们可以封装一个方法:**
-
-``` java
- /**
- * 居中绘制文本
- *
- * @param text
- * @param canvas
- * @param paint
- */
- public void drawTextByCenter(String text, float x, float y, Canvas canvas, Paint paint) {
- Paint tempPaint = new Paint(paint); // 创建一个临时画笔,防止影响原来画笔的状态
- tempPaint.setTextAlign(Paint.Align.CENTER); // 设置文本对齐方式为居中
-
- // 通过y计算出baseline的位置
- float baseline = y - (tempPaint.descent() + tempPaint.ascent()) / 2;
-
- canvas.drawText(text, x, baseline, tempPaint); //绘制文本
- }
-```
-
-**测试方法是否正确**
-
-``` java
- canvas.translate(mCenterX, mCenterY);
-
- String text = "ASSA";
- mPaint.setColor(Color.BLACK);
-
- drawTextByCenter(text, 0, 0, canvas, mPaint);
-```
-
-结果:
-
-
-
-## 绘制多行
-
-前面讲的所有绘制文本都是绘制单行文本,而对于长文本就束手无策了,但是我们偶尔也需要绘制一些长文本,例如绘制一段文本,这样前面的就无法满足需求了。
-
-我们先思考一下如何才能绘制一段长文本让其可以自动换行,如自动测量文本长度,然后根据测量的内容截开成n行,然后一行一行的绘制。
-
-
-
-
-
-
-
-
-
From 1ff2e82bf0426103051e117efc217558b998bdb9 Mon Sep 17 00:00:00 2001
From: sloop
Date: Wed, 12 Oct 2016 21:43:19 +0800
Subject: [PATCH 003/160] Update
---
CustomView/Advance/Code/SearchView.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CustomView/Advance/Code/SearchView.md b/CustomView/Advance/Code/SearchView.md
index 13af1f7c..19d404a3 100644
--- a/CustomView/Advance/Code/SearchView.md
+++ b/CustomView/Advance/Code/SearchView.md
@@ -1,5 +1,7 @@
## SearchView 源代码
+[下载代码 (右键 -> 另存为)](https://raw.githubusercontent.com/GcsSloop/AndroidNote/master/CustomView/Advance/Code/SearchView.java)
+
``` java
/**
* Author: GcsSloop
From 62adce7309380be15d6f4b0c66a206e1f9dbfc16 Mon Sep 17 00:00:00 2001
From: sloop
Date: Wed, 12 Oct 2016 21:45:19 +0800
Subject: [PATCH 004/160] Update
---
CustomView/Advance/Code/SetPolyToPoly.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CustomView/Advance/Code/SetPolyToPoly.md b/CustomView/Advance/Code/SetPolyToPoly.md
index 3988fac2..9d77d6f3 100644
--- a/CustomView/Advance/Code/SetPolyToPoly.md
+++ b/CustomView/Advance/Code/SetPolyToPoly.md
@@ -2,6 +2,8 @@
## SetPolyToPoly.java
+[下载代码 (右键 -> 另存为)](https://raw.githubusercontent.com/GcsSloop/AndroidNote/master/CustomView/Advance/Code/SetPolyToPoly.java)
+
```java
public class SetPolyToPoly extends View{
private static final String TAG = "SetPolyToPoly";
From 610aa5c14a3e76cfecf7c728d29d9180b55e93c5 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Fri, 14 Oct 2016 04:35:36 +0800
Subject: [PATCH 005/160] Update
---
.../Advance/[15]Dispatch-TouchEvent-Source.md | 546 ++++++++++++++++++
1 file changed, 546 insertions(+)
diff --git a/CustomView/Advance/[15]Dispatch-TouchEvent-Source.md b/CustomView/Advance/[15]Dispatch-TouchEvent-Source.md
index c868ef90..16fd056b 100644
--- a/CustomView/Advance/[15]Dispatch-TouchEvent-Source.md
+++ b/CustomView/Advance/[15]Dispatch-TouchEvent-Source.md
@@ -1,2 +1,548 @@
# 事件分发机制详解
+在上一篇文章 [事件分发机制原理][dispatch-touchevent-theory] 中简要分析了一下事件分发机制的原理,原理是十分简单的,一句话就能总结:**责任链模式,事件层层传递,直到被消费。** 虽然原理简单,但是随着 Android 不断的发展,实际运用场景也越来越复杂,所以想要彻底玩转事件分发机制还需要一定技巧,本篇事件分发机制详解将带大家了解 ...
+
+> **你以为我接下来要讲源码?**
+> 我就不按套路,所有的源码都是为了适应具体的应用场景而写的,只要能够理解运用场景,理解源码也就十分简单了。所以本篇的核心问题是:**正确理解在实际场景中事件分发机制的作用。** 会涉及到源码,但不是主角。
+
+**注意:本文中所有源码分析部分均基于 API23(Android 6.0) 版本,由于安卓系统源码改变很多,可能与之前版本有所不同,但基本流程都是一致的。**
+
+
+
+## 常见事件
+
+既然是事件分发,总要有事件才能分发吧,所以我们先了解一下常见的几种事件。
+
+根据面向对象思想,事件被封装成 MotionEvent 对象,由于本篇重点不在于此,所以只会涉及到几个与手指触摸相关的常见事件:
+
+| 事件 | 简介 |
+| ------------- | ------------------------- |
+| ACTION_DOWN | 手指 **初次接触到屏幕** 时触发。 |
+| ACTION_MOVE | 手指 **在屏幕上滑动** 时触发,会会多次触发。 |
+| ACTION_UP | 手指 **离开屏幕** 时触发。 |
+| ACTION_CANCEL | 事件 **被上层拦截** 时触发。 |
+
+对于单指触控来说,一次简单的交互流程是这样的:
+
+**手指落下(ACTION_DOWN) -> 移动(ACTION_MOVE) -> 离开(ACTION_UP)**
+
+> * 本次事例中 ACTION_MOVE 有多次触发。
+> * 如果仅仅是单击(手指按下再抬起),不会触发 ACTION_MOVE。
+
+
+
+## 事件分发、拦截与消费
+
+关于这一部分内容,上一篇文章 [事件分发机制原理][dispatch-touchevent-theory] 已经将流程整理的比较清楚了,本文会深入细节来研究这些内容。之所以分开讲,是为了防止大家被细节所迷惑而忽略了整体逻辑。
+
+> `√` 表示有该方法。
+>
+> `X` 表示没有该方法。
+
+| 类型 | 相关方法 | ViewGroup | View |
+| :--: | :-------------------: | :-------: | :--: |
+| 事件分发 | dispatchTouchEvent | √ | √ |
+| 事件拦截 | onInterceptTouchEvent | √ | X |
+| 事件消费 | onTouchEvent | √ | √ |
+
+
+
+### View 相关
+
+`dispatchTouchEvent` 是事件分发机制中的核心,所有的事件调度都归它管。不过我细看表格, ViewGroup 有 dispatchTouchEvent 也就算了,毕竟人家有一堆 ChildView 需要管理,但为啥 View 也有?这就引出了我们的第一个疑问。
+
+#### Q: 为什么 View 会有 dispatchTouchEvent ?
+
+A: 我们知道 View 可以注册很多事件监听器,例如:单击事件(onClick)、长按事件(onLongClick)、触摸事件(onTouch),并且View自身也有 onTouchEvent 方法,那么问题来了,这么多与事件相关的方法应该由谁管理?毋庸置疑就是 `dispatchTouchEvent`,所以 View 也会有事件分发。
+
+相信看到这里很多小伙伴会产生第二个疑问,View 有这么多事件监听器,到底哪个先执行?
+
+#### Q: 与 View 事件相关的各个方法调用顺序是怎样的?
+
+A: **如果不去看源码,想一下让自己设计会怎样?**
+
+* 单击事件(onClickListener) 需要两个两个事件(ACTION_DOWN 和 ACTION_UP )才能触发,如果先分配给onClick判断,等它判断完,用户手指已经离开屏幕,黄花菜都凉了,定然造成 View 无法响应其他事件,应该最后调用。(最后)
+* 长按事件(onLongClickListener) 同理,也是需要长时间等待才能出结果,肯定不能排到前面,但因为不需要ACTION_UP,应该排在 onClick 前面。(onLongClickListener > onClickListener)
+* 触摸事件(onTouchListener) 如果用户注册了触摸事件,说明用户要自己处理触摸时间了,这个应该排在最前面。(最前)
+* View自身处理(onTouchEvent) 提供了一种默认的处理方式,如果用户已经处理好了,也就不需要了,所以应该排在 onClickListener 后面。(onClickListener > onTouchListener)
+
+**所以事件的调度顺序应该是 `onTouchListener > onTouchEvent > onLongClickListener > onClickListener`**。
+
+
+
+下面我们来看一下实际测试结果:
+
+> 手指按下,不移动,稍等片刻再抬起。
+
+```shell
+[Listener ]: onTouchListener ACTION_DOWN
+[GcsView ]: onTouchEvent ACTION_DOWN
+[Listener ]: onLongClickListener
+[Listener ]: onTouchListener ACTION_UP
+[GcsView ]: onTouchEvent ACTION_UP
+[Listener ]: onClickListener
+```
+
+可以看到,测试结果也支持我们猜测的结论,因为长按 onLongClickListener 不需要 ACTION_UP 所以会在 ACTION_DOWN 之后就触发。
+
+接下来就看一下源码是怎么设计的(省略了大量无关代码):
+
+```java
+public boolean dispatchTouchEvent(MotionEvent event) {
+ ...
+ boolean result = false; // result 为返回值,主要作用是告诉调用者事件是否已经被消费。
+ if (onFilterTouchEventForSecurity(event)) {
+ ListenerInfo li = mListenerInfo;
+ /**
+ * 如果设置了OnTouchListener,并且当前 View 可点击,就调用监听器的 onTouch 方法,
+ * 如果 onTouch 方法返回值为 true,就设置 result 为 true。
+ */
+ if (li != null && li.mOnTouchListener != null
+ && (mViewFlags & ENABLED_MASK) == ENABLED
+ && li.mOnTouchListener.onTouch(this, event)) {
+ result = true;
+ }
+
+ /**
+ * 如果 result 为 false,则调用自身的 onTouchEvent。
+ * 如果 onTouchEvent 返回值为 true,则设置 result 为 true。
+ */
+ if (!result && onTouchEvent(event)) {
+ result = true;
+ }
+ }
+ ...
+ return result;
+}
+```
+
+> **如果觉得源码还是太长,那么用伪代码实现应当是这样的(省略若干安全判断),简单粗暴:**
+>
+> ```java
+> public boolean dispatchTouchEvent(MotionEvent event) {
+> if (mOnTouchListener.onTouch(this, event)) {
+> return true;
+> } else if (onTouchEvent(event)) {
+> return true;
+> }
+> return false;
+> }
+> ```
+
+正当你沉迷在源码的"精妙"逻辑的时候,你可能没发现有两个东西失踪了,等回过神来,定睛一看,哎呦妈呀,**OnClick 和 OnLongClick 去哪里了?**
+
+不要担心,OnClick 和 OnLongClick 的具体调用位置在 **onTouchEvent** 中,看源码(同样省略大量无关代码):
+
+```java
+public boolean onTouchEvent(MotionEvent event) {
+ ...
+ final int action = event.getAction();
+ // 检查各种 clickable
+ if (((viewFlags & CLICKABLE) == CLICKABLE ||
+ (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
+ (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
+ switch (action) {
+ case MotionEvent.ACTION_UP:
+ ...
+ removeLongPressCallback(); // 移除长按
+ ...
+ performClick(); // 检查单击
+ ...
+ break;
+ case MotionEvent.ACTION_DOWN:
+ ...
+ checkForLongClick(0); // 检测长按
+ ...
+ break;
+ ...
+ }
+ return true; // ◀︎表示事件被消费
+ }
+ return false;
+}
+```
+
+> **注意了,第一个重点要出现了(敲黑板)!**
+>
+> 
+>
+> **注意上面代码中存在一个 `return true;` 并且是只要 View 可点击就返回 true,就表示事件被消费了。**
+>
+> 举个栗子: I have a **RelativeLayout**,I have a **View**,Ugh,**RelativeLayout - View**
+>
+> ```xml
+> android:background="#CCC"
+> android:id="@+id/layout"
+> android:onClick="myClick"
+> android:layout_width="200dp"
+> android:layout_height="200dp">
+> android:clickable="true"
+> android:layout_width="200dp"
+> android:layout_height="200dp" />
+>
+> ```
+>
+> 现在你有了一个 **RelativeLayout - View** 你开开心心的为 RelativeLayout 设置了一个点击事件`myClick`,然而你会发现不论怎么点都不会接收到信息,仔细一看,发现内部的 View 有一个属性 `android:clickable="true"` 正是这个看似不起眼的属性把事件给消费掉了,由此我们可以得出如下结论:
+> **1. 不论 View 自身是否注册点击事件,只要 View 是可点击的就会消费事件。**
+> **2. 事件是否被消费由返回值决定,true 表示消费,false 表示不消费,与是否使用了事件无关。**
+
+关于 View 的事件分发先说这么多,下面我们来看一下 ViewGroup 的事件分发。
+
+
+
+### ViewGroup 相关
+
+**ViewGroup(通常是各种Layout) 的事件分发相对来说就要麻烦一些,因为 ViewGroup 不仅要考虑自身,还要考虑各种 ChildView,一旦处理不好就容易引起各种事件冲突,正所谓养儿方知父母难啊。**
+
+#### VIewGroup 的事件分发流程又是如何的呢?
+
+ 上一篇文章 [事件分发机制原理][dispatch-touchevent-theory] 中我们了解到事件是通过ViewGroup一层一层传递的,最终传递给 View,ViewGroup 要比它的 ChildView 先拿到事件,并且有权决定是否告诉要告诉 ChildView。在默认的情况下 ViewGroup 事件分发流程是这样的。
+
+* 1.判断自身是否需要(询问 onInterceptTouchEvent 是否拦截),如果需要,调用自己的 onTouchEvent。
+* 2.自身不需要或者不确定,则询问 ChildView ,一般来说是调用手指触摸位置的 ChildView。
+* 3.如果子 ChildView 不需要则调用自身的 onTouchEvent。
+
+
+用伪代码应该是这样的:
+
+```java
+public boolean dispatchTouchEvent(MotionEvent ev) {
+ boolean result = false; // 默认状态为没有消费过
+
+ if (!onInterceptTouchEvent(ev)) { // 如果没有拦截交给子View
+ result = child.dispatchTouchEvent(ev);
+ }
+
+ if (!result) { // 如果事件没有被消费,询问自身onTouchEvent
+ result = onTouchEvent(ev);
+ }
+
+ return result;
+}
+```
+
+**有人看到这里可能会有疑问,我看过源码,ViewGroup 的 `dispatchTouchEvent` 可有二百多行呢,你弄这几行就想忽悠我,别以为我读书少。**
+
+当然了,上述源码是不完善的,还有很多问题是没有解决的,例如:
+
+##### 1. ViewGroup 中可能有多个 ChildView,如何判断应该分配给哪一个?
+
+这个很容易,就是把所有的 ChildView 遍历一遍,如果手指触摸的点在 ChildView 区域内就分发给这个View。
+
+##### 2. 当该点的 ChildView 有重叠时应该如何分配?
+
+当 ChildView 重叠时,**一般会分配给显示在最上面的 ChildView**。
+如何判断哪个是显示在最上面的呢?后面加载的一般会覆盖掉之前的,所以**显示在最上面的是最后加载的**。
+
+如下:
+
+```xml
+
+
+
+
+```
+
+
+
+当手指点击有重叠区域时,分如下几种情况:
+
+1. 只有 View1 可点击时,事件将会分配给 View1,即使被 View2 遮挡,这一部分仍是 View1 的可点击区域。
+2. 只有 View2 可点击时,事件将会分配给 View2。
+3. View1 和 View2 均可点击时,事件会分配给后加载的 View2,View2 将事件消费掉,View1接收不到事件。
+
+**注意:**
+
+* 上面说的是可点击,可点击包括很多种情况,只要你给View注册了 `onClickListener、onLongClickListener、OnContextClickListener` 其中的任何一个监听器或者设置了 `android:clickable="true"` 就代表这个 View 是可点击的。
+ 另外,某些 View 默认就是可点击的,例如,Button,CheckBox 等。
+* 给 View 注册 `OnTouchListener` 不会影响 View 的可点击状态。即使给 View 注册 `OnTouchListener` ,**只要不返回 `true` 就不会消费事件**。
+
+
+##### 3. ViewGroup 和 ChildView 同时注册了事件监听器(onClick等),哪个会执行?
+
+事件优先给 ChildView,会被 ChildView消费掉,ViewGroup 不会响应。
+
+##### 4. 所有事件都应该被同一 View 消费
+
+在上面的例子中我们分析后可以了解到,同一次点击事件只能被一个 View 消费,这是为什呢?主要是为了防止事件响应混乱,如果再一次完整的事件中分别将不同的事件分配给了不同的 View 容易造成事件响应混乱。
+
+> ( View 中 onClick 事件需要同时接收到 ACTION_DOWN 和 ACTION_UP 才能触发,如果分配给了不同的 View,那么 onClick 将无法被正确触发)。
+
+**安卓为了保证所有的事件都是被一个 View 消费的,对第一次的事件( ACTION_DOWN )进行了特殊判断,View 只有消费了 ACTION_DOWN 事件,才能接收到后续的事件(可点击控件会默认消费所有事件),并且会将后续所有事件传递过来,不会再传递给其他 View,除非上层 View 进行了拦截。**
+**如果上层 View 拦截了当前正在处理的事件,会收到一个 ACTION_CANCEL,表示当前事件已经结束,后续事件不会再传递过来。**
+
+
+
+**源码:**
+
+> 其实如果能够理解上面的内容,不看源码也能非常顺利的使用事件分发,但源码中能挖掘出更多的内容。
+
+```java
+public boolean dispatchTouchEvent(MotionEvent ev) {
+ // 调试用
+ if (mInputEventConsistencyVerifier != null) {
+ mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
+ }
+
+ // 判断事件是否是针对可访问的焦点视图(很晚才添加的内容,个人猜测和屏幕辅助相关,方便盲人等使用设备)
+ if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
+ ev.setTargetAccessibilityFocus(false);
+ }
+
+ boolean handled = false;
+ if (onFilterTouchEventForSecurity(ev)) {
+ final int action = ev.getAction();
+ final int actionMasked = action & MotionEvent.ACTION_MASK;
+
+ // 处理第一次ACTION_DOWN.
+ if (actionMasked == MotionEvent.ACTION_DOWN) {
+ // 清除之前所有的状态
+ cancelAndClearTouchTargets(ev);
+ resetTouchState();
+ }
+
+ // 检查是否需要拦截.
+ 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 {
+ // 没有目标来处理该事件,而且也不是一个新的事件事件(ACTION_DOWN), 进行拦截。
+ intercepted = true;
+ }
+
+ // 判断事件是否是针对可访问的焦点视图
+ if (intercepted || mFirstTouchTarget != null) {
+ ev.setTargetAccessibilityFocus(false);
+ }
+
+ // 检查事件是否被取消(ACTION_CANCEL).
+ final boolean canceled = resetCancelNextUpFlag(this)
+ || actionMasked == MotionEvent.ACTION_CANCEL;
+
+ final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
+ TouchTarget newTouchTarget = null;
+ boolean alreadyDispatchedToNewTouchTarget = false;
+
+ // 如果没有取消也没有被拦截 (进入事件分发)
+ if (!canceled && !intercepted) {
+
+ // 如果事件是针对可访问性焦点视图,我们将其提供给具有可访问性焦点的视图。
+ // 如果它不处理它,我们清除该标志并像往常一样将事件分派给所有的 ChildView。
+ // 我们检测并避免保持这种状态,因为这些事非常罕见。
+ View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
+ ? findChildWithAccessibilityFocus() : null;
+
+ if (actionMasked == MotionEvent.ACTION_DOWN
+ || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
+ || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
+ final int actionIndex = ev.getActionIndex();
+ final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
+ : TouchTarget.ALL_POINTER_IDS;
+
+ // 清除此指针ID的早期触摸目标,防止不同步。
+ removePointersFromTouchTargets(idBitsToAssign);
+
+ final int childrenCount = mChildrenCount;
+ if (newTouchTarget == null && childrenCount != 0) {
+ final float x = ev.getX(actionIndex); // 获取触摸位置坐标
+ final float y = ev.getY(actionIndex);
+ // 查找可以接受事件的 ChildView
+ final ArrayList preorderedList = buildOrderedChildList();
+ final boolean customOrder = preorderedList == null
+ && isChildrenDrawingOrderEnabled();
+ final View[] children = mChildren;
+ // ▼注意,从最后向前扫描
+ for (int i = childrenCount - 1; i >= 0; i--) {
+ final int childIndex = customOrder
+ ? getChildDrawingOrder(childrenCount, i) : i;
+ final View child = (preorderedList == null)
+ ? children[childIndex] : preorderedList.get(childIndex);
+
+ // 如果有一个视图具有可访问性焦点,我们希望它首先获取事件,
+ // 如果不处理,我们将执行正常的分派。
+ // 尽管这可能会分发两次,但它能保证在给定的时间内更安全的执行。
+ if (childWithAccessibilityFocus != null) {
+ if (childWithAccessibilityFocus != child) {
+ continue;
+ }
+ childWithAccessibilityFocus = null;
+ i = childrenCount - 1;
+ }
+
+ // 检查View是否允许接受事件(即处于显示状态(VISIBLE)或者正在播放动画)
+ // 检查触摸位置是否在View区域内
+ if (!canViewReceivePointerEvents(child)
+ || !isTransformedTouchPointInView(x, y, child, null)) {
+ ev.setTargetAccessibilityFocus(false);
+ continue;
+ }
+
+ // getTouchTarget 中判断了 child 是否包含在 mFirstTouchTarget 中
+ // 如果有返回 target,如果没有返回 null
+ newTouchTarget = getTouchTarget(child);
+ if (newTouchTarget != null) {
+ // ChildView 已经准备好接受在其区域内的事件。
+ newTouchTarget.pointerIdBits |= idBitsToAssign;
+ break; // ◀︎已经找到目标View,跳出循环
+ }
+
+ resetCancelNextUpFlag(child);
+ if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
+ mLastTouchDownTime = ev.getDownTime();
+ if (preorderedList != null) {
+ for (int j = 0; j < childrenCount; j++) {
+ if (children[childIndex] == mChildren[j]) {
+ mLastTouchDownIndex = j;
+ break;
+ }
+ }
+ } else {
+ mLastTouchDownIndex = childIndex;
+ }
+ mLastTouchDownX = ev.getX();
+ mLastTouchDownY = ev.getY();
+ newTouchTarget = addTouchTarget(child, idBitsToAssign);
+ alreadyDispatchedToNewTouchTarget = true;
+ break;
+ }
+
+ ev.setTargetAccessibilityFocus(false);
+ }
+ if (preorderedList != null) preorderedList.clear();
+ }
+
+ if (newTouchTarget == null && mFirstTouchTarget != null) {
+ // 没有找到 ChildView 接收事件
+ newTouchTarget = mFirstTouchTarget;
+ while (newTouchTarget.next != null) {
+ newTouchTarget = newTouchTarget.next;
+ }
+ newTouchTarget.pointerIdBits |= idBitsToAssign;
+ }
+ }
+ }
+
+ // 分发 TouchTarget
+ if (mFirstTouchTarget == null) {
+ // 没有 TouchTarget,将当前 ViewGroup 当作普通的 View 处理。
+ handled = dispatchTransformedTouchEvent(ev, canceled, null,
+ TouchTarget.ALL_POINTER_IDS);
+ } else {
+ // 分发TouchTarget,如果我们已经分发过,则避免分配给新的目标。
+ // 如有必要,取消分发。
+ 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;
+ }
+ }
+
+ // 如果需要,更新指针的触摸目标列表或取消。
+ if (canceled
+ || actionMasked == MotionEvent.ACTION_UP
+ || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
+ resetTouchState();
+ } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
+ final int actionIndex = ev.getActionIndex();
+ final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
+ removePointersFromTouchTargets(idBitsToRemove);
+ }
+ }
+
+ if (!handled && mInputEventConsistencyVerifier != null) {
+ mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
+ }
+ return handled;
+}
+```
+
+
+
+## 核心要点
+
+1. **事件分发原理: 责任链模式,事件层层传递,直到被消费。**
+2. **View 的 `dispatchTouchEvent` 主要用于调度自身的监听器和 onTouchEvent。**
+3. **View的事件的调度顺序是 onTouchListener > onTouchEvent > onLongClickListener > onClickListener 。**
+4. **不论 View 自身是否注册点击事件,只要 View 是可点击的就会消费事件。**
+5. **事件是否被消费由返回值决定,true 表示消费,false 表示不消费,与是否使用了事件无关。**
+6. **ViewGroup 中可能有多个 ChildView 时,将事件分配给包含点击位置的 ChildView。**
+7. **ViewGroup 和 ChildView 同时注册了事件监听器(onClick等),由 ChildView 消费。**
+8. **一次触摸流程中产生事件应被同一 View 消费,全部接收或者全部拒绝。**
+9. **只要接受 ACTION_DOWN 就意味着接受所有的事件,拒绝 ACTION_DOWN 则不会收到后续内容。**
+10. **如果当前正在处理的事件被上层 View 拦截,会收到一个 ACTION_CANCEL,后续事件不会再传递过来**。
+
+
+
+## 总结
+
+本文啰嗦了这么多内容,但真正需要注意的就是核心要点中的几个概念,只要能正确理解这些概念,相信理解事件分发机制将再也不是难题。
+
+> 最后,个人推荐阅读源码的方法,先尝试用自己的角度去分析,建立概念,然后看源码进行验证、对比,如果发现自己建立的概念有问题,就尝试修正自己的概念,这样比较容易理解原作者的意图,也不容易被众多的代码所迷惑。
+>
+> 就像 ViewGroup 中的 dispatchTouchEvent 内容非常多,主要是为了应对实际的场景,里面有很多 安全判断,处理多指触控 等内容,这些如果不先建立概念就去看源码很容易被这些细节问题所迷惑。
+
+
+
+## 参考资料
+
+[View ](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/View.java)
+[ViewGroup.java](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewGroup.java)
+[Android Touch事件分发详解](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/Android%20Touch%E4%BA%8B%E4%BB%B6%E5%88%86%E5%8F%91%E8%AF%A6%E8%A7%A3.md)
+[基于源码来了解Android的事件分发机制](http://minjie.tech/2016/09/03/%E5%9F%BA%E4%BA%8E%E6%BA%90%E7%A0%81%E6%9D%A5%E4%BA%86%E8%A7%A3Android%E7%9A%84%E4%BA%8B%E4%BB%B6%E5%88%86%E5%8F%91%E6%9C%BA%E5%88%B6/)
+
+
+
+## About Me
+
+### 作者微博: @GcsSloop
+
+
+
+
+
+[dispatch-touchevent-theory]: http://www.gcssloop.com/customview/dispatch-touchevent-theory "事件分发机制原理-GcsSloop"
+
From c178bd0c9530982c32c2c4a0876d492b607c2492 Mon Sep 17 00:00:00 2001
From: sloop
Date: Sat, 15 Oct 2016 02:11:24 +0800
Subject: [PATCH 006/160] Update
---
CustomView/Advance/[15]Dispatch-TouchEvent-Source.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CustomView/Advance/[15]Dispatch-TouchEvent-Source.md b/CustomView/Advance/[15]Dispatch-TouchEvent-Source.md
index 16fd056b..503d2ed6 100644
--- a/CustomView/Advance/[15]Dispatch-TouchEvent-Source.md
+++ b/CustomView/Advance/[15]Dispatch-TouchEvent-Source.md
@@ -63,7 +63,7 @@ A: **如果不去看源码,想一下让自己设计会怎样?**
* 单击事件(onClickListener) 需要两个两个事件(ACTION_DOWN 和 ACTION_UP )才能触发,如果先分配给onClick判断,等它判断完,用户手指已经离开屏幕,黄花菜都凉了,定然造成 View 无法响应其他事件,应该最后调用。(最后)
* 长按事件(onLongClickListener) 同理,也是需要长时间等待才能出结果,肯定不能排到前面,但因为不需要ACTION_UP,应该排在 onClick 前面。(onLongClickListener > onClickListener)
-* 触摸事件(onTouchListener) 如果用户注册了触摸事件,说明用户要自己处理触摸时间了,这个应该排在最前面。(最前)
+* 触摸事件(onTouchListener) 如果用户注册了触摸事件,说明用户要自己处理触摸事件了,这个应该排在最前面。(最前)
* View自身处理(onTouchEvent) 提供了一种默认的处理方式,如果用户已经处理好了,也就不需要了,所以应该排在 onClickListener 后面。(onClickListener > onTouchListener)
**所以事件的调度顺序应该是 `onTouchListener > onTouchEvent > onLongClickListener > onClickListener`**。
From d1ba104e0b3b54af14d17c7599ebf469c1e7d96b Mon Sep 17 00:00:00 2001
From: sloop
Date: Sat, 15 Oct 2016 02:12:05 +0800
Subject: [PATCH 007/160] Update
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 3c93c94f..842cdcfc 100644
--- a/README.md
+++ b/README.md
@@ -34,7 +34,7 @@
* [安卓自定义View进阶 - Matrix详解](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B10%5DMatrix_Method.md)
* [安卓自定义View进阶 - Matrix Camera](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B11%5DMatrix_3D_Camera.md)
* [安卓自定义View进阶 - 事件分发机制原理](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B12%5DDispatch-TouchEvent-Theory.md)
-
+ * [安卓自定义View进阶 - 事件分发机制详解](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B15%5DDispatch-TouchEvent-Source.md)
******
## [教程类](https://github.com/GcsSloop/AndroidNote/tree/master/Course/README.md)
From 5fdb9f1503afc48516e43492b6bc45618654dbd3 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Sat, 15 Oct 2016 16:15:39 +0800
Subject: [PATCH 008/160] Update
---
CustomView/Advance/[99]DrawText.md | 279 +++++++++++++++++++++++++++++
1 file changed, 279 insertions(+)
create mode 100644 CustomView/Advance/[99]DrawText.md
diff --git a/CustomView/Advance/[99]DrawText.md b/CustomView/Advance/[99]DrawText.md
new file mode 100644
index 00000000..fe9982a1
--- /dev/null
+++ b/CustomView/Advance/[99]DrawText.md
@@ -0,0 +1,279 @@
+# drawText之坐标、居中、绘制多行
+
+本文用于讲解Canvas中关于DrawText相关的一些常见问题,如坐标,居中,和绘制多行。
+
+之前由于个人的疏忽以及对问题的想当然,没有进行验证,在 [【安卓自定义View进阶 - 图片文字】](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B4%5DCanvas_PictureText.md) 这篇文章中出现了一个错误,有不少眼睛雪亮的网友都发现了该错误并给予了反馈,非常感谢这些网友的帮助。
+
+
+
+## 错误原因
+
+这个错误是drawText方法中坐标的一个问题,就一般的绘图而言,坐标一般都是指定左上角,然而drawText默认并不是左上角,甚至不是大家测试后认为的左下角,而是一个**由Paint中Align决定的对齐基准线**,该API注释原文如下:
+
+> Draw the text, with origin at (x,y), using the specified paint. **The origin is interpreted based on the Align setting in the paint.**
+
+在默认情况下基线如下图所示:
+
+> PS:基线(BaseLine)有两条,是用户指定的坐标确定的,在默认情况下,基线X在字符串左侧,基线y在字符串下方(但并不是最低的部分)。
+
+
+
+## 分析这样设计的原因
+
+**Q: 为何基线y要放到文字下面,而不是上面?**
+
+> A : 既然采用这种对齐方式,必然有其缘由,至少不是为了坑我这种小白。据本人猜测可能有以下原因:
+* 1.符合人类书写习惯,不论是汉字还是英文或是其他语言,我们在书写对齐的时候都是以下面为基准线的,而不是上面,(**我们默认的基准线类似于四线格中的第三条线**)。
+
+* 2.字的特点,字的显示不仅有有中文汉字,还有一些特殊字符,并且大部分是根据下面对齐的,如果把对齐的基线放到上面并使其显示整齐,设计太麻烦,如下:
+
+* 3.字体的特点,我们都知道,字有很多的字体风格,不同字体风格的大小是不同的,如果以以上面为基准线而让其底部对齐,这设计难度简直不敢想象:
+
+**综上所述,基线y放到下面不仅符合人的书写习惯,而且更加便于设计。**
+
+## drawText从入门到懵逼
+
+虽然之前在 [图片文字](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B4%5DCanvas_PictureText.md) 这篇文章中已经简单的了解部分关于文字的方法,但Paint中关于文字的方法还有很多,本文会了解一些我们比较关心的一些内容,例如绘制居中的文本,多行文本等,在此之前我们先了解一下Paint中与文本相关的内部类或者枚举:
+
+名称 | 类型 | 主要作用
+---------------|:------:|------------------------------------------------------
+Align | 枚举 | 指定基准线与文本的相对关系(包含 左对齐 右对齐 居中)
+Style | 枚举 | 设置样式,但不仅仅是为文本服务(包含 描边 填充 描边加填充)
+FontMetrics | 内部类 | 描述给定的文本大小,字体,间距等各种度量值(度量结果类型为float)
+FontMetricsInt | 内部类 | 作用同上,但度量结果返回值是int型的
+
+### Align
+
+Align中文意思是对齐,其作用正式如此,我们使用过 Word 的人都知道文本有 *左对齐、居中、右对齐、两端对齐、分散对齐* 五种模式,Align作用就是设置文本的对齐模式,但是并没有这么多,仅有 **左对齐、居中、右对齐** 三种模式,如下:
+
+> 吐槽:就因为没有两端对齐和分散对齐,导致长文本,尤其是中英混合的长文本在手机上显示效果奇差,相信做阅读类软件的程序员深有体会,最终还要自定义View解决。
+
+枚举类型 | 作用
+---------|-------------------------------------------------------------
+LEFT | 左对齐 基线X在文本左侧,基线y在文本下方,文本出现在给定坐标的右侧,是默认的对齐方式
+RIGHT | 右对齐 基线x在文本右侧,基线y在文本下方,文本出现在给定坐标的左侧
+CENTER | 居中对齐 基线x在文本中间,基线y在文本下方,文本出现在给定坐标的两侧
+
+Align对应的方法如下:
+
+``` java
+ public Paint.Align getTextAlign () // 获取对齐方式
+
+ public void setTextAlign (Paint.Align align) // 设置对齐方式
+```
+
+在实际运用中基线与模式之间的关系则如下图所示:
+
+
+
+> PS 该图片是截屏加工所得,其中红色的先是各自的基准线。注意汉字有一部分是在基准线下面的。
+
+其核心代码如下:
+
+``` java
+ // 平移画布 - 如有疑问请参考画布操作这篇文章
+ canvas.translate(mCenterX,0); // mCenterX 是屏幕宽度的一半,在onSizeChanged中获取
+
+ // 设置字体颜色
+ mPaint.setColor(Color.rgb(0x06,0xaf,0xcd));
+
+ // 在不同模式下绘制文字
+ mPaint.setTextAlign(Paint.Align.LEFT);
+ canvas.drawText("左对齐", 0, 200, mPaint);
+
+ mPaint.setTextAlign(Paint.Align.CENTER);
+ canvas.drawText("居中对齐", 0, 400, mPaint);
+
+ mPaint.setTextAlign(Paint.Align.RIGHT);
+ canvas.drawText("右对齐", 0, 600, mPaint);
+```
+
+### Style
+
+Style的意思是样式,这个在 [Canvas之绘制基本形状](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B2%5DCanvas_BasicGraphics.md) 这篇文章中讲过,它有三种状态:
+
+枚举类型 | 作用
+----------------|----------------
+FILL | 填充
+STROKE | 描边
+FILL_AND_STROKE | 填充加描边
+
+Style 对应的方法如下:
+
+``` java
+public Paint.Style getStyle () // 获取当然样式
+
+public void setStyle (Paint.Style style) // 设置样式
+```
+
+效果如下:
+
+
+
+核心代码:
+
+``` java
+ // 平移画布 - 如有疑问请参考画布操作这篇文章
+ canvas.translate(mCenterX,0); // mCenterX 是屏幕宽度的一半,在onSizeChanged中获取
+
+ mPaint.setTextAlign(Paint.Align.CENTER);
+ mPaint.setColor(Color.rgb(0x06,0xaf,0xcd));
+
+ // 设置不同的样式
+
+ // 填充
+ mPaint.setStyle(Paint.Style.FILL);
+ canvas.drawText("GcsSloop 中文", 0, 200, mPaint);
+
+ // 描边
+ mPaint.setStyle(Paint.Style.STROKE);
+ canvas.drawText("GcsSloop 中文", 0, 400, mPaint);
+
+ // 描边加填充
+ mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
+ canvas.drawText("GcsSloop 中文", 0, 600, mPaint);
+```
+
+### FontMetrics
+
+FontMetrics 是 Paint 的一个内部类,根据名字我们可以大致知道这是一个描述**字体规格**相关的类,关于这个类的描述原文是这样的:
+
+> Class that describes the various metrics for a font at a given text size. Remember, Y values increase going down, so those values will be positive, and values that measure distances going up will be negative. This class is returned by getFontMetrics().
+
+翻译一下:
+
+> 简单来说,FontMetrics包含了当前文本相关的各项参数,你可以通过 Paint中 _getFontMetrics()_ 这个方法来获取到这个类。
+(另外,原文中还特地强调了一下关于坐标正负的问题。)
+
+##### FontMetrics包含了以下几个数值:
+
+> 如果没有特殊说明,一下文章中“基线”默认代表“基线Y”。
+
+名称 | 正负| 释义
+--------|:---:|--------------------------------------------------------
+top | - | 在指定字形和大小的情况下,字符最高点与基线之间的距离
+ascent | - | 单个字符的最高点与基线距离的推荐值(不包括字母上面的符号)
+descent | + | 单个字符的最低点与基线距离的推荐值
+bottom | + | 在指定字形和大小的情况下,字符最低点与基线之间的距离
+leading | + | 行间距,当前行bottom与下一行top之间的距离的推荐值 (通常为0,因为top与ascent,bottom与leading之间的距离足够作为行间距了)
+
+
+
+看了上面啰啰嗦嗦讲了一堆,你可能会产生一些疑问,这里我简单解释一下:
+
+0、 FontMetrics到底有什么用?
+
+> FontMetrics 是字体规格,比较精确的测量出文字相关的各种数据,在很多地方都是用得到的,比较常见的就是我们的音乐播放器中的歌词显示,需要实时的变字体位置,这里就需要比较精确是数值来进行计算以确保不会出现两行文字重叠等问题。
+
+1、为什么表格中最高点距离基线的值是负值,而最低点反而是正值?
+
+> 这是因为手机屏幕坐标系的特殊性,在数学中常见的坐标系y轴是向上的,而对于手机而言,y轴默认是向下的,所以数值越小越靠近屏幕顶端,即最高点的数值比基线小,最高点减去基线结果自然为负数。 更多参考 [坐标系](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Base/%5B1%5DCoordinateSystem.md) 这篇文章。
+
+2、为什么表格中很多都是推荐值?
+
+> 这是因为字的规格不仅受字大小的影响,而且受字体风格的影响,不同的字体风格差别很大,其结果可能会有偏差,故大部分都是推荐值。
+
+3、 具体绘制的文本不同会影响 FontMetrics 中的数值吗?
+
+> 不会, **FontMetrics中的数值受字体大小(FontSize) 和 字体风格(Typeface) 影响**, 而不会因为具体绘制的内容不同而改变,给Paint设置完字体大小和字体风格后就能获取到正确的FontMetrics。
+
+**获取FontMetrics的方法**
+``` java
+ Paint.FontMetrics mMetrics = mPaint.getFontMetrics();
+
+ Log.e("TAG", "top=" + mMetrics.top);
+ Log.e("TAG", "ascent=" + mMetrics.ascent);
+ Log.e("TAG", "descent=" + mMetrics.descent);
+ Log.e("TAG", "bottom=" + mMetrics.bottom);
+ Log.e("TAG", "leading=" + mMetrics.leading);
+```
+结果:
+```
+05-15 21:18:13.315 13112-13112/com.gcssloop.canvas E/TAG: top=-152.98828
+05-15 21:18:13.315 13112-13112/com.gcssloop.canvas E/TAG: ascent=-129.88281
+05-15 21:18:13.315 13112-13112/com.gcssloop.canvas E/TAG: descent=34.179688
+05-15 21:18:13.315 13112-13112/com.gcssloop.canvas E/TAG: bottom=37.939453
+05-15 21:18:13.315 13112-13112/com.gcssloop.canvas E/TAG: leading=0.0
+```
+
+**由于FontMetrics中的 ascent 与 descent 比较常用,所以可以直接通过Paint获取:**
+``` java
+ Log.e("TAG", "P.ascent=" + mPaint.ascent());
+ Log.e("TAG", "P.descent=" + mPaint.descent());
+```
+结果,可以看到与通过FontMetrics获得的结果相同。
+```
+05-15 21:24:18.950 13112-13112/com.gcssloop.canvas E/TAG: P.ascent=-129.88281
+05-15 21:24:18.955 13112-13112/com.gcssloop.canvas E/TAG: P.descent=34.179688
+```
+
+
+## 文字居中
+
+对于绘制居中的文本来说,我们可以封装一个方法用中心点作为绘制坐标,在绘制的时候转换为实际坐标。
+
+根据前面的知识可知,想让文字水平居中很容易,只需要设置 TextAlign 为 CENTER,那么基线X(BaseLineX)自然就是这个字符串的中心了。
+
+而让文字垂直居中则有些麻烦了,因为基线Y(BaseLineY)既不是顶部,底部,也不是中心,所以文本居中最难的内容就是计算BaseLineY的位置。
+
+我们已知:
+
+> 1. 中心位置坐标,centerX, centerY
+2. 文本高度:height = descent-ascent
+3. descent的坐标:descentY = centerY + 1/2height
+4. baseLineY坐标:baseLineY = descentY-descent
+
+通过上面内容可以推算出如下公式:
+
+> baseLineY = centerY - 1/2(ascent + descent)
+
+**根据公式我们可以封装一个方法:**
+
+``` java
+ /**
+ * 居中绘制文本
+ *
+ * @param text
+ * @param canvas
+ * @param paint
+ */
+ public void drawTextByCenter(String text, float x, float y, Canvas canvas, Paint paint) {
+ Paint tempPaint = new Paint(paint); // 创建一个临时画笔,防止影响原来画笔的状态
+ tempPaint.setTextAlign(Paint.Align.CENTER); // 设置文本对齐方式为居中
+
+ // 通过y计算出baseline的位置
+ float baseline = y - (tempPaint.descent() + tempPaint.ascent()) / 2;
+
+ canvas.drawText(text, x, baseline, tempPaint); //绘制文本
+ }
+```
+
+**测试方法是否正确**
+
+``` java
+ canvas.translate(mCenterX, mCenterY);
+
+ String text = "ASSA";
+ mPaint.setColor(Color.BLACK);
+
+ drawTextByCenter(text, 0, 0, canvas, mPaint);
+```
+
+结果:
+
+
+
+## 绘制多行
+
+前面讲的所有绘制文本都是绘制单行文本,而对于长文本就束手无策了,但是我们偶尔也需要绘制一些长文本,例如绘制一段文本,这样前面的就无法满足需求了。
+
+我们先思考一下如何才能绘制一段长文本让其可以自动换行,如自动测量文本长度,然后根据测量的内容截开成n行,然后一行一行的绘制。
+
+
+
+
+
+
+
+
+
From 7485212306ff9128805c28bb5c1777a481b543df Mon Sep 17 00:00:00 2001
From: sloop
Date: Sun, 16 Oct 2016 06:15:53 +0800
Subject: [PATCH 009/160] Update
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index 842cdcfc..ebecd7b4 100644
--- a/README.md
+++ b/README.md
@@ -35,6 +35,7 @@
* [安卓自定义View进阶 - Matrix Camera](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B11%5DMatrix_3D_Camera.md)
* [安卓自定义View进阶 - 事件分发机制原理](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B12%5DDispatch-TouchEvent-Theory.md)
* [安卓自定义View进阶 - 事件分发机制详解](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B15%5DDispatch-TouchEvent-Source.md)
+
******
## [教程类](https://github.com/GcsSloop/AndroidNote/tree/master/Course/README.md)
From 383ae9e0cc2350f2bbe4f50a43e48e7b6a7b9117 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Sun, 16 Oct 2016 17:31:38 +0800
Subject: [PATCH 010/160] Update
---
CustomView/Demo/PieView.zip | Bin 46473 -> 45694 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
diff --git a/CustomView/Demo/PieView.zip b/CustomView/Demo/PieView.zip
index 633b42150474dfb1b54e5b00232285b19d1fa22c..f88db430606fe0e3d9564e305deb332e0a63a70b 100644
GIT binary patch
delta 3611
zcmZ`*3s@6Z79J)%!XpVv0wIAAd5TJ4m7=sNAVmd5lCo%gq9ThR)FM%_vPQOBDMCfP
zR^+t^K2QvV25q})VU^mhRti2=6j!VJ;iIitTv74O-aB_DGsIo;CEv{dpL6b+`~UY0
z#lMkn`bdF#7SGm+K!2Sj>Ky`a&xV;itqFdg*oX;4iU~gaTq;q6)DfOL776r+A=yR;
zn9I8oI-*^R^i}qyS7XJtWsx6i?#ur?($4tQE(?6%930&A*L!zk`BO_ow=R6LdLMYu
zRT*xMnS42F;MC)>71vfY1Ya9RT
ze>|mhd)n5cxkDe68ZVC1D-K-aKNUS1m-}L_=JT}&{xBx3yz||>Z<|y#kL`0Ks(PB+
z&vlwcCpugV@xp`In_Qv?;x_-V=*sR{|A{x}qk2uDOlM4*ZIY&(hLt2*Oq>A}Cp~X8(Y)oV8JSo`!m$FC&_T-AM+%MY4)Uk-y_Ja)Nxa
zz!r%Qo{{rx5!#x}dtyUff&SILpvl=6!*HaujA&jh1}xnY=H;we+nKN7-~P-mvk
z%}ie(nztq`RP}yxYO-$in)DQ{3xUkU8Y*f|N0_)sAG(5{RWxp9Y1_2bOH*Q|L`M>b
zV${XqlKmSau4?z(T2TF`WY?akG5Q(t{@Rr(H-qNa?i%#Ep4%5itT=u)DB4L`xL&t)
z$;jQp5mP?gn*Fn8d9hddH>c8uPVC}!bl%|iXx|xl+FjZH>zj4sQ+J)|4VqrtQ4v>>
z{`}wq!Y3oC|N7%o=U)|7{C1*c?V#Rw`h`B#msx`ka@@Z^-8Q+MG;60yR~-ww@&(V2hS5V?+>DpaLmnN2`Z&!tX>sd@h%^75
z)_ux+>|pS`pC#Q>`48uxU-Y%4)}c#~^NYTBp$NJpv<;fVf&cQ{fs~1qORP-tmKdq2
zi7~NB$=Xn^Do6}#Ct1ugtb`KkNtFn)uAxf(pV~rq%?5pHchDtJVoTN4`T~hCfW28N
z3}$bP!tv~lgXlecgKW4`M7y&?beeW&@rXGT-+FySDGj0OpSxkhf>yUgoNp_hi!YXl
zGjPUPu`kaPnf=b4{&~YsPa|}ASfDTH_V55xq%PD!`3Rj4jdVFq{#rVX$3tPS#JF1&n
zsRT6@LL6hPxQQ=zRQBPE4OM$^mZaK+)96fpPq4Jc3sakGqB$bOySajpy-qu8ZE*!G
z48nwbC2isc-nX!#cJTL)*?%XvN|0bEJ(MC&Ma(Z`-WC&S^q%ZFbt
z!i5=s(fbRRz}zI#$B9opEgE&;P5l_f0}`H6dMK(gGIg05A5s&MPf%kx*C+-H_Q`zX
zcx>46wVBx~R2gfQt;mL>6W5dXfMrJ%K?2T_=9oA;HYq!Ed0r?J4N%$1`(?b%b_8f~
z_V#3o^v~lyHX83``{nwT4s0DXxS2BOA*^?uJAEXaZuS?}TUFtV2)4#=FC
z{lTKRdJl`4fnt2DVnE3OA-NMVLH5Y4I1}83=xv1hflWsRTqfb5kc@Pu8AsrAm>vsA
z*dqsJWF?n#1hpcCJ=Pc~8&u?91fb6722UA{t%BGM3Il%|A>m_Gd=*&=G`9-LjUp5e
zhoGHx-)|t>&ajSb#hgPb(!rI=YB}VNPyIQFX))IAF_P^$?68V7adAh`B6ef?ZGf!V
zjb7iAb}AawT&tk@>(wYHW5skJn$}MAS)=Cl`o=M29m{q$O2g4cd*5XW;WG>i#Z%MT
zo*?W;62xSVc728KNiF3&8=XP4mos+>=m(%bA|z{lV3ddTPW=R#e^Cth(a)KAI(vhP
zR4GwBNHjR(eOd&udjRpy&{Mueh59AbuFU(K?VW0z32kAb(Nk-;iW^tf9ts?Ct
zlob|6WhujlWevhCJ;T_6h-Npg&vKfDtZkWBjmG9K_9
zWAA7gLDVE0`K63(vXG#M^)rN6=*+7{||+lb37*FC03)tu}a*tya)N;4!WHnBH?F;
Lm>_y;(A|Fl@k-$c
delta 4504
zcmZ8j2|SePAD?;0>csZ6_wC%*6o~_2x~CYU~ysc33~D0
zcJbFL2+p#3O=j{cwQ=`ptbLJQlo_i1`k35Ny1J%Z=q+)3x6|tu=KS8D8$U;^&^T6?
z2%cVM{>^-$$INjz%M0&&g8vIYowxt?EJrWn?3UNBf@KbGTKeY2K!(`Q_u{?Bh9(
zz1xDmdZy2Ijjw&=vf*BDt+8=KY?~7HbHRlf=gB{unhFSYgy%|F`#-5luFk?Dr)8Vj
z##jtSh=y63X>=KE#cWth>%sdG#gH++zAo0b+uc2N*`DHb%tLtck!N{@R^os*q%Qkk
zD}VhCLHZ)giw9~&w}MZQo=^>(2&m$A3N^ruY7TA&%RyRlz5M)WhAGk2yrf}XuEF~~
zg7-)0^8T5@heKe77$kuS{2&DHc^?i1rr`kw7>gdX+{x2v-k5K^%cmsj^JzPh
zNwIN2!r><)FMZQfgr4NkE$-Y
zHVe1F_37#E8
zvhopTGrwB0%TGmoGpE(shQ^(ZYM#9CYMij6{C-<{qfLkXB*mfYY?9_v)z|B&E_y~g
zkEWM<{;e{(Geogy%l9?AnR;y-QixIE8k`&0A3o=VxJ2mD7S|4JyTc0G$Wfg6)_Y63
zcjDrfLpQ_i@TUW>+eSQ_c%Ss;pCpYzD`L}hnpn7yL?%sgBWmmRT=F~-l*PWyao-)1a3e17kE
zqV(H+$)z-0#IavK`kZWOuAg4h$;qy3irn_;p^->^dxm4_j&-*Zw_Z&xqt=Q08qsA7
z>)j2zMB?OIlkWPQCr5iv#QEX(m-@56>gRh!IZu5IOlB1uR_i{x
zpJ?Z^=}wdbx5=)weD4~FabT;p_0d3~@ZiBO
zz#rSU!O6GXhs^e9&vo^95&x-%-aV22+^M{$`vkZYNj0Y?)E)8jsAv1VIk;VjSn9;N
z7@Uz&InR|ayr(|Sw1^zq
zoNOdC>ONzgJudlqxH~;Sg
LqMS0!6ZbKQgeMqys
zY7`x|Z{6LQl%MO5F?a0?P74b@#CqYGUt(kbYR6(#YgE9y*lP<#8-`uMcS&&@ercs1
zT5Bq$|Cg3ziS%9@w)2J5-#NG{j;m9dQMwyrJX3n^W4rWgS@tg%DQWt@TbkWGfd!tN
zitCq6l0;BFqZGKC9F|FC=lSrekU&sbFlPr8MTet+4j;~;03v9h7&
zl^=f2!iZfANXzbs`J_Tqmfu*T+Npr4wRYPz5xKY9V^TZE^+2ME`iSU731RJVy8LFJ
z`|n4fNOvE05t9{$AJTuYvxlYo?GXJ#h1!#Xl}4W7u!PrccRxbF<#hdGtK2xPPR#(>VF6
z^`unk-K2=oL3#nEd@%aAs{9u!Vx`BG8rO2#wPO^x1wZ-eoN4m!>i2kKk0`}hsxlCG5Er%pFxT%&?bN}mRH-Y{gw^aqmJ!CKv4
zGL`zY+%qeMwvaXzBJD;3dD9};!
zHuu>4xc-NCQv>Ud7(IZG@&WzzqWL4wDdX#iDQzaobX(T7z_f2O-_0
z;l;t3bRm$Pf(P9cQIX{<3oJ^wpnx*MQFD*pq6pvo&D0cyjm=C@2Y^H+yO^QyQx|_lzW1&Wa}-Vs5#}uq;;q9~
z2JezdsNZbzAlm4^{042brEt+GU#|EHao$)uQ>_HnNiUt(8Gq5JXbuFhN+koUbTJTG
zC9wvT97}tMm|=waAXI`f9MQh2GxV@HSm1(@Of7|Fh3o2!g=BCm`iQlp#{lyR(|cJZ
zp&l#x>OZ}+5Z%W=#u$t=hClB7vpWRRtWtq!GDHOmCZjf3vrLM5
z9TLFiY#AINU_d5bVGZgF5NfH}B-%+LpIZ3QfE8v-4!t=fsD=mPQ
zTuBgTLJlMt)8LQ2ANET|FcqLoAP9DCnMZ=gmMx;d@;Y(`c|9N#nVm<6wn^|J5jz=t
z&67nl%j7em?&htDT2X#5~M4Q_`?+lFAxJy0kePv
zg)iF#IQSPZq55T9iXYPieSttj6S$0(deuY9S1r3B0Zh+V{UK*WE=s=N5D7f-)5*EMGVKCnbY@;`4C**kH
z47gSjcypyNvNTuB9C4vBYqkpPv6eq-c(afd1#*m8bC*28haz6Rn=4{MZxBq8ALUle
zgqW%@X1E3^*FW|x^>7B>6VKF;{2-el&i6)EU895!si^a80$Y^{jTrJ!VbE0~itYm>
zVib|AwK=LMlWKT&Yz+M3nFw!21$z6jcR8yz{Lcdn7J
zvWb<7qO0i&m(#HUFbu59CHRDb%IT1O8o*X6fNVj$9mciO5j?Si%y-wUphFLqV6c@-
z6yOe!5f5a*0cwIwnUGEWDzg;0OEeXf&t`=)q4s#
zeK>L!VvytagG9jn<2;uPs;dl=+N&nDc*#X~nK`nK3srO+E*nXy101i@6p@{WR#`lJ
S?PD;;@OMNSgNe&Q-u@4mA&0#H
From d873e1f6307451f9e1d4427abdff380a3c4575fc Mon Sep 17 00:00:00 2001
From: sloop
Date: Sun, 16 Oct 2016 17:38:12 +0800
Subject: [PATCH 011/160] Update
---
CustomView/Advance/[02]Canvas_BasicGraphics.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/CustomView/Advance/[02]Canvas_BasicGraphics.md b/CustomView/Advance/[02]Canvas_BasicGraphics.md
index 28820e87..86336f5e 100644
--- a/CustomView/Advance/[02]Canvas_BasicGraphics.md
+++ b/CustomView/Advance/[02]Canvas_BasicGraphics.md
@@ -476,12 +476,12 @@ public class PieView extends View {
// 设置数据
public void setData(ArrayList mData) {
this.mData = mData;
- initDate(mData);
+ initData(mData);
invalidate(); // 刷新
}
// 初始化数据
- private void initDate(ArrayList mData) {
+ private void initData(ArrayList mData) {
if (null == mData || mData.size() == 0) // 数据有问题 直接返回
return;
From 9623462edaccdeb395a6bc6b0860501238f04590 Mon Sep 17 00:00:00 2001
From: sloop
Date: Mon, 17 Oct 2016 18:25:39 +0800
Subject: [PATCH 012/160] Update
---
CustomView/README.md | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/CustomView/README.md b/CustomView/README.md
index 85791f40..664ace01 100644
--- a/CustomView/README.md
+++ b/CustomView/README.md
@@ -43,6 +43,12 @@
+*******
+
+
+
+
+
### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop)
From 2321fcac249db3135c10be132ac6fb2db2c48771 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Tue, 18 Oct 2016 17:02:46 +0800
Subject: [PATCH 013/160] Update
---
CustomView/Base/[02]AngleAndRadian.md | 24 +++++++++++++-----------
1 file changed, 13 insertions(+), 11 deletions(-)
diff --git a/CustomView/Base/[02]AngleAndRadian.md b/CustomView/Base/[02]AngleAndRadian.md
index dab77f5a..2d4dd12f 100644
--- a/CustomView/Base/[02]AngleAndRadian.md
+++ b/CustomView/Base/[02]AngleAndRadian.md
@@ -20,17 +20,17 @@
由于两者进制是不同的(**角度是60进制,弧度是10进制**),在合适的地方使用合适的单位来描述会更加方便。
> **例如:**
-角度是60进位制,遇到30°6′这样的角,应该转化为10进制的30.1°。但弧度就不需要,因为弧度本身就是十进制的实数。
+> 角度是60进位制,遇到30°6′这样的角,应该转化为10进制的30.1°。但弧度就不需要,因为弧度本身就是十进制的实数。
## 二.角度与弧度的定义
角度和弧度一样都是描述角的一种度量单位,下面是它们的定义:
-名称 | 定义
-:---:| ---
-角度 | 两条射线从圆心向圆周射出,形成一个夹角和夹角正对的一段弧。**当这段弧长正好等于圆周长的360分之一时,两条射线的夹角的大小为1度.**
-弧度 | 两条射线从圆心向圆周射出,形成一个夹角和夹角正对的一段弧。**当这段弧长正好等于圆的半径时,两条射线的夹角大小为1弧度.**
+| 名称 | 定义 |
+| :--: | ---------------------------------------- |
+| 角度 | 两条射线从圆心向圆周射出,形成一个夹角和夹角正对的一段弧。**当这段弧长正好等于圆周长的360分之一时,两条射线的夹角的大小为1度.** |
+| 弧度 | 两条射线从圆心向圆周射出,形成一个夹角和夹角正对的一段弧。**当这段弧长正好等于圆的半径时,两条射线的夹角大小为1弧度.** |
**如图:**
@@ -47,14 +47,16 @@ C = 2πr;
一周对应的角度为360度(角度),对应的弧度为2π弧度。
-故: **180度 = π弧度.**
+故的等价关系: **180(度) = π(弧度).**
-可得:
+由等价关系可得如下换算公式:
-公式 | 例子
-----------------------|---------------------
-**弧度 = 角度xπ/180** | 2π = 360 x π / 180
-**角度 = 弧度x180/π** | 360 = 2π x 180 / π
+> rad 是弧度, deg 是角度
+
+| 公式 | 例子 |
+| ----------------------- | ------------------ |
+| **rad = deg x π / 180** | 2π = 360 x π / 180 |
+| **deg = rad x 180 / π** | 360 = 2π x 180 / π |
维基百科的公式:
From 5f270c2f752be9e182591c9072805b0af3094be7 Mon Sep 17 00:00:00 2001
From: sloop
Date: Wed, 19 Oct 2016 00:14:57 +0800
Subject: [PATCH 014/160] Update
---
CustomView/Advance/[07]Path_Over.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CustomView/Advance/[07]Path_Over.md b/CustomView/Advance/[07]Path_Over.md
index a942b63d..19007696 100644
--- a/CustomView/Advance/[07]Path_Over.md
+++ b/CustomView/Advance/[07]Path_Over.md
@@ -282,7 +282,7 @@ Path的布尔运算有五种逻辑,如下:
代码:
-``` java
+``` java
int x = 80;
int r = 100;
From e6de20672bb31def5e00922a065ef37539ac8a1d Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Wed, 19 Oct 2016 04:09:56 +0800
Subject: [PATCH 015/160] Update
---
CustomView/Advance/[05]Path_Basic.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/CustomView/Advance/[05]Path_Basic.md b/CustomView/Advance/[05]Path_Basic.md
index a46a6e3a..85f12f53 100644
--- a/CustomView/Advance/[05]Path_Basic.md
+++ b/CustomView/Advance/[05]Path_Basic.md
@@ -213,9 +213,9 @@ close方法用于连接当前最后一个点和最初的一个点(如果两个
```
-很明显,两个lineTo分别代表第1和第2条线,而close在此处的作用就算连接了B(200,0)点和圆的O之间的第3条线,使之形成一个封闭的图形。
+很明显,两个lineTo分别代表第1和第2条线,而close在此处的作用就算连接了B(200,0)点和原点O之间的第3条线,使之形成一个封闭的图形。
-**注意:close的作用是封闭路径,与当前最后一个点和第一个点并不等价。如果连接了最后一个点和第一个点仍然无法形成封闭图形,则close什么 也不做。**
+**注意:close的作用是封闭路径,与连接当前最后一个点和第一个点并不等价。如果连接了最后一个点和第一个点仍然无法形成封闭图形,则close什么 也不做。**
### 第2组: addXxx与arcTo
From 8cb6e5d8162e3227f8bbec3031da01b96b044bd4 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Thu, 20 Oct 2016 00:39:14 +0800
Subject: [PATCH 016/160] Update
---
CustomView/Advance/[15]Dispatch-TouchEvent-Source.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CustomView/Advance/[15]Dispatch-TouchEvent-Source.md b/CustomView/Advance/[15]Dispatch-TouchEvent-Source.md
index 503d2ed6..b31bb10b 100644
--- a/CustomView/Advance/[15]Dispatch-TouchEvent-Source.md
+++ b/CustomView/Advance/[15]Dispatch-TouchEvent-Source.md
@@ -64,7 +64,7 @@ A: **如果不去看源码,想一下让自己设计会怎样?**
* 单击事件(onClickListener) 需要两个两个事件(ACTION_DOWN 和 ACTION_UP )才能触发,如果先分配给onClick判断,等它判断完,用户手指已经离开屏幕,黄花菜都凉了,定然造成 View 无法响应其他事件,应该最后调用。(最后)
* 长按事件(onLongClickListener) 同理,也是需要长时间等待才能出结果,肯定不能排到前面,但因为不需要ACTION_UP,应该排在 onClick 前面。(onLongClickListener > onClickListener)
* 触摸事件(onTouchListener) 如果用户注册了触摸事件,说明用户要自己处理触摸事件了,这个应该排在最前面。(最前)
-* View自身处理(onTouchEvent) 提供了一种默认的处理方式,如果用户已经处理好了,也就不需要了,所以应该排在 onClickListener 后面。(onClickListener > onTouchListener)
+* View自身处理(onTouchEvent) 提供了一种默认的处理方式,如果用户已经处理好了,也就不需要了,所以应该排在 onTouchListener 后面。(onTouchListener > onTouchEvent)
**所以事件的调度顺序应该是 `onTouchListener > onTouchEvent > onLongClickListener > onClickListener`**。
From 8caccc5aac60601fb609a30e8527f2e78a392034 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Sat, 22 Oct 2016 23:44:35 +0800
Subject: [PATCH 017/160] Update
---
Course/Markdown/markdown-editor.md | 65 ++++++++++++++++++++++++++++++
1 file changed, 65 insertions(+)
create mode 100644 Course/Markdown/markdown-editor.md
diff --git a/Course/Markdown/markdown-editor.md b/Course/Markdown/markdown-editor.md
new file mode 100644
index 00000000..796e6452
--- /dev/null
+++ b/Course/Markdown/markdown-editor.md
@@ -0,0 +1,65 @@
+自从接触了 Markdown 之后,就一直用 Markdown 作为自己的主要书写工具,不论是平时做一些简单的纪录,还是用来写博客,写文档都是非常方便。本文就是用 Markdown 进行书写的。
+
+> 我最早因为 GitHub 而了解到 Markdown,当是支持 Markdown 的平台并不多,现在发现很多平台都已经开始支持 Markdown了,不论是老牌的 CSDN 还是比较新的 简书、掘金、DiyCode 等都支持使用Markdown进行写作,借此趋势,赶紧向还不了解 Markdown 的魔法师强势安利一波。
+>
+> 如果你已经开始使用 Markdown了,那么本文作用对你可以能并不大,请看后续文章。
+
+
+
+## 什么是 Markdown ?
+
+**Markdown 是一种轻量级标记语言,创始人为約翰·格魯伯(John Gruber)。 它允许人们“使用易读易写的纯文本格式编写文档,然后转换成有效的XHTML(或者HTML)文档”。**
+
+相比于 HTML (~~How To Make Love~~ 大雾), **Markdown 更加精简,更加注重内容,其主要宗旨是「易读易写」** 一般 Markdown 最终都是要转换为 HTML 的,可用于书写博客或者网页,但借助某些工具,可以讲 Markdown 转换为 pdf,word,Latex 等其他常见的文件格式。
+
+
+
+## 为什么选择 Markdown ?
+
+**选择 Markdown 但理由只有一个:方便,节省时间!**
+
+至于为什么这样说,请看下面内容:
+
+* **语法简洁**,没有任何编程基础的人十几分钟语言即可入门。
+* **注重内容**,专注于内容编写,不再因为格式拍版而苦恼 (word格式刷工具哭晕在厕所)。
+* **易阅读性**,即便是没有经过转换的 Markdown 文件,大部分文字内容仍可阅读。
+* **易编辑性**,任何文本编辑器都能编辑 Markdown 文件。
+* **跨平台性**,任何平台均能打开 Markdown 文件,由于是纯文本文件,不存在格式兼容的问题。
+* **导出方便**,支持导出为 HTML,PDF,Word(.docx),LaTex 等常见格式(需要工具支持)。
+
+> 在 Windows 上编写的文档,非常方便的就能在 Mac 上继续编辑,方便数据迁移,降低沟通成本。
+
+
+
+## Markdown 存在的问题
+
+前面吹嘘了 Markdown 的那么多优点,下面就说一下其中的不足:
+
+* **图片问题**,很多人都觉得 Markdown 文件插入图片麻烦,还要自己上传找链接。
+* **语法兼容**,基础语法是兼容的,但不同工具(平台)的扩展语法不兼容(由于没有统一标准)。
+* **细节控制**,Markdown只提供最基础的格式,其显示样式主要由CSS控制,很难针对性的控制部分内容。
+
+> 以上应该是 Markdown 最常见的一些麻烦,不过不必担心,**后续文章会教大家来如何解决这些问题,取其精华,去其糟粕,让 Markdown 运用得心应手**。
+
+
+
+## Markdown 编辑器推荐
+
+俗话说,工欲善其事,必先利其器,虽然 Markdown 用任何文本编辑器都能打开编辑,但仍需要专业工具进行转化,常见 Markdown 编辑器我基本上都尝试用过,在此简单推荐几种,大家找适合自己的就行。
+
+**仅推荐本地编辑器,在线编辑器根据需要自己选择,很多平台都已经支持直接用 Markdown 进行编辑了。**
+
+| 编辑器 | 支持平台 | 支持导出格式 |
+| ---------------------------------------- | --------------------------- | ---------------------------------------- |
+| [**Typora**](http://www.typora.io/)
正在开发, 界面简洁, 对 HTML 语法支持较弱, 支持导出文件类型较多。 | Mac、
Windows、
Linux | HTML、
Word(.docx)、
PDF、
LaTex 等 |
+| [**EME**](https://eme.moe/)
正在开发, 界面简洁,对 HTML 语法支持友好,支持导出文件类型较少。 | Mac、
Windows、
Linux | 仅 PDF |
+| [**Mou**](http://25.io/mou/)
停止开发, 界面简洁, 对 HTML 语法支持友好, 但支持导出文件类型较少。 | Mac | HTML、
PDF |
+| [**Sublime Text**](http://www.sublimetext.com/3)
神兵利器,需要安装插件才能使用, 相对比较麻烦, 适合高级魔法师。 | Mac、
Windows、
Linux | 多种格式(插件) |
+| [**Atom**](https://atom.io/)
神兵利器, 支持多种常见的编程语言, 如果仅仅是为了写 Markdown 不推荐安装。 | Mac、
Windows、
Linux | HTML、
PDF(插件) |
+
+
+
+## 快速入门
+
+前面是为了帮助还不了解Markdown
+
From c6a9c902b3d4afcb7f1a22765168d8e09182a79f Mon Sep 17 00:00:00 2001
From: sloop
Date: Sun, 23 Oct 2016 23:34:08 +0800
Subject: [PATCH 018/160] Update
---
Course/Markdown/{markdown-editor.md => markdown-start.md} | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename Course/Markdown/{markdown-editor.md => markdown-start.md} (100%)
diff --git a/Course/Markdown/markdown-editor.md b/Course/Markdown/markdown-start.md
similarity index 100%
rename from Course/Markdown/markdown-editor.md
rename to Course/Markdown/markdown-start.md
From e02c1949645a5bf219f7c07706c8b700489a4134 Mon Sep 17 00:00:00 2001
From: sloop
Date: Mon, 24 Oct 2016 17:33:54 +0800
Subject: [PATCH 019/160] Update
---
CustomView/Base/[03]Color.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CustomView/Base/[03]Color.md b/CustomView/Base/[03]Color.md
index 77694a0c..86d3983c 100644
--- a/CustomView/Base/[03]Color.md
+++ b/CustomView/Base/[03]Color.md
@@ -82,6 +82,8 @@ RGB 从0x00到0xff表示颜色从浅到深。
``` java
int color = getResources().getColor(R.color.mycolor);
+
+ int color = getColor(R.color.myColor); //API 23 及以上支持该方法
```
### 4.在xml文件(layout或style)中引用或者创建颜色
From e0fbf45afbbc8e0681fd2893cb71e116e443f88d Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Tue, 25 Oct 2016 22:45:50 +0800
Subject: [PATCH 020/160] Update
---
Course/Markdown/markdown-grammar.md | 331 ++++++++++++++++++++++++++++
Course/Markdown/markdown-start.md | 125 ++++++++++-
2 files changed, 449 insertions(+), 7 deletions(-)
create mode 100644 Course/Markdown/markdown-grammar.md
diff --git a/Course/Markdown/markdown-grammar.md b/Course/Markdown/markdown-grammar.md
new file mode 100644
index 00000000..da5c9660
--- /dev/null
+++ b/Course/Markdown/markdown-grammar.md
@@ -0,0 +1,331 @@
+# Markdown基础语法
+
+为保证语法兼容性,本文只介绍基础语法,扩展语法等其它内容,会在后续的文章中单独介绍。
+**注意:所有的标记符号均使用英文,中文无效。**
+
+****
+
+## 标题
+
+Markdown 支持多种标题格式。
+
+利用 `=` (等号)和 `-`(减号)可以定义一级标题和二级标题,(任何数量的 `=` 和 `-` 都有效果) :
+
+
+
+ | Markdown |
+ 预览 |
+
+
+ 一级标题 ==== |
+ 一级标题 |
+
+
+ 二级标题 ---- |
+ 二级标题 |
+
+
+
+通过在行首插入 1 到 6 个 # ,来定义对应的 1 到 6 阶 标题,个人推荐使用这种,例如:
+
+
+
+ | Markdown |
+ 预览 |
+
+
+ | # 一级标题 |
+ 一级标题 |
+
+
+ | ## 二级标题 |
+ 二级标题 |
+
+
+ | ### 三级标题 |
+ 三级标题 |
+
+
+ | #### 四级标题 |
+ 四级标题 |
+
+
+ | ##### 五级标题 |
+ 五级标题 |
+
+
+ | ###### 六级标题 |
+ 六级标题 |
+
+
+
+****
+
+## 段落和换行
+
+在 Markdown 中段落由一行或者多行文本组成,相邻的两行文字会被视为同一段落,如果存在空行则被视为不同段落( Markdown 对空行的定义是看起来是空行就是空行,即使空行中存在 `空格` `TAB` `回车` 等不可见字符,同样会被视为空行)。
+
+Markdown支持段内换行,如果你想进行段落内换行可以在上一行结尾插入两个以上的空格后再回车。
+
+
+
+ | Markdown |
+ 预览 |
+
+
+
+ 第一行
+ 相邻被视为同一段落。
+ |
+
+
+ 第一行 相邻被视为同一段落。
+
+ |
+
+
+
+ 第一行[空格][空格]
+ 上一行结尾存在两个空格,段内换行
+ |
+
+
+ 第一行
+ 上一行结尾存在两个空格,段内换行。
+
+ |
+
+
+
+ 第一行
+
+ 两行之间存在空行,视为不同段落。
+ |
+
+
+ 第一行
+
+
+ 两行之间存在空行,视为不同段落。
+
+ |
+
+
+
+******
+
+## 强调
+
+删除线的 `~` 符号一般位于键盘左上角位置。
+
+
+
+ | Markdown |
+ 预览 |
+
+
+ | *倾斜* |
+
+ 倾斜
+ |
+
+
+ | **粗体** |
+ 粗体 |
+
+
+ | ~~删除线~~ |
+ 删除线 |
+
+
+ |
+ > 引用
+ |
+
+ 引用
+ |
+
+
+
+******
+
+## 链接和图片
+
+为了规避某些平台的防盗链机制,图片推荐使用图床,否则在不同平台上发布需要重新上传很麻烦的,图床最好选大平台的图床,一时半会不会倒闭的那种,个人目前主要用的是微博图床 [Chrome插件-微博图床](https://chrome.google.com/webstore/detail/%E6%96%B0%E6%B5%AA%E5%BE%AE%E5%8D%9A%E5%9B%BE%E5%BA%8A/fdfdnfpdplfbbnemmmoklbfjbhecpnhf?hl=zh-CN) 以及 [Alfred脚本-微博图床](https://imciel.com/2016/07/17/weibo-picture-upload-alfred-workflow/)
+
+
+
+ | Markdown |
+ 预览 |
+
+
+ | [GcsSloop](http://www.gcssloop.com) |
+ GcsSloop |
+
+
+ |  |
+  |
+
+
+
+******
+
+## 列表
+
+无序列表前面可以用 `*` `+` `-` 等,结果是相同的。
+有序列表的数字即便不按照顺序排列,结果仍是有序的。
+
+
+
+
+
+ | Markdown |
+ 预览 |
+
+
+
+ * 项目
+ * 项目
+ * 项目
+ * 子项目
+ * 子项目
+ * 项目
+ |
+
+
+ - 项目
+ - 项目
+ - 项目
+
+
+ - 项目
+
+ |
+
+
+
+
+ 1. 项目
+ 2. 项目
+ 3. 项目
+ 1. 子项目
+ 2. 子项目
+ 4. 项目
+ |
+
+
+ - 项目
+ - 项目
+ - 项目
+
+ - 子项目
+ - 子项目
+
+
+ - 项目
+
+ |
+
+
+
+****
+
+## 下划线和特殊符号
+
+由于 Markdown 使用一些特殊符号进行标记,当我们想要在文档中使用这些特殊符号并防止被 Markdown 转换的时候,可以使用 `\` (转义符) 将这些特殊符号进行转义。
+
+
+
+ | Markdown |
+ 预览 |
+
+
+
+ 在一行中用三个以上的星号、减号、下划线来建立一个分隔线
+ ---
+ |
+
+
+ |
+
+
+ 可以利用反斜杠(转义字符)来插入一些在语法中有特殊意义的符号
+ \*Hi\*
+ |
+ *Hi* |
+
+
+
+****
+
+## 代码
+
+### 1.行内代码
+
+行内代码可以使用反引号来标记(反引号一般位于键盘左上角,要用英文):
+
+```
+一句话 `行内代码` 一句话。
+```
+
+**预览:**
+
+一句话 `行内代码` 一句话。
+
+### 2.多行代码
+
+多行代码使用 3 个反引号来标记(反引号一般位于键盘左上角,要用英文) ,在第一个 ````` 后面可以跟语言类型,没有语言类型可以省略不写:
+
+```
+``` java
+// 我是注释
+int a = 5;
+```
+```
+
+**预览:**
+
+``` java
+// 我是注释
+int a = 5;
+```
+
+
+
+****
+
+## 表格
+
+**扩展的 Markdown 支持手写表格**,格式也非常简单,第二行分割线部分可以使用 `:` 来控制内容状态。
+**注意,Markdown 标准(原生)语法中没有表格支持,但现在多数平台已经支持了该语法,如 GitHub,CSDN,简书 等均支持,所以写在这里:**
+
+**Markdown:**
+
+```
+| 默认 | 靠右 | 居中 | 靠左 |
+| ---- | ---: | :--: | :--- |
+| 内容 | 内容 | 内容 | 内容 |
+| 内容 | 内容 | 条目 | 内容 |
+```
+
+**预览:**
+
+| 默认 | 靠右 | 居中 | 靠左 |
+| ---- | ---: | :--: | :--- |
+| 内容 | 内容 | 内容 | 内容 |
+| 内容 | 内容 | 条目 | 内容 |
+
+
+## 参考资料
+
+[Markdown-语法说明](http://www.markdown.cn/)
+
+
+## About Me
+
+### 作者微博: @GcsSloop
+
+
\ No newline at end of file
diff --git a/Course/Markdown/markdown-start.md b/Course/Markdown/markdown-start.md
index 796e6452..ffca84e8 100644
--- a/Course/Markdown/markdown-start.md
+++ b/Course/Markdown/markdown-start.md
@@ -1,10 +1,14 @@
-自从接触了 Markdown 之后,就一直用 Markdown 作为自己的主要书写工具,不论是平时做一些简单的纪录,还是用来写博客,写文档都是非常方便。本文就是用 Markdown 进行书写的。
+# Markdown 快速入门
+
+自从接触了 Markdown 之后,就一直用 Markdown 作为自己的主要书写工具,不论是平时做一些简单的纪录,还是用来写博客,写文档都是非常方便。你现在看到的这篇文章就是用 Markdown 进行书写的。
> 我最早因为 GitHub 而了解到 Markdown,当是支持 Markdown 的平台并不多,现在发现很多平台都已经开始支持 Markdown了,不论是老牌的 CSDN 还是比较新的 简书、掘金、DiyCode 等都支持使用Markdown进行写作,借此趋势,赶紧向还不了解 Markdown 的魔法师强势安利一波。
>
-> 如果你已经开始使用 Markdown了,那么本文作用对你可以能并不大,请看后续文章。
+> **如果你已经开始使用 Markdown了,那么本文作用对你可以能并不大,请看后续文章。**
+
+****
## 什么是 Markdown ?
@@ -14,6 +18,8 @@
+****
+
## 为什么选择 Markdown ?
**选择 Markdown 但理由只有一个:方便,节省时间!**
@@ -27,10 +33,12 @@
* **跨平台性**,任何平台均能打开 Markdown 文件,由于是纯文本文件,不存在格式兼容的问题。
* **导出方便**,支持导出为 HTML,PDF,Word(.docx),LaTex 等常见格式(需要工具支持)。
-> 在 Windows 上编写的文档,非常方便的就能在 Mac 上继续编辑,方便数据迁移,降低沟通成本。
+在 Windows 上编写的文档,非常方便的就能在 Mac 上继续编辑,方便数据迁移,降低沟通成本。
+****
+
## Markdown 存在的问题
前面吹嘘了 Markdown 的那么多优点,下面就说一下其中的不足:
@@ -39,9 +47,9 @@
* **语法兼容**,基础语法是兼容的,但不同工具(平台)的扩展语法不兼容(由于没有统一标准)。
* **细节控制**,Markdown只提供最基础的格式,其显示样式主要由CSS控制,很难针对性的控制部分内容。
-> 以上应该是 Markdown 最常见的一些麻烦,不过不必担心,**后续文章会教大家来如何解决这些问题,取其精华,去其糟粕,让 Markdown 运用得心应手**。
-
+以上应该是 Markdown 最常见的一些麻烦,不过不必担心,**后续文章会教大家来如何解决这些问题,取其精华,去其糟粕,让 Markdown 运用得心应手**。
+****
## Markdown 编辑器推荐
@@ -57,9 +65,112 @@
| [**Sublime Text**](http://www.sublimetext.com/3)
神兵利器,需要安装插件才能使用, 相对比较麻烦, 适合高级魔法师。 | Mac、
Windows、
Linux | 多种格式(插件) |
| [**Atom**](https://atom.io/)
神兵利器, 支持多种常见的编程语言, 如果仅仅是为了写 Markdown 不推荐安装。 | Mac、
Windows、
Linux | HTML、
PDF(插件) |
-
+****
## 快速入门
-前面是为了帮助还不了解Markdown
+本文是为了帮助还不了解 Markdown 的魔法师有一个简单的认知,所以快速入门只说最基本的一些内容。
+
+
+
+### 标题
+
+通过在行首插入 1 到 6 个 `#` ,来定义对应的 1 到 6 阶 标题:
+
+
+
+ | Markdown |
+ 预览 |
+
+
+ | # 一级标题 |
+ 一级标题 |
+
+
+ | ## 二级标题 |
+ 二级标题 |
+
+
+ | ### 三级标题 |
+ 三级标题 |
+
+
+
+
+
+### 分段
+
+在 Markdown 中段落由一行或者多行文本组成,相邻的两行文字会被视为同一段落,如果存在空行则被视为不同段落( Markdown 对空行的定义是看起来是空行就是空行,即使空行中存在 `空格` `TAB` `回车` 等不可见字符,同样会被视为空行)。
+
+
+
+ | Markdown |
+ 预览 |
+
+
+
+ 第一行
+ 相邻被视为同一段落。
+ |
+
+
+ 第一行 相邻被视为同一段落。
+
+ |
+
+
+
+ 第一行
+
+ 两行之间存在空行,视为不同段落。
+ |
+
+
+ 第一行
+
+
+ 两行之间存在空行,视为不同段落。
+
+ |
+
+
+
+
+
+### 链接和图片
+
+
+
+ | Markdown |
+ 预览 |
+
+
+ | [GcsSloop](http://www.gcssloop.com) |
+ GcsSloop |
+
+
+ |  |
+  |
+
+
+
+
+
+知道了上面这些内容就已经算是入门了,可以用 Markdown 进行快乐的写作,如果想了解更多语法相关内容,请看下一篇 [Markdown实用技巧-基础语法][markdown-grammar]
+
+
+## 参考资料
+
+[Markdown-基础语法](http://www.markdown.cn/)
+
+
+## About Me
+
+### 作者微博: @GcsSloop
+
+
+
+[markdown-grammar]: /markdown/markdown-grammar "Markdown实用技巧-语法"
+
+
From 1a8e20396555b11efe5617dd0077af301b918e22 Mon Sep 17 00:00:00 2001
From: sloop
Date: Fri, 28 Oct 2016 21:15:38 +0800
Subject: [PATCH 021/160] Update
---
README.md | 3 +++
1 file changed, 3 insertions(+)
diff --git a/README.md b/README.md
index ebecd7b4..a6d027ac 100644
--- a/README.md
+++ b/README.md
@@ -45,6 +45,9 @@
* [优雅的发布Android开源库(论JitPack的优越性)](https://github.com/GcsSloop/AndroidNote/blob/master/Course/ReleaseLibraryByJitPack.md)
* [用JitPack发布时附加文档和源码](https://github.com/GcsSloop/AndroidNote/blob/master/Course/jitpack-javadoc.md)
+* [Markdown 快速入门](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-start.md)
+* [Markdown 基础语法](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-grammar.md)
+
******
## Tips
From ab011079e4c3ac91a381ffba2df5ca6d3d1c5c5d Mon Sep 17 00:00:00 2001
From: sloop
Date: Fri, 28 Oct 2016 21:16:40 +0800
Subject: [PATCH 022/160] Update
---
README.md | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index a6d027ac..176fc41d 100644
--- a/README.md
+++ b/README.md
@@ -45,7 +45,9 @@
* [优雅的发布Android开源库(论JitPack的优越性)](https://github.com/GcsSloop/AndroidNote/blob/master/Course/ReleaseLibraryByJitPack.md)
* [用JitPack发布时附加文档和源码](https://github.com/GcsSloop/AndroidNote/blob/master/Course/jitpack-javadoc.md)
-* [Markdown 快速入门](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-start.md)
+------
+
+* [Markdown 快速入门](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-start.md)
* [Markdown 基础语法](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-grammar.md)
******
From 966b91d89159cc0a9385df34f115e62646033da2 Mon Sep 17 00:00:00 2001
From: sloop
Date: Sat, 29 Oct 2016 22:15:04 +0800
Subject: [PATCH 023/160] Update
---
Course/Markdown/markdown-grammar.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Course/Markdown/markdown-grammar.md b/Course/Markdown/markdown-grammar.md
index da5c9660..a84c9f40 100644
--- a/Course/Markdown/markdown-grammar.md
+++ b/Course/Markdown/markdown-grammar.md
@@ -1,4 +1,4 @@
-# Markdown基础语法
+# Markdown 基础语法
为保证语法兼容性,本文只介绍基础语法,扩展语法等其它内容,会在后续的文章中单独介绍。
**注意:所有的标记符号均使用英文,中文无效。**
@@ -328,4 +328,4 @@ int a = 5;
### 作者微博: @GcsSloop
-
\ No newline at end of file
+
From 1d2bdb1c9beb3ac03c9b317579064ce2e7c838af Mon Sep 17 00:00:00 2001
From: sloop
Date: Sun, 30 Oct 2016 22:41:47 +0800
Subject: [PATCH 024/160] Update
---
CustomView/Advance/[16]MotionEvent | 1 +
1 file changed, 1 insertion(+)
create mode 100644 CustomView/Advance/[16]MotionEvent
diff --git a/CustomView/Advance/[16]MotionEvent b/CustomView/Advance/[16]MotionEvent
new file mode 100644
index 00000000..f34de326
--- /dev/null
+++ b/CustomView/Advance/[16]MotionEvent
@@ -0,0 +1 @@
+# MotionEvent
From 0457d83787ba151bec0d721d5f2e17ff7d5fc347 Mon Sep 17 00:00:00 2001
From: sloop
Date: Sun, 30 Oct 2016 22:42:12 +0800
Subject: [PATCH 025/160] Update
---
CustomView/Advance/{[16]MotionEvent => [16]MotionEvent.md} | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename CustomView/Advance/{[16]MotionEvent => [16]MotionEvent.md} (100%)
diff --git a/CustomView/Advance/[16]MotionEvent b/CustomView/Advance/[16]MotionEvent.md
similarity index 100%
rename from CustomView/Advance/[16]MotionEvent
rename to CustomView/Advance/[16]MotionEvent.md
From 8f4c4f26f041908cc5c42f9c832c9149d3652e73 Mon Sep 17 00:00:00 2001
From: sloop
Date: Tue, 1 Nov 2016 23:17:27 +0800
Subject: [PATCH 026/160] Update
---
Course/Markdown/markdown-link.md | 1 +
1 file changed, 1 insertion(+)
create mode 100644 Course/Markdown/markdown-link.md
diff --git a/Course/Markdown/markdown-link.md b/Course/Markdown/markdown-link.md
new file mode 100644
index 00000000..7d82df34
--- /dev/null
+++ b/Course/Markdown/markdown-link.md
@@ -0,0 +1 @@
+# Markdown
From f39da495322b9db8991c8875ecf2791a49bac0ee Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Wed, 2 Nov 2016 09:43:18 +0800
Subject: [PATCH 027/160] Update
---
CustomView/Advance/[16]MotionEvent.md | 308 +++++++++++++++++++++++++-
1 file changed, 307 insertions(+), 1 deletion(-)
diff --git a/CustomView/Advance/[16]MotionEvent.md b/CustomView/Advance/[16]MotionEvent.md
index f34de326..8d0548d2 100644
--- a/CustomView/Advance/[16]MotionEvent.md
+++ b/CustomView/Advance/[16]MotionEvent.md
@@ -1 +1,307 @@
-# MotionEvent
+# Android MotionEvent 详解
+
+Android MotionEvent 详解,之前用了两篇文章 [事件分发机制原理][customview/dispatch-touchevent-theory] 和 [事件分发机制详解][customview/dispatch-touchevent-source] 来讲解事件分发,而作为事件分发主角之一的 MotionEvent 并没有过多的说明,本文就带大家了解 MotionEvent 的相关内容,简要介绍触摸事件,主要包括 单点触控、多点触控、鼠标事件 以及 getAction() 和 getActionMasked() 的区别。
+
+Android 将所有的输入事件都放在了 MotionEvent 中,随着安卓的不断发展壮大,MotionEvent 也开始变得越来越复杂,下面是我自己整理的 MotionEvent 大事记:
+
+| 版本号 | 更新内容 |
+| :-------------------: | --------------------------- |
+| Android 1.0 (API 1 ) | 支持单点触控和轨迹球的事件。 |
+| Android 1.6 (API 4 ) | 支持手势。 |
+| Android 2.2 (API 8 ) | 支持多点触控。 |
+| Android 3.1 (API 12) | 支持触控笔,鼠标,键盘,操纵杆,游戏控制器等输入工具。 |
+
+以上仅仅是简要的说明几次比较大的变动,细小的修复和更新不计其数,此处就不一一列出了,反正也没人关心这些东西。
+MotionEvent 负责集中处理所有类型设备的输入事件,但是由于某些设备使用的几率较小本文会忽略讲解,或者简要讲解,例如:
+1、轨迹球只出现在最早的设备上,现代的设备上已经见不到了,本文不再叙述。
+2、触控笔和手指处理流程基本相同,不再多说。
+3、鼠标在手机上使用概率也比较小,会在文末简要介绍。
+
+
+
+## 单点触控
+
+单点触控就非常简单啦,入门的工程师都会用,上一篇文章也简要介绍过,主要涉及以下几个事件:
+
+| 事件 | 简介 |
+| -------------- | ------------------------ |
+| ACTION_DOWN | 手指 **初次接触到屏幕** 时触发。 |
+| ACTION_MOVE | 手指 **在屏幕上滑动** 时触发,会多次触发。 |
+| ACTION_UP | 手指 **离开屏幕** 时触发。 |
+| ACTION_CANCEL | 事件 **被上层拦截** 时触发。 |
+| ACTION_OUTSIDE | 手指 **不在控件区域** 时触发。 |
+
+和以下的几个方法:
+
+| 方法 | 简介 |
+| :---------- | ---------------------- |
+| getAction() | 获取事件类型。 |
+| getX() | 获得触摸点在当前 View 的 X 轴坐标。 |
+| getY() | 获得触摸点在当前 View 的 Y 轴坐标。 |
+| getRawX() | 获得触摸点在整个屏幕的 X 轴坐标。 |
+| getRawY() | 获得触摸点在整个屏幕的 Y 轴坐标。 |
+
+> 关于 `get` 和 `getRaw` 的区别可以参考这一篇文章 [安卓自定义View基础-坐标系][customview/CoordinateSystem]
+
+单点触控一次简单的交互流程是这样的:
+
+**手指落下(ACTION_DOWN) -> 多次移动(ACTION_MOVE) -> 离开(ACTION_UP)**
+
+> - 本次事例中 ACTION_MOVE 有多次触发。
+> - 如果仅仅是单击(手指按下再抬起),不会触发 ACTION_MOVE。
+
+
+
+针对单点触控的事件处理一般是这样写的:
+
+```java
+@Override
+public boolean onTouchEvent(MotionEvent event) {
+ // ▼ 注意这里使用的是 getAction(),先埋一个小尾巴。
+ switch (event.getAction()){
+ case MotionEvent.ACTION_DOWN:
+ // 手指按下
+ break;
+ case MotionEvent.ACTION_MOVE:
+ // 手指移动
+ break;
+ case MotionEvent.ACTION_UP:
+ // 手指抬起
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ // 事件被拦截
+ break;
+ case MotionEvent.ACTION_OUTSIDE:
+ // 超出区域
+ break;
+ }
+ return super.onTouchEvent(event);
+}
+```
+
+相信小伙伴对此已经非常熟悉了,经常使用的东西,我也不啰嗦了。
+
+但其中有两个比较特殊的事件: `ACTION_CANCEL` 和 `ACTION_OUTSIDE` 。
+为什么说特殊呢,因为它们是由程序触发而产生的,而且触发条件也非常特殊,通常情况下即便不处理这两个事件也没有什么问题。接下来我们就扒一扒它们的真面目:
+
+### ACTION_CANCEL
+
+**`ACTION_CANCEL` 的触发条件是事件被上层拦截**,然而我们在 [事件分发机制原理][customview/dispatch-touchevent-theory] 一文中了解到当事件被上层 View 拦截的时候,ChildView 是收不到任何事件的,ChildView 收不到任何事件,自然也不会收到 `ACTION_CANCEL` 了,所以说这个 `ACTION_CANCEL` 的正确触发条件并不是这样,那么是什么呢?
+
+**事实上,只有上层 View 回收事件处理权的时候,ChildView 才会收到一个 `ACTION_CANCEL` 事件。**
+
+这样说可能不太容易不理解,咱举个例子?
+
+
+
+> 例如:上层 View 是一个 RecyclerView,它收到了一个 `ACTION_DOWN` 事件,由于这个可能是点击事件,所以它先传递给对应 ItemView,询问 ItemView 是否需要这个事件,然而接下来又传递过来了一个 `ACTION_MOVE` 事件,且移动的方向和 RecyclerView 的可滑动方向一致,所以 RecyclerView 判断这个事件是滚动事件,于是要收回事件处理权,这时候对应的 ItemView 会收到一个 `ACTION_CANCEL` ,并且不会再收到后续事件。
+>
+> **通俗一点?**
+>
+> RecyclerView:儿砸,这里有一个 `ACTION_DOWN` 你看你要不要。
+> ItemView :好嘞,我看看。
+> RecyclerView:噫?居然是移动事件`ACTION_MOVE`,我要滚起来了,儿砸,我可能要把你送去你姑父家(缓存区)了,在这之前给你一个 `ACTION_CANCEL`,你要收好啊。
+> ItemView :…...
+>
+> 这是实际开发中最有可能见到 `ACTION_CANCEL` 的场景了。
+
+
+
+### ACTION_OUTSIDE
+
+`ACTION_OUTSIDE`的触发条件更加奇葩,从字面上看,outside 意思不就是超出区域么?然而不论你如何滑动超出控件区域都不会触发 `ACTION_OUTSIDE` 这个事件。相信很多魔法师都对此很是疑惑,说好的超出区域呢?
+
+实际上这个事件根本就不是在这里用的,看官方解释(装一下逼):
+
+> A movement has happened outside of the normal bounds of the UI element. This does not provide a full gesture, but only the initial location of the movement/touch.
+>
+> 一个触摸事件已经发生了UI元素的正常范围之外。因此不再提供完整的手势,只提供 运动/触摸 的初始位置。
+
+我们知道,正常情况下,如果初始点击位置在该视图区域之外,该视图根本不可能会收到事件,然而,万事万物都不是绝对的,肯定还有一些特殊情况,你可曾还记得点击 Dialog 区域外关闭吗?Dialog 就是一个特殊的视图(没有占满屏幕大小的窗口),能够接收到视图区域外的事件(虽然在通常情况下你根本用不到这个事件),除了 Dialog 之外,你最可能看到这个事件的场景是悬浮窗,当然啦,想要接收到视图之外的事件需要一些特殊的设置。
+
+> 设置视图的 WindowManager 布局参数的 flags为[`FLAG_WATCH_OUTSIDE_TOUCH`](http://developer.android.com/reference/android/view/WindowManager.LayoutParams.html#FLAG_WATCH_OUTSIDE_TOUCH),这样点击事件发生在这个视图之外时,该视图就可以接收到一个 `ACTION_OUTSIDE` 事件。
+>
+> 参见StackOverflow:[How to dismiss the dialog with click on outside of the dialog?](http://stackoverflow.com/questions/8384067/how-to-dismiss-the-dialog-with-click-on-outside-of-the-dialog)
+
+由于这个事件用到的几率比较小,此处就不展开叙述了,以后用到的时候再详细讲解。
+
+
+
+## 多点触控
+
+Android 在 2.2 版本的时候开始支持多点触控,一旦出现了多点触控,很多东西就突然之间变得麻烦起来了,首先要解决的问题就是 **多个手指同时按在屏幕上,会产生很多的事件,这些事件该如何区分呢?**
+
+为了区分这些事件,工程师们用了一个很简单的办法--**编号,当手指第一次按下时产生一个唯一的号码,手指抬起或者事件被拦截就回收编号,就这么简单。**
+
+**第一次按下的手指特殊处理作为主指针,之后按下的手指作为辅助指针**,然后随之衍生出来了以下事件(注意增加的事件和事件简介的变化):
+
+| 事件 | 简介 |
+| --------------------------- | ------------------------------ |
+| ACTION_DOWN | **第一个** 手指 **初次接触到屏幕** 时触发。 |
+| ACTION_MOVE | 手指 **在屏幕上滑动** 时触发,会多次触发。 |
+| ACTION_UP | **最后一个** 手指 **离开屏幕** 时触发。 |
+| **ACTION_POINTER_DOWN** | 有非主要的手指按下(**即按下之前已经有手指在屏幕上**)。 |
+| **ACTION_POINTER_UP** | 有非主要的手指抬起(**即抬起之后仍然有手指在屏幕上**)。 |
+| 以下事件类型不推荐使用 | ------------------ |
+| ~~ACTION_POINTER\_1\_DOWN~~ | 第 2 个手指按下,已废弃,不推荐使用。 |
+| ~~ACTION_POINTER\_2\_DOWN~~ | 第 3 个手指按下,已废弃,不推荐使用。 |
+| ~~ACTION_POINTER\_3\_DOWN~~ | 第 4 个手指按下,已废弃,不推荐使用。 |
+| ~~ACTION_POINTER\_1\_UP~~ | 第 2 个手指抬起,已废弃,不推荐使用。 |
+| ~~ACTION_POINTER\_2\_UP~~ | 第 3 个手指抬起,已废弃,不推荐使用。 |
+| ~~ACTION_POINTER\_3\_UP~~ | 第 4 个手指抬起,已废弃,不推荐使用。 |
+
+和以下方法:
+
+| 方法 | 简介 |
+| ------------------------------- | ---------------------------------------- |
+| getActionMasked() | 与 `getAction()` 类似,**多点触控必须使用这个方法获取事件类型**。 |
+| getActionIndex() | 获取该事件是哪个指针(手指)产生的。 |
+| getPointerCount() | 获取在屏幕上手指的个数。 |
+| getPointerId(int pointerIndex) | 获取一个指针(手指)的唯一标识符ID,在手指按下和抬起之间ID始终不变。 |
+| findPointerIndex(int pointerId) | 通过PointerId获取到当前状态下PointIndex,之后通过PointIndex获取其他内容。 |
+| getX(int pointerIndex) | 获取某一个指针(手指)的X坐标 |
+| getY(int pointerIndex) | 获取某一个指针(手指)的Y坐标 |
+
+由于多点触控部分涉及内容比较多,也很复杂,我准备单独用一篇文章进行详细叙述,所以这里只叙述一些基础的内容作为铺垫:
+
+### getAction() 与 getActionMasked()
+
+当多个手指在屏幕上按下的时候,会产生大量的事件,如何在获取事件类型的同时区分这些事件就是一个大问题了。
+
+一般来说我们可以通过为事件添加一个int类型的index属性来区分,但是我们知道谷歌工程师是有洁癖的(在 [自定义View分类与流程][customview/CustomViewProcess] 的onMeasure中已经见识过了),为了添加一个通常数值不会超过10的index属性就浪费一个int大小的空间简直是不能忍受的,于是工程师们将这个index属性和事件类型直接合并了。
+
+int类型共32位(0x00000000),他们用最低8位(0x000000**ff**)表示事件类型,再往前的8位(0x0000**ff**00)表示事件编号,以手指按下为例讲解数值是如何合成的:
+
+> ACTION_DOWN 的默认数值为 (0x00000000)
+> ACTION_POINTER_DOWN 的默认数值为 (0x00000005)
+
+| 手指按下 | 触发事件(数值) |
+| :-----: | :--------------------------------------- |
+| 第1个手指按下 | ACTION_DOWN (0x0000**00**00) |
+| 第2个手指按下 | ACTION_POINTER_DOWN (0x0000**01**05) |
+| 第3个手指按下 | ACTION_POINTER_DOWN (0x0000**02**05) |
+| 第4个手指按下 | ACTION_POINTER_DOWN (0x0000**03**05) |
+
+**注意:**
+上面表格中用粗体标示出的数值,可以看到随着按下手指数量的增加,这个数值也是一直变化的,进而导致我们使用 `getAction()` 获取到的数值无法与标准的事件类型进行对比,为了解决这个问题,他们创建了一个 `getActionMasked()` 方法,这个方法可以清除index数值,让其变成一个标准的事件类型。
+**1、多点触控时必须使用 `getActionMasked()` 来获取事件类型。**
+**2、单点触控时由于事件数值不变,使用 `getAction()` 和 `getActionMasked()` 两个方法都可以。**
+**3、使用 getActionIndex() 可以获取到这个index数值。不过请注意,getActionIndex() 只在 down 和 up 时有效,move 时是无效的。**
+
+### PointId
+
+虽然前面刚刚说了一个 actionIndex,可以使用 getActionIndex() 获得,但通过 actionIndex 字面意思知道,这个只表示事件的序号,而且根据其说明文档解释,这个 ActionIndex 只有在手指按下(down)和抬起(up)时是有用的,在移动(move)时是没有用的,事件追踪非常重要的一环就是移动(move),然而它却没卵用,这也太不实在了 ( ̄Д ̄)ノ
+
+**郑重声明:追踪事件流,请认准 PointId,这是唯一官方指定标准,不要相信 ActionIndex 那个小婊砸。**
+
+PointId 在手指按下时产生,手指抬起或者事件被取消后消失,是一个事件流程中唯一不变的标识,可以在手指按下时 通过 `getPointerId(int pointerIndex)` 获得。 (参数中的 pointerIndex 就是 actionIndex)
+
+关于事件流的追踪等问题在讲解多点触控时再详细讲解。
+
+
+
+## 历史数据(批处理)
+
+由于我们的设备非常灵敏,手指稍微移动一下就会产生一个移动事件,所以移动事件会产生的特别频繁,为了提高效率,设计师会将近期的多个移动事件(move)按照事件发生的顺序进行排序打包放在同一个 MotionEvent 中,与之对应的产生了以下方法:
+
+| 事件 | 简介 |
+| --------------------------------- | ---------------------------------------- |
+| getHistorySize() | 获取历史事件集合大小 |
+| getHistoricalX(int pos) | 获取第pos个历史事件x坐标
(pos < getHistorySize()) |
+| getHistoricalY(int pos) | 获取第pos个历史事件y坐标
(pos < getHistorySize()) |
+| getHistoricalX (int pin, int pos) | 获取第pin个手指的第pos个历史事件x坐标
(pin < getPointerCount(), pos < getHistorySize() ) |
+| getHistoricalY (int pin, int pos) | 获取第pin个手指的第pos个历史事件y坐标
(pin < getPointerCount(), pos < getHistorySize() ) |
+
+**注意:**
+
+1. pin 全称是 pointerIndex,表示第几个手指,此处为了节省空间使用了缩写。
+2. 历史数据只有 ACTION_MOVE 事件。
+3. 历史数据单点触控和多点触控均可以用。
+
+下面是官方文档给出的一个简单使用示例:
+
+```java
+void printSamples(MotionEvent ev) {
+ final int historySize = ev.getHistorySize();
+ final int pointerCount = ev.getPointerCount();
+ for (int h = 0; h < historySize; h++) {
+ System.out.printf("At time %d:", ev.getHistoricalEventTime(h));
+ for (int p = 0; p < pointerCount; p++) {
+ System.out.printf(" pointer %d: (%f,%f)",
+ ev.getPointerId(p), ev.getHistoricalX(p, h), ev.getHistoricalY(p, h));
+ }
+ }
+ System.out.printf("At time %d:", ev.getEventTime());
+ for (int p = 0; p < pointerCount; p++) {
+ System.out.printf(" pointer %d: (%f,%f)",
+ ev.getPointerId(p), ev.getX(p), ev.getY(p));
+ }
+}
+```
+
+
+
+
+
+## 鼠标事件
+
+由于触控笔事件和手指事件处理流程大致相同,所以就不讲解了,这里讲解一下与鼠标相关的几个事件:
+
+| 事件 | 简介 |
+| ------------------ | ---------------------------------------- |
+| ACTION_HOVER_ENTER | 指针移入到窗口或者View区域,但没有按下。 |
+| ACTION_HOVER_MOVE | 指针在窗口或者View区域移动,但没有按下。 |
+| ACTION_HOVER_EXIT | 指针移出到窗口或者View区域,但没有按下。 |
+| ACTION_SCROLL | 滚轮滚动,可以触发水平滚动(AXIS_HSCROLL)或者垂直滚动(AXIS_VSCROLL) |
+
+注意:
+
+1、这些事件类型是 安卓4.0 (API 14) 才添加的。
+2、使用 ` getActionMasked()` 获得这些事件类型。
+3、这些事件不会传递到 [onTouchEvent(MotionEvent)](https://developer.android.com/reference/android/view/View.html#onTouchEvent(android.view.MotionEvent)) 而是传递到 [onGenericMotionEvent(MotionEvent)](https://developer.android.com/reference/android/view/View.html#onGenericMotionEvent(android.view.MotionEvent)) 。
+
+
+
+## 输入设备类型判断
+
+输入设备类型判断也是安卓4.0 (API 14) 才添加的,主要包括以下几种设备:
+
+| 设备类型 | 简介 |
+| ----------------- | ---- |
+| TOOL_TYPE_ERASER | 橡皮擦 |
+| TOOL_TYPE_FINGER | 手指 |
+| TOOL_TYPE_MOUSE | 鼠标 |
+| TOOL_TYPE_STYLUS | 手写笔 |
+| TOOL_TYPE_UNKNOWN | 未知类型 |
+
+**使用 `getToolType(int pointerIndex)` 来获取对应的输入设备类型,pointIndex可以为0,但必须小于 `getPointerCount()`。**
+
+
+
+## 总结
+
+虽然本文标题是 MotionEvent 详解,但由于 MotionEvent 实在太庞大了,本文只能涉及一些比较常用的内容,某些不太常用的内容就在以后用到的时候再详细介绍吧,像游戏手柄等输入设备由于我暂时不做游戏开发,也没有过多了解,所以就不介绍给大家啦。
+
+由于个人水平有限,文章中可能会出现错误,如果你觉得哪一部分有错误,或者发现了错别字等内容,欢迎在评论区告诉我,另外,据说关注 [作者微博](http://weibo.com/GcsSloop) 不仅能第一时间收到新文章消息,还能变帅哦。
+
+
+
+## 参考资料
+
+[MotionEvent ](https://developer.android.com/reference/android/view/MotionEvent.html)
+[Android MotionEvent详解](http://www.jianshu.com/p/0c863bbde8eb)
+
+
+
+## About Me
+
+### 作者微博: @GcsSloop
+
+
+
+
+
+[customview/CoordinateSystem]: http://www.gcssloop.com/customview/CoordinateSystem "安卓自定义View基础-坐标系"
+[customview/CustomViewProcess]: http://www.gcssloop.com/customview/CustomViewProcess "安卓自定义View进阶-分类与流程"
+[customview/dispatch-touchevent-theory]: http://www.gcssloop.com/customview/dispatch-touchevent-theory "安卓自定义View进阶-事件分发机制原理"
+[customview/dispatch-touchevent-source]: http://www.gcssloop.com/customview/dispatch-touchevent-source "安卓自定义View进阶-事件分发机制详解"
\ No newline at end of file
From 6bde98d0373d39ec3cc99776a1c9fbe9a2508d36 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Thu, 3 Nov 2016 23:17:59 +0800
Subject: [PATCH 028/160] Update
---
CustomView/Advance/[16]MotionEvent.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CustomView/Advance/[16]MotionEvent.md b/CustomView/Advance/[16]MotionEvent.md
index 8d0548d2..5be26dd8 100644
--- a/CustomView/Advance/[16]MotionEvent.md
+++ b/CustomView/Advance/[16]MotionEvent.md
@@ -202,7 +202,7 @@ PointId 在手指按下时产生,手指抬起或者事件被取消后消失,
## 历史数据(批处理)
-由于我们的设备非常灵敏,手指稍微移动一下就会产生一个移动事件,所以移动事件会产生的特别频繁,为了提高效率,设计师会将近期的多个移动事件(move)按照事件发生的顺序进行排序打包放在同一个 MotionEvent 中,与之对应的产生了以下方法:
+由于我们的设备非常灵敏,手指稍微移动一下就会产生一个移动事件,所以移动事件会产生的特别频繁,为了提高效率,系统会将近期的多个移动事件(move)按照事件发生的顺序进行排序打包放在同一个 MotionEvent 中,与之对应的产生了以下方法:
| 事件 | 简介 |
| --------------------------------- | ---------------------------------------- |
From a8ac3aa01490cf5fbd1fa4032913aeefb5cc842c Mon Sep 17 00:00:00 2001
From: sloop
Date: Fri, 4 Nov 2016 17:12:41 +0800
Subject: [PATCH 029/160] Update
---
README.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/README.md b/README.md
index 176fc41d..6449598c 100644
--- a/README.md
+++ b/README.md
@@ -35,6 +35,8 @@
* [安卓自定义View进阶 - Matrix Camera](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B11%5DMatrix_3D_Camera.md)
* [安卓自定义View进阶 - 事件分发机制原理](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B12%5DDispatch-TouchEvent-Theory.md)
* [安卓自定义View进阶 - 事件分发机制详解](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B15%5DDispatch-TouchEvent-Source.md)
+ * [安卓自定义View进阶 - MotionEvent 详解](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B16%5DMotionEvent.md)
+
******
From 7d37cb7fcd2daebd4e7afec4a00288be6e1050bf Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Fri, 4 Nov 2016 17:14:17 +0800
Subject: [PATCH 030/160] Update
---
CustomView/Advance/[16]MotionEvent.md | 69 +++++++++++++++++++++++++--
1 file changed, 65 insertions(+), 4 deletions(-)
diff --git a/CustomView/Advance/[16]MotionEvent.md b/CustomView/Advance/[16]MotionEvent.md
index 5be26dd8..6daca725 100644
--- a/CustomView/Advance/[16]MotionEvent.md
+++ b/CustomView/Advance/[16]MotionEvent.md
@@ -1,4 +1,4 @@
-# Android MotionEvent 详解
+# MotionEvent 详解
Android MotionEvent 详解,之前用了两篇文章 [事件分发机制原理][customview/dispatch-touchevent-theory] 和 [事件分发机制详解][customview/dispatch-touchevent-source] 来讲解事件分发,而作为事件分发主角之一的 MotionEvent 并没有过多的说明,本文就带大家了解 MotionEvent 的相关内容,简要介绍触摸事件,主要包括 单点触控、多点触控、鼠标事件 以及 getAction() 和 getActionMasked() 的区别。
@@ -47,8 +47,8 @@ MotionEvent 负责集中处理所有类型设备的输入事件,但是由于
**手指落下(ACTION_DOWN) -> 多次移动(ACTION_MOVE) -> 离开(ACTION_UP)**
-> - 本次事例中 ACTION_MOVE 有多次触发。
-> - 如果仅仅是单击(手指按下再抬起),不会触发 ACTION_MOVE。
+> * 本次事例中 ACTION_MOVE 有多次触发。
+> * 如果仅仅是单击(手指按下再抬起),不会触发 ACTION_MOVE。

@@ -84,6 +84,8 @@ public boolean onTouchEvent(MotionEvent event) {
但其中有两个比较特殊的事件: `ACTION_CANCEL` 和 `ACTION_OUTSIDE` 。
为什么说特殊呢,因为它们是由程序触发而产生的,而且触发条件也非常特殊,通常情况下即便不处理这两个事件也没有什么问题。接下来我们就扒一扒它们的真面目:
+
+
### ACTION_CANCEL
**`ACTION_CANCEL` 的触发条件是事件被上层拦截**,然而我们在 [事件分发机制原理][customview/dispatch-touchevent-theory] 一文中了解到当事件被上层 View 拦截的时候,ChildView 是收不到任何事件的,ChildView 收不到任何事件,自然也不会收到 `ACTION_CANCEL` 了,所以说这个 `ACTION_CANCEL` 的正确触发条件并不是这样,那么是什么呢?
@@ -188,6 +190,21 @@ int类型共32位(0x00000000),他们用最低8位(0x000000**ff**)表示事件
**2、单点触控时由于事件数值不变,使用 `getAction()` 和 `getActionMasked()` 两个方法都可以。**
**3、使用 getActionIndex() 可以获取到这个index数值。不过请注意,getActionIndex() 只在 down 和 up 时有效,move 时是无效的。**
+目前来说获取事件类型使用 `getActionMasked()` 就行了,但是如果一定要编译时兼容古董版本的话,可以考虑使用这样的写法:
+
+```java
+final int action = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO)
+ ? event.getActionMasked()
+ : event.getAction();
+switch (action){
+ case MotionEvent.ACTION_DOWN:
+ // TODO
+ break;
+}
+```
+
+
+
### PointId
虽然前面刚刚说了一个 actionIndex,可以使用 getActionIndex() 获得,但通过 actionIndex 字面意思知道,这个只表示事件的序号,而且根据其说明文档解释,这个 ActionIndex 只有在手指按下(down)和抬起(up)时是有用的,在移动(move)时是没有用的,事件追踪非常重要的一环就是移动(move),然而它却没卵用,这也太不实在了 ( ̄Д ̄)ノ
@@ -241,6 +258,50 @@ void printSamples(MotionEvent ev) {
+## 获取事件发生的时间
+
+获取事件发生的时间。
+
+| 方法 | 简介 |
+| ------------------------------- | ------------ |
+| getDownTime() | 获取手指按下时的时间。 |
+| getEventTime() | 获取当前事件发生的时间。 |
+| getHistoricalEventTime(int pos) | 获取历史事件发生的时间。 |
+
+> 1. pos 表示历史数据中的第几个数据。( pos < getHistorySize() )
+> 2. 返回值类型为 long,单位是毫秒。
+
+## 获取压力(接触面积大小)
+
+MotionEvent支持获取某些输入设备(手指或触控笔)的与屏幕的接触面积和压力大小,主要有以下方法:
+
+> 描述中使用了手指,触控笔也是一样的。
+
+| 方法 | 简介 |
+| ---------------------------------------- | ---------------------------- |
+| getSize () | 获取第1个手指与屏幕接触面积的大小 |
+| getSize (int pin) | 获取第pin个手指与屏幕接触面积的大小 |
+| getHistoricalSize (int pos) | 获取历史数据中第1个手指在第pos次事件中的接触面积 |
+| getHistoricalSize (int pin, int pos) | 获取历史数据中第pin个手指在第pos次事件中的接触面积 |
+| getPressure () | 获取第一个手指的压力大小 |
+| getPressure (int pin) | 获取第pin个手指的压力大小 |
+| getHistoricalPressure (int pos) | 获取历史数据中第1个手指在第pos次事件中的压力大小 |
+| getHistoricalPressure (int pin, int pos) | 获取历史数据中第pin个手指在第pos次事件中的压力大小 |
+
+> 1. pin 全称是 pointerIndex,表示第几个手指。(pin < getPointerCount() )
+> 2. pos 表示历史数据中的第几个数据。( pos < getHistorySize() )
+
+**注意:**
+
+**1、获取接触面积大小和获取压力大小是需要硬件支持的。**
+**2、非常不幸的是大部分设备所使用的电容屏不支持压力检测,但能够大致检测出接触面积。**
+**3、大部分设备的 `getPressure()` 是使用接触面积来模拟的。**
+**4、由于某些未知的原因(可能系统版本和硬件问题),某些设备不支持该方法。**
+
+我用不同的设备对这两个方法进行了测试,然而不同设备测试出来的结果不相同,之后经过我多方查证,发现是系统问题,有的设备上只有 `getSize()` 能用,有的设备上只有 `getPressure()` 能用,而有的则两个都不能用。
+
+**由于获取接触面积和获取压力大小受系统和硬件影响,使用的时候一定要进行数据检测,以防因为设备问题而导致程序出错。**
+
## 鼠标事件
@@ -304,4 +365,4 @@ void printSamples(MotionEvent ev) {
[customview/CoordinateSystem]: http://www.gcssloop.com/customview/CoordinateSystem "安卓自定义View基础-坐标系"
[customview/CustomViewProcess]: http://www.gcssloop.com/customview/CustomViewProcess "安卓自定义View进阶-分类与流程"
[customview/dispatch-touchevent-theory]: http://www.gcssloop.com/customview/dispatch-touchevent-theory "安卓自定义View进阶-事件分发机制原理"
-[customview/dispatch-touchevent-source]: http://www.gcssloop.com/customview/dispatch-touchevent-source "安卓自定义View进阶-事件分发机制详解"
\ No newline at end of file
+[customview/dispatch-touchevent-source]: http://www.gcssloop.com/customview/dispatch-touchevent-source "安卓自定义View进阶-事件分发机制详解"
From dce067e8c1cfc664ae06c627213e8703dd602fb6 Mon Sep 17 00:00:00 2001
From: sloop
Date: Fri, 4 Nov 2016 17:15:37 +0800
Subject: [PATCH 031/160] Update
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 6449598c..650392c4 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,7 @@
* [安卓自定义View进阶 - Matrix Camera](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B11%5DMatrix_3D_Camera.md)
* [安卓自定义View进阶 - 事件分发机制原理](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B12%5DDispatch-TouchEvent-Theory.md)
* [安卓自定义View进阶 - 事件分发机制详解](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B15%5DDispatch-TouchEvent-Source.md)
- * [安卓自定义View进阶 - MotionEvent 详解](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B16%5DMotionEvent.md)
+ * [安卓自定义View进阶 - MotionEvent详解](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B16%5DMotionEvent.md)
******
From bfbab13bc7789d4a36324e1277397ac3454f9118 Mon Sep 17 00:00:00 2001
From: sloop
Date: Sat, 5 Nov 2016 22:18:34 +0800
Subject: [PATCH 032/160] Update
---
CustomView/Advance/[16]MotionEvent.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CustomView/Advance/[16]MotionEvent.md b/CustomView/Advance/[16]MotionEvent.md
index 6daca725..94c9c8f1 100644
--- a/CustomView/Advance/[16]MotionEvent.md
+++ b/CustomView/Advance/[16]MotionEvent.md
@@ -92,7 +92,7 @@ public boolean onTouchEvent(MotionEvent event) {
**事实上,只有上层 View 回收事件处理权的时候,ChildView 才会收到一个 `ACTION_CANCEL` 事件。**
-这样说可能不太容易不理解,咱举个例子?
+这样说可能不太容易理解,咱举个例子?

From 3c5658053b304e9db3cfe5b54b1764590c3fe064 Mon Sep 17 00:00:00 2001
From: sloop
Date: Sun, 6 Nov 2016 23:10:49 +0800
Subject: [PATCH 033/160] Update
---
CustomView/README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/CustomView/README.md b/CustomView/README.md
index 664ace01..cb873f7d 100644
--- a/CustomView/README.md
+++ b/CustomView/README.md
@@ -47,6 +47,7 @@
+
### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop)
From 0739442b436c5d99ff42893f3615c90e30f78e77 Mon Sep 17 00:00:00 2001
From: sloop
Date: Mon, 7 Nov 2016 23:08:14 +0800
Subject: [PATCH 034/160] Update
---
README.md | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 650392c4..41cb0496 100644
--- a/README.md
+++ b/README.md
@@ -47,7 +47,9 @@
* [优雅的发布Android开源库(论JitPack的优越性)](https://github.com/GcsSloop/AndroidNote/blob/master/Course/ReleaseLibraryByJitPack.md)
* [用JitPack发布时附加文档和源码](https://github.com/GcsSloop/AndroidNote/blob/master/Course/jitpack-javadoc.md)
-------
+******
+
+## Markdown
* [Markdown 快速入门](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-start.md)
* [Markdown 基础语法](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-grammar.md)
From 601741899d1982afe7d7a0a855a828c057550f1b Mon Sep 17 00:00:00 2001
From: sloop
Date: Tue, 8 Nov 2016 04:01:45 +0800
Subject: [PATCH 035/160] Update
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 41cb0496..5ac46879 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
> #### PS:点击分类标题可以查看该分类的详细信息。
-## [博客](http://www.gcssloop.com/#blog)
+## [博客](http://www.gcssloop.com/#blog "GcsSloop的博客")
新开的博客,在博客中可以获得更好的阅读体验,同时在博客的评论区可以更及时的向我反馈文章中的问题。
From e50525389b803bf72a5ffa1d61e7bc41f7479510 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Tue, 8 Nov 2016 13:31:40 +0800
Subject: [PATCH 036/160] Update
---
CustomView/Advance/Code/SearchView.java | 44 +++++++++++++++----------
CustomView/Advance/Code/SearchView.md | 44 +++++++++++++++----------
2 files changed, 54 insertions(+), 34 deletions(-)
diff --git a/CustomView/Advance/Code/SearchView.java b/CustomView/Advance/Code/SearchView.java
index 8f3a3a23..c6ee6262 100644
--- a/CustomView/Advance/Code/SearchView.java
+++ b/CustomView/Advance/Code/SearchView.java
@@ -16,6 +16,33 @@ public class SearchView extends View {
private int mViewWidth;
private int mViewHeight;
+ public SearchView(Context context) {
+ this(context,null);
+ }
+
+ public SearchView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initAll();
+ }
+
+ public void initAll() {
+
+ initPaint();
+
+ initPath();
+
+ initListener();
+
+ initHandler();
+
+ initAnimator();
+
+ // 进入开始动画
+ mCurrentState = State.STARTING;
+ mStartingAnimator.start();
+
+ }
+
// 这个视图拥有的状态
public static enum State {
NONE,
@@ -57,24 +84,7 @@ public static enum State {
private int count = 0;
- public SearchView(Context context) {
- super(context);
-
- initPaint();
-
- initPath();
-
- initListener();
- initHandler();
-
- initAnimator();
-
- // 进入开始动画
- mCurrentState = State.STARTING;
- mStartingAnimator.start();
-
- }
private void initPaint() {
mPaint = new Paint();
diff --git a/CustomView/Advance/Code/SearchView.md b/CustomView/Advance/Code/SearchView.md
index 19d404a3..311df8e3 100644
--- a/CustomView/Advance/Code/SearchView.md
+++ b/CustomView/Advance/Code/SearchView.md
@@ -21,6 +21,33 @@ public class SearchView extends View {
private int mViewWidth;
private int mViewHeight;
+ public SearchView(Context context) {
+ this(context,null);
+ }
+
+ public SearchView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initAll();
+ }
+
+ public void initAll() {
+
+ initPaint();
+
+ initPath();
+
+ initListener();
+
+ initHandler();
+
+ initAnimator();
+
+ // 进入开始动画
+ mCurrentState = State.STARTING;
+ mStartingAnimator.start();
+
+ }
+
// 这个视图拥有的状态
public static enum State {
NONE,
@@ -62,24 +89,7 @@ public class SearchView extends View {
private int count = 0;
- public SearchView(Context context) {
- super(context);
-
- initPaint();
-
- initPath();
-
- initListener();
- initHandler();
-
- initAnimator();
-
- // 进入开始动画
- mCurrentState = State.STARTING;
- mStartingAnimator.start();
-
- }
private void initPaint() {
mPaint = new Paint();
From 77ed21e10a284c16bb8198e4f93b5a0585072392 Mon Sep 17 00:00:00 2001
From: sloop
Date: Wed, 9 Nov 2016 22:45:55 +0800
Subject: [PATCH 037/160] Update
---
Course/Markdown/markdown-link.md | 189 ++++++++++++++++++++++++++++++-
1 file changed, 188 insertions(+), 1 deletion(-)
diff --git a/Course/Markdown/markdown-link.md b/Course/Markdown/markdown-link.md
index 7d82df34..265575ec 100644
--- a/Course/Markdown/markdown-link.md
+++ b/Course/Markdown/markdown-link.md
@@ -1 +1,188 @@
-# Markdown
+# Markdown实用技巧-链接和图片
+
+Sloop 喝过半杯咖啡,涨红的脸色渐渐复了原,旁人便又问道,“ Sloop,你当真会写文章么?” Sloop 看着问他的人,显出不屑置辩的神气。他们便接着说道,“你怎的连半个赞也捞不到呢?” Sloop 立刻显出颓唐不安模样,脸上笼上了一层灰色,嘴里说些话;这回可是全是技术名词之类,一些不懂了。在这时候,众人也都哄笑起来:办公室内外充满了快活的空气。
+
+在这些时候,我可以附和着笑,老板是决不责备的。而且老板见了 Sloop,也每每这样问他,引人发笑。Sloop 自己知道不能和他们谈天,便只好向实习生说话。有一回对我说道,“你会用 Markdown 么?” 我略略点一点头。他说,“会用 Markdown,……我便考你一考。Markdown 的链接,你是怎样写的?” 我想,码农一样的人,也配考我么?便回过脸去,不再理会。Sloop 等了许久,很恳切的说道,“不能写罢?……我教给你,记着!这些写法应该记着。将来做程序员的时候,写文档要用。”我暗想我和程序员的等级还很远呢,而且我们这里的程序员也从不写文档;又好笑,又不耐烦,懒懒的答他道,“谁要你教,不是一对方括号后面跟一对圆括号么?” Sloop 显出极高兴的样子,将两个指头的长指甲敲着电脑,点头说,“对呀对呀!……链接有4样基本写法,你知道么?” 我愈不耐烦了,努着嘴走远。Sloop 刚想打开编辑器,给我演示,见我毫不热心,便又叹一口气,显出极惋惜的样子。
+
+*****
+
+## 前言
+
+**本文是适用于对 Markdown 有一定了解的魔法师,以帮助他们挖掘更多关于 Markdown 的可能性,例如:链接的不同类型以及使用方式,如何在新标签页打开链接,如何控制图片大小等,对 Markdown 还不了解的魔法师请参考 [Markdown快速入门][markdown-start] 和 [Markdown基础语法][markdown-grammar] 。**
+
+**注意:以下的部分语法不属于标准语法,存在不兼容的问题,不能保证所有平台都能够使用。对于非标准语法(拓展语法)我会进行标注说明。**
+
+*****
+
+## 行内式链接:
+
+```markdown
+博客地址: [GcsSloop](http://www.gcssloop.com)
+博客地址: [GcsSloop](http://www.gcssloop.com "GcsSloop的博客")
+```
+
+博客地址: [GcsSloop](http://www.gcssloop.com)
+博客地址: [GcsSloop](http://www.gcssloop.com "GcsSloop的博客")
+
+*****
+
+## 参考式链接:
+
+```markdown
+[GcsSloop的博客][gcssloop]
+
+[gcssloop]: http://www.gcssloop.com
+// 或者
+[gcssloop]: http://www.gcssloop.com "点击访问GcsSloop的博客"
+```
+
+[GcsSloop的博客][gcssloop]
+
+**为什么要使用参考式呢?**
+在写文章的时候很可能会在文章不同的地方引用同一篇文章,使用参考式可以少写一点字符。
+更重要的是,参考文章的链接可能会改变,如果将参考链接统一写在文末的话,改起来会更容易。
+
+*****
+
+## 自动链接:
+
+```markdown
+
+```
+
+
+
+*****
+
+## 相对链接:
+
+**如果你的内容是发布在 GitHub 或者自己的个人网站上,那么相对链接是一个很好用的东西**,例如引用本站的一张图片可以这样写:
+
+```markdown
+
+```
+
+
+
+**相对链接优点:**
+
+* 线下预览和线上效果相同,不受网络影响。
+* 避免了分别上传图片导致的麻烦。
+* GitHub复制仓库更方便,改仓库名字不会导致资源链接失效。
+
+**相对链接缺点:**
+
+* 不适用于需要在多平台发布的文章。
+* 某些本地编辑器不支持读取相对链接。
+
+*****
+
+## 图片链接:
+
+图片链接其实就是把图片和链接嵌套在一起:
+
+> 顺便为 DiyCode 打一个小广告,欢迎更多小伙伴的加入。
+
+```markdown
+[](http://www.diycode.cc/wiki/encouragement)
+```
+
+[](http://www.diycode.cc/wiki/encouragement){:target="_blank"}
+
+*****
+
+**注意:从这里开始往下的部分,属于拓展语法,可能存在某些平台(编辑器)无法识别的问题,请亲自测试后再使用。**
+
+## 在新标签页打开:
+
+Markdown 的默认链接方式都是在当前标签页打开的,这就会导致打开链接之后原始页面被覆盖掉,如果想要让链接在新标签页打开可以在后面添加上 `{:target="_blank"}` 或者使用 HTML 语法。
+
+```
+[GcsSloop](http://www.gcssloop.com){:target="_blank"}
+
+[GcsSloop的博客][gcssloop]{:target="_blank"}
+
+{:target="_blank"}
+
+关于GcsSloop
+```
+
+[GcsSloop](http://www.gcssloop.com){:target="_blank"}
+[GcsSloop的博客][gcssloop]{:target="_blank"}
+{:target="_blank"}
+关于GcsSloop
+
+注意: 部分平台可能不识别 `{:target="_blank"}` 标签,例如你正在看的这个 GitHub 就不识别。。
+
+*****
+
+## 注释(脚注):
+
+注释一般用于解释一些专业名词或者难以理解的内容,由于注释的解释部分一般放在文末,所以又称为脚注:
+
+```
+GcsSloop[^1]是一个超级魔法师[^2] 。
+
+[^1]: GcsSloop:非著名程序员。
+[^2]: 魔法师:会魔法的人类
+```
+
+GcsSloop[^1]是一个超级魔法师[^2] 。
+
+注意:脚注不论在何处定义,最终都是显示在文末。部分平台不识别该语法。
+
+*****
+
+## 控制图片大小
+
+使用 `{:width="300" height="100"}` 或者 HTML 格式可以控制图片显示大小,图片有宽度(width)和高度(height)两个属性,如果只指定了一个,另一个会按照比例缩放。
+
+```
+{:width="300"}
+
+
+```
+
+{:width="300"}
+
+右键,新标签页打开图片,你会发现原图其实挺大的。
+注意: 部分平台可能不识别 `{:width="300" height="100"}` 标签,你正在看的这个 GitHub 依旧不识别。
+
+*****
+
+## 总结:
+
+Markdown 存在很多的变种,对其语法进行了不同程度的拓展,使其更加的强大,但是使用拓展语法之前请三思。个人建议如下:
+
+* 如果文章(文档)只在单一平台发布,使用任何该平台支持的拓展语法都没问题。
+* 如果文章(文档)需要在多个平台发布,尽量使用标准语法,使用拓展语法之前请注意测试平台兼容性。
+* 图片尽量使用图床管理,而且要进行本地备份。
+
+
+*****
+
+## About Me
+
+### 作者微博: @GcsSloop
+
+
+
+## 参考链接:
+
+[Markdown-基础语法](http://www.markdown.cn/)
+[Markdown语法:在新窗口新标签页中打开](http://yinping4256.github.io/cn/Markdown%E8%AF%AD%E6%B3%95%E5%9C%A8%E6%96%B0%E7%AA%97%E5%8F%A3%E6%96%B0%E6%A0%87%E7%AD%BE%E9%A1%B5%E4%B8%AD%E6%89%93%E5%BC%80/)
+
+
+### 注释:
+
+[^1]: GcsSloop:非著名程序员。
+[^2]: 魔法师:会魔法的人类
+
+
+
+
+[markdown-start]: http://www.gcssloop.com/markdown/markdown-star "Markdown实用技巧-快速入门"
+
+[markdown-grammar]: http://www.gcssloop.com/markdown/markdown-grammar "Markdown实用技巧-基础语法"
+
+[gcssloop]: http://www.gcssloop.com "点击访问GcsSloop的博客"
From 5a2c379d45ccbe8dadbdc03ffbfe83fa276aee3d Mon Sep 17 00:00:00 2001
From: sloop
Date: Wed, 9 Nov 2016 22:48:19 +0800
Subject: [PATCH 038/160] Update
---
Course/Markdown/markdown-link.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/Course/Markdown/markdown-link.md b/Course/Markdown/markdown-link.md
index 265575ec..05e2a57e 100644
--- a/Course/Markdown/markdown-link.md
+++ b/Course/Markdown/markdown-link.md
@@ -91,8 +91,12 @@ Sloop 喝过半杯咖啡,涨红的脸色渐渐复了原,旁人便又问道
*****
+IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII
+
**注意:从这里开始往下的部分,属于拓展语法,可能存在某些平台(编辑器)无法识别的问题,请亲自测试后再使用。**
+IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII
+
## 在新标签页打开:
Markdown 的默认链接方式都是在当前标签页打开的,这就会导致打开链接之后原始页面被覆盖掉,如果想要让链接在新标签页打开可以在后面添加上 `{:target="_blank"}` 或者使用 HTML 语法。
From db0d1ded31844ff636d3fbde72bc2b91a4d08fd7 Mon Sep 17 00:00:00 2001
From: sloop
Date: Wed, 9 Nov 2016 22:50:45 +0800
Subject: [PATCH 039/160] Update
---
Course/Markdown/markdown-link.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Course/Markdown/markdown-link.md b/Course/Markdown/markdown-link.md
index 05e2a57e..fdf47d57 100644
--- a/Course/Markdown/markdown-link.md
+++ b/Course/Markdown/markdown-link.md
@@ -133,7 +133,7 @@ GcsSloop[^1]是一个超级魔法师[^2] 。
GcsSloop[^1]是一个超级魔法师[^2] 。
-注意:脚注不论在何处定义,最终都是显示在文末。部分平台不识别该语法。
+注意:脚注不论在何处定义,最终都是显示在文末。部分平台不识别该语法,GitHub 依旧不识别。
*****
From 757db08821caccb118ad1f1fdd87819479618e43 Mon Sep 17 00:00:00 2001
From: sloop
Date: Thu, 10 Nov 2016 21:42:23 +0800
Subject: [PATCH 040/160] Update
---
Course/Markdown/markdown-link.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/Course/Markdown/markdown-link.md b/Course/Markdown/markdown-link.md
index fdf47d57..5c3d40cc 100644
--- a/Course/Markdown/markdown-link.md
+++ b/Course/Markdown/markdown-link.md
@@ -1,5 +1,7 @@
# Markdown实用技巧-链接和图片
+博客地址: http://www.gcssloop.com/markdown/markdown-links
+
Sloop 喝过半杯咖啡,涨红的脸色渐渐复了原,旁人便又问道,“ Sloop,你当真会写文章么?” Sloop 看着问他的人,显出不屑置辩的神气。他们便接着说道,“你怎的连半个赞也捞不到呢?” Sloop 立刻显出颓唐不安模样,脸上笼上了一层灰色,嘴里说些话;这回可是全是技术名词之类,一些不懂了。在这时候,众人也都哄笑起来:办公室内外充满了快活的空气。
在这些时候,我可以附和着笑,老板是决不责备的。而且老板见了 Sloop,也每每这样问他,引人发笑。Sloop 自己知道不能和他们谈天,便只好向实习生说话。有一回对我说道,“你会用 Markdown 么?” 我略略点一点头。他说,“会用 Markdown,……我便考你一考。Markdown 的链接,你是怎样写的?” 我想,码农一样的人,也配考我么?便回过脸去,不再理会。Sloop 等了许久,很恳切的说道,“不能写罢?……我教给你,记着!这些写法应该记着。将来做程序员的时候,写文档要用。”我暗想我和程序员的等级还很远呢,而且我们这里的程序员也从不写文档;又好笑,又不耐烦,懒懒的答他道,“谁要你教,不是一对方括号后面跟一对圆括号么?” Sloop 显出极高兴的样子,将两个指头的长指甲敲着电脑,点头说,“对呀对呀!……链接有4样基本写法,你知道么?” 我愈不耐烦了,努着嘴走远。Sloop 刚想打开编辑器,给我演示,见我毫不热心,便又叹一口气,显出极惋惜的样子。
From c2fcb422d6121461c8152d0f58678434cf58fe33 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Mon, 14 Nov 2016 00:19:43 +0800
Subject: [PATCH 041/160] Update
---
README.md | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 5ac46879..1ebb8588 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@
## [自定义View](https://github.com/GcsSloop/AndroidNote/tree/master/CustomView/README.md)
-
+
* 基础篇
* [安卓自定义View基础 - 坐标系](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Base/%5B01%5DCoordinateSystem.md)
* [安卓自定义View基础 - 角度弧度](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Base/%5B02%5DAngleAndRadian.md)
@@ -53,6 +53,7 @@
* [Markdown 快速入门](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-start.md)
* [Markdown 基础语法](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-grammar.md)
+* [Markdown 基础语法](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-grammar.md)
******
@@ -62,7 +63,7 @@
* [程序员不可不知的版权协议](https://github.com/GcsSloop/AndroidNote/blob/magic-world/ChaosCrystal/%E5%BC%80%E6%BA%90%E5%85%B1%E4%BA%AB%E5%8D%8F%E8%AE%AE.md)
******
-
+
## [速查表](https://github.com/GcsSloop/AndroidNote/tree/master/QuickChart/README.md)
* [Canvas常用操作速查表](https://github.com/GcsSloop/AndroidNote/blob/master/QuickChart/Canvas.md)
From 9fe13234466a2a0b2c21ce7736ed3c994b8a3cad Mon Sep 17 00:00:00 2001
From: sloop
Date: Tue, 15 Nov 2016 00:25:45 +0800
Subject: [PATCH 042/160] Update
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 1ebb8588..85ac6cc0 100644
--- a/README.md
+++ b/README.md
@@ -53,7 +53,7 @@
* [Markdown 快速入门](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-start.md)
* [Markdown 基础语法](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-grammar.md)
-* [Markdown 基础语法](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-grammar.md)
+* [Markdown 链接图片](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-link.md)
******
From 0130ee225e8befe8efab1669e04384acd79f79fc Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Wed, 16 Nov 2016 23:40:56 +0800
Subject: [PATCH 043/160] Update
---
CustomView/Advance/[17]touch-matrix-region.md | 576 ++++++++++++++++++
1 file changed, 576 insertions(+)
create mode 100644 CustomView/Advance/[17]touch-matrix-region.md
diff --git a/CustomView/Advance/[17]touch-matrix-region.md b/CustomView/Advance/[17]touch-matrix-region.md
new file mode 100644
index 00000000..0803a838
--- /dev/null
+++ b/CustomView/Advance/[17]touch-matrix-region.md
@@ -0,0 +1,576 @@
+# 特殊控件的事件处理方案
+
+本文带大家了解 Android 特殊形状控件的事件处理方式,主要是利用了 Region 和 Matrix 的一些方法,超级实用的事件处理方案,相信看完本篇之后,任何奇葩控件的事件处理都会变得十分简单。
+
+不得不说,Android 对事件体系封装的非常棒,即便对事件体系不太了解的人,只要简单的调用方法就能使用,而且具有防呆设计,能够保证事件流的完整性和统一性,最大可能性的避免了事件处理的混乱,着实令人佩服。
+
+然而世界上并没有绝对完美的东西,**当【事件处理】遇上【自定义View】,一场好戏就开演了,玩的好叫坐镇军前,指挥千军万马而分毫不乱,玩的不好就是抓耳挠腮,眼见敌人前后包抄而无可奈何。**
+
+## 特殊形状控件
+
+在通常的情况下,自定义 View 直接使用系统的事件体系处理就行,我们也不需要特殊处理,然而当一些特殊的控件出现的时候,麻烦就来了,举个栗子:
+
+
+
+这是一个在遥控器上非常常见的按键布局,注意中间上下左右选择的部分,看起来十分简单,然而当你真正准备在手机上实现的时候麻烦就出现了。因为所有的View默认都是矩形的,所以事件接收区域也是矩形的,如果直接使用系统提供的 View 来组合出一摸一样的布局也很简单,但点击区域该如何处理?显然这样有部分点击区域是在控件外面的,并且控件之间会产生重叠区域,点击的时候容易判断错误,例如:
+
+> 红色方框表示用 View 组合的情况下,单个 View 的可点击区域。
+
+
+
+当我们面对这样比较奇特的控件的时候,有很多处理办法,比较投机的一种就是背景贴一个静态图,按钮做成透明的,设置小一点,放在对应的位置,这样可以保证不会误触,当然了如果想要点击效果可以在按钮按下的时候更新一下背景图,这样虽然也可以,但可点击区域会变小,体验效果会变差。设计方案变得非常复杂,而且逻辑也不容易处理,可以说是一种非常糟糕的设计。
+
+当然了,看了我这么多文章的小伙伴应该也猜到我接下来要说什么了,没错,就是自定义 View。当我们面对一些奇葩控件的时候,自定义 View 就变成了一种非常好用的处理方案。
+
+相信小伙伴们看过 [前面的文章][CustomViewIndex] 之后,对各种图形的绘制已经不成问题了,所以我们直接处理重点问题。
+
+
+
+## 特殊形状控的点击区域判断
+
+要进行特殊形状的点击判断,要用到一个之前没有使用过的类:Region。
+
+Region 直接翻译的意思是 地域,区域。**在此处应该是区域的意思**。它和 Path 有些类似,但 Path 可以是不封闭图形,而 Region 总是封闭的。可以通过 `setPath` 方法将 Path 转换为 Region。
+
+ **本文中我们重点要使用到的是 Region 中的 `contains` 方法,这个方法可以判断一个点是否包含在该区域内。**
+
+下面是一个简单的示例程序:
+
+```java
+public class RegionClickView extends View {
+ Paint mPaint;
+ Region globalRegion;
+ Region circleRegion1;
+ Region circleRegion2;
+ Path circlePath1;
+ Path circlePath2;
+
+ public RegionClickView(Context context) {
+ super(context);
+
+ mPaint = new Paint();
+ mPaint.setStyle(Paint.Style.FILL);
+ mPaint.setColor(Color.GRAY);
+
+ // 用Path创建两个圆
+ circlePath1 = new Path();
+ circlePath2 = new Path();
+ circlePath1.addCircle(100, 100, 50, Path.Direction.CW);
+ circlePath2.addCircle(400, 400, 50, Path.Direction.CW);
+
+ // 创建 Region
+ circleRegion1 = new Region();
+ circleRegion2 = new Region();
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ // 将剪裁边界设置为视图大小
+ globalRegion = new Region(0, 0, w, h);
+
+ // ▼将 Path 添加到 Region 中
+ circleRegion1.setPath(circlePath1, globalRegion);
+ circleRegion2.setPath(circlePath2, globalRegion);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ switch (event.getAction()){
+ case MotionEvent.ACTION_DOWN:
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+
+ // ▼点击区域判断
+ if (circleRegion1.contains(x,y)){
+ Toast.makeText(this.getContext(),"圆1被点击",Toast.LENGTH_SHORT).show();
+ }
+ if (circleRegion2.contains(x,y)){
+ Toast.makeText(this.getContext(),"圆2被点击",Toast.LENGTH_SHORT).show();
+ }
+ break;
+ }
+ return super.onTouchEvent(event);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ // ▼注意此处将全局变量转化为局部变量,方便 GC 回收 Canvas
+ Path circle1 = circlePath1;
+ Path circle2 = circlePath2;
+
+ // 绘制两个圆
+ canvas.drawPath(circle1,mPaint);
+ canvas.drawPath(circle2,mPaint);
+ }
+}
+```
+
+> 代码中比较重要的内容都用 ▼ 符号标记出来了。
+
+上述代码非常简单,就是创建了两个 Path,在 Path 中添加圆形,之后将 Path 设置到 Region 中,当手指在屏幕上按下的时候判断手指按下位置是否在 Region 区域内。
+
+代码整体逻辑非常简单,大家测试一下就行了。
+
+
+
+## 画布变换后坐标转换问题
+
+还是上述的例子,绘制一个上下左右选择按键,这个控件是上下左右对称的,熟悉我代码风格的小伙伴都知道,如果遇上这种问题,我肯定是要将坐标系平移到这个控件中心的,这样数据比较好计算,然而进行画布变换操作会产生一个新问题:**手指触摸的坐标系和画布坐标系不统一,就可能引起手指触摸位置和绘制位置不统一。**
+
+举个栗子:
+
+> 在手指按下位置绘制一个圆。
+
+```java
+public class CanvasVonvertTouchTest extends CustomView{
+ float down_x = -1;
+ float down_y = -1;
+
+ public CanvasVonvertTouchTest(Context context) {
+ this(context, null);
+ }
+
+ public CanvasVonvertTouchTest(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ switch (event.getActionMasked()){
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_MOVE:
+ down_x = event.getX();
+ down_y = event.getY();
+ invalidate();
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ down_x = down_y = -1;
+ invalidate();
+ break;
+ }
+
+ return true;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ float x = down_x;
+ float y = down_y;
+
+ // 绘制触摸坐标系,颜色为灰色,为了能够显示出坐标系,将坐标系位置稍微偏移了一点
+ drawTouchCoordinateSpace(canvas);
+
+ canvas.translate(mViewWidth/2, mViewHeight/2); // 画布平移
+
+ // 绘制平移后的坐标系,颜色为红色
+ drawTranslateCoordinateSpace(canvas);
+
+ // 如果没有就返回
+ if (x == -1 && y == -1)
+ return;
+
+ // 在触摸位置绘制一个小圆
+ canvas.drawCircle(x,y,20,mDeafultPaint);
+ }
+
+ /**
+ * 绘制触摸坐标系,颜色为灰色,为了能够显示出坐标系,将坐标系位置稍微偏移了一点
+ */
+ private void drawTouchCoordinateSpace(Canvas canvas) {
+ canvas.save();
+ canvas.translate(10,10);
+ CanvasAidUtils.set2DAxisLength(1000, 0, 1400, 0);
+ CanvasAidUtils.setLineColor(Color.GRAY);
+ CanvasAidUtils.draw2DCoordinateSpace(canvas);
+ canvas.restore();
+ }
+
+ /**
+ * 绘制平移后的坐标系,颜色为红色
+ */
+ private void drawTranslateCoordinateSpace(Canvas canvas) {
+ CanvasAidUtils.set2DAxisLength(500, 500, 700, 700);
+ CanvasAidUtils.setLineColor(Color.RED);
+ CanvasAidUtils.draw2DCoordinateSpace(canvas);
+ CanvasAidUtils.draw2DCoordinateSpace(canvas);
+ }
+}
+```
+
+可以看到,直接拿手指触摸位置的坐标来绘制会导致绘制位置不正确,大概会像这样子:
+
+
+
+两者坐标是相同的,但是由于坐标系不同,导致实际显示位置不同。
+
+**那么问题来了,我们在之前的文章中讲过,映射不同坐标系的坐标用 什么来着?**
+**是 Matrix。**
+
+如果看过我之前的文章但没有想起来的说明你们根本没有认真看,全部拖出去糟蹋 5 分钟!
+没看过的点 [Matrix原理][Matrix_Basic] 和 [Matrix详解][Matrix_Method] 。
+
+> **Matrix 是一个矩阵,主要功能是坐标映射,数值转换。**
+
+那么接下来我们就对上面的示例进行简单的改造一下,让触摸位置和实际绘制绘制重合。
+
+**注意:比较重要的修改位置用▼标记出来了。**
+
+```java
+public class CanvasVonvertTouchTest extends CustomView{
+ float down_x = -1;
+ float down_y = -1;
+
+ public CanvasVonvertTouchTest(Context context) {
+ this(context, null);
+ }
+
+ public CanvasVonvertTouchTest(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ switch (event.getActionMasked()){
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_MOVE:
+ // ▼ 注意此处使用 getRawX,而不是 getX
+ down_x = event.getRawX();
+ down_y = event.getRawY();
+ invalidate();
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ down_x = down_y = -1;
+ invalidate();
+ break;
+ }
+
+ return true;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ float[] pts = {down_x, down_y};
+
+ // 绘制触摸坐标系,颜色为灰色,为了能够显示出坐标系,将坐标系位置稍微偏移了一点
+ drawTouchCoordinateSpace(canvas);
+
+ canvas.translate(mViewWidth/2, mViewHeight/2); // 画布平移
+
+ // 绘制平移后的坐标系,颜色为红色
+ drawTranslateCoordinateSpace(canvas);
+
+ // 如果没有就返回
+ if (pts[0] == -1 && pts[1] == -1)
+ return;
+
+ // ▼ 获得当前矩阵的逆矩阵
+ Matrix invertMatrix = new Matrix();
+ canvas.getMatrix().invert(invertMatrix);
+
+ // ▼ 使用 mapPoints 将触摸位置转换为画布坐标
+ invertMatrix.mapPoints(pts);
+
+ // 在触摸位置绘制一个小圆
+ canvas.drawCircle(pts[0],pts[1],20,mDeafultPaint);
+ }
+
+ /**
+ * 绘制触摸坐标系,颜色为灰色,为了能够显示出坐标系,将坐标系位置稍微偏移了一点
+ */
+ private void drawTouchCoordinateSpace(Canvas canvas) {
+ canvas.save();
+ canvas.translate(10,10);
+ CanvasAidUtils.set2DAxisLength(1000, 0, 1400, 0);
+ CanvasAidUtils.setLineColor(Color.GRAY);
+ CanvasAidUtils.draw2DCoordinateSpace(canvas);
+ canvas.restore();
+ }
+
+ /**
+ * 绘制平移后的坐标系,颜色为红色
+ */
+ private void drawTranslateCoordinateSpace(Canvas canvas) {
+ CanvasAidUtils.set2DAxisLength(500, 500, 700, 700);
+ CanvasAidUtils.setLineColor(Color.RED);
+ CanvasAidUtils.draw2DCoordinateSpace(canvas);
+ CanvasAidUtils.draw2DCoordinateSpace(canvas);
+ }
+}
+
+```
+
+**绘制结果:**
+
+小白点和黑色的圆没有完全重合是因为系统显示触摸位置的绘制逻辑和我使用的绘制逻辑不太相同,两者的位置是一样的。
+
+
+
+其实核心部分就这两点:
+
+```java
+// ▼ 注意此处使用 getRawX,而不是 getX
+down_x = event.getRawX();
+down_y = event.getRawY();
+
+// -------------------------------------
+
+// ▼ 获得当前矩阵的逆矩阵
+Matrix invertMatrix = new Matrix();
+canvas.getMatrix().invert(invertMatrix);
+
+// ▼ 使用 mapPoints 将触摸位置转换为画布坐标
+invertMatrix.mapPoints(pts);
+```
+
+1. 使用全局坐标系
+2. 使用逆矩阵的 mapPoints
+
+**原理嘛,其实非常简单,我们在画布上正常的绘制,需要将画布坐标系转换为全局坐标系后才能真正的绘制内容。所以我们反着来,将获得到的全局坐标系坐标使用当前画布的逆矩阵转化一下,就转化为当前画布的坐标系坐标了,如果对 [Matrix原理][Matrix_Basic] 和 [Matrix详解][Matrix_Method] 理解了,即便我不说你们也肯定会想到这个方案的。**
+
+## 仿遥控器按钮代码示例
+
+在解决了上述两大难题之后,相信不论形状如何奇葩的自定义控件,基本上都难不倒大家了,最后用一个简单的示例作为结尾,还是文章开头所举的例子,核心内容就是上面讲的两个东西。
+
+```java
+public class RemoteControlMenu extends CustomView {
+ Path up_p, down_p, left_p, right_p, center_p;
+ Region up, down, left, right, center;
+
+ Matrix mMapMatrix = null;
+
+ int CENTER = 0;
+ int UP = 1;
+ int RIGHT = 2;
+ int DOWN = 3;
+ int LEFT = 4;
+ int touchFlag = -1;
+ int currentFlag = -1;
+
+ MenuListener mListener = null;
+
+ int mDefauColor = 0xFF4E5268;
+ int mTouchedColor = 0xFFDF9C81;
+
+
+ public RemoteControlMenu(Context context) {
+ this(context, null);
+ }
+
+ public RemoteControlMenu(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ up_p = new Path();
+ down_p = new Path();
+ left_p = new Path();
+ right_p = new Path();
+ center_p = new Path();
+
+ up = new Region();
+ down = new Region();
+ left = new Region();
+ right = new Region();
+ center = new Region();
+
+ mDeafultPaint.setColor(mDefauColor);
+ mDeafultPaint.setAntiAlias(true);
+
+ mMapMatrix = new Matrix();
+
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ mMapMatrix.reset();
+
+ // 注意这个区域的大小
+ Region globalRegion = new Region(-w, -h, w, h);
+ int minWidth = w > h ? h : w;
+ minWidth *= 0.8;
+
+ int br = minWidth / 2;
+ RectF bigCircle = new RectF(-br, -br, br, br);
+
+ int sr = minWidth / 4;
+ RectF smallCircle = new RectF(-sr, -sr, sr, sr);
+
+ float bigSweepAngle = 84;
+ float smallSweepAngle = -80;
+
+ // 根据视图大小,初始化 Path 和 Region
+ center_p.addCircle(0, 0, 0.2f * minWidth, Path.Direction.CW);
+ center.setPath(center_p, globalRegion);
+
+ right_p.addArc(bigCircle, -40, bigSweepAngle);
+ right_p.arcTo(smallCircle, 40, smallSweepAngle);
+ right_p.close();
+ right.setPath(right_p, globalRegion);
+
+ down_p.addArc(bigCircle, 50, bigSweepAngle);
+ down_p.arcTo(smallCircle, 130, smallSweepAngle);
+ down_p.close();
+ down.setPath(down_p, globalRegion);
+
+ left_p.addArc(bigCircle, 140, bigSweepAngle);
+ left_p.arcTo(smallCircle, 220, smallSweepAngle);
+ left_p.close();
+ left.setPath(left_p, globalRegion);
+
+ up_p.addArc(bigCircle, 230, bigSweepAngle);
+ up_p.arcTo(smallCircle, 310, smallSweepAngle);
+ up_p.close();
+ up.setPath(up_p, globalRegion);
+
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ float[] pts = new float[2];
+ pts[0] = event.getRawX();
+ pts[1] = event.getRawY();
+ mMapMatrix.mapPoints(pts);
+
+ int x = (int) pts[0];
+ int y = (int) pts[1];
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ touchFlag = getTouchedPath(x, y);
+ currentFlag = touchFlag;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ currentFlag = getTouchedPath(x, y);
+ break;
+ case MotionEvent.ACTION_UP:
+ currentFlag = getTouchedPath(x, y);
+ // 如果手指按下区域和抬起区域相同且不为空,则判断点击事件
+ if (currentFlag == touchFlag && currentFlag != -1 && mListener != null) {
+ if (currentFlag == CENTER) {
+ mListener.onCenterCliched();
+ } else if (currentFlag == UP) {
+ mListener.onUpCliched();
+ } else if (currentFlag == RIGHT) {
+ mListener.onRightCliched();
+ } else if (currentFlag == DOWN) {
+ mListener.onDownCliched();
+ } else if (currentFlag == LEFT) {
+ mListener.onLeftCliched();
+ }
+ }
+ touchFlag = currentFlag = -1;
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ touchFlag = currentFlag = -1;
+ break;
+ }
+
+ invalidate();
+ return true;
+ }
+
+ // 获取当前触摸点在哪个区域
+ int getTouchedPath(int x, int y) {
+ if (center.contains(x, y)) {
+ return 0;
+ } else if (up.contains(x, y)) {
+ return 1;
+ } else if (right.contains(x, y)) {
+ return 2;
+ } else if (down.contains(x, y)) {
+ return 3;
+ } else if (left.contains(x, y)) {
+ return 4;
+ }
+ return -1;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ canvas.translate(mViewWidth / 2, mViewHeight / 2);
+
+ // 获取测量矩阵(逆矩阵)
+ if (mMapMatrix.isIdentity()) {
+ canvas.getMatrix().invert(mMapMatrix);
+ }
+
+ // 绘制默认颜色
+ canvas.drawPath(center_p, mDeafultPaint);
+ canvas.drawPath(up_p, mDeafultPaint);
+ canvas.drawPath(right_p, mDeafultPaint);
+ canvas.drawPath(down_p, mDeafultPaint);
+ canvas.drawPath(left_p, mDeafultPaint);
+
+ // 绘制触摸区域颜色
+ mDeafultPaint.setColor(mTouchedColor);
+ if (currentFlag == CENTER) {
+ canvas.drawPath(center_p, mDeafultPaint);
+ } else if (currentFlag == UP) {
+ canvas.drawPath(up_p, mDeafultPaint);
+ } else if (currentFlag == RIGHT) {
+ canvas.drawPath(right_p, mDeafultPaint);
+ } else if (currentFlag == DOWN) {
+ canvas.drawPath(down_p, mDeafultPaint);
+ } else if (currentFlag == LEFT) {
+ canvas.drawPath(left_p, mDeafultPaint);
+ }
+ mDeafultPaint.setColor(mDefauColor);
+ }
+
+ public void setListener(MenuListener listener) {
+ mListener = listener;
+ }
+
+ // 点击事件监听器
+ public interface MenuListener {
+ void onCenterCliched();
+
+ void onUpCliched();
+
+ void onRightCliched();
+
+ void onDownCliched();
+
+ void onLeftCliched();
+ }
+}
+```
+
+**运行效果:**
+
+当手指在某一区域活动时,该区域会高亮显示,如果注册了监听器,点击某一区域会触发监听器回调。
+
+
+
+
+
+## 总结
+
+本文虽然代码比较多,但核心概念非常简单,主要涉及以下两点:
+
+1. Region 的区域检测。
+2. Matrix 的坐标映射。
+
+**这两个知识点都不是很难,然而灵活运用起来却是非常强大的,如果有对 Matrix 不了解的小伙伴,推荐去看我 [之前的文章][CustomViewIndex],里面有关于Matrix的详细介绍,**
+
+
+
+## About Me
+
+### 作者微博: @GcsSloop
+
+
+
+
+
+[CustomViewIndex]: http://www.gcssloop.com/customview/CustomViewIndex
+[Matrix_Basic]: http://www.gcssloop.com/customview/Matrix_Basic
+[Matrix_Method]: http://www.gcssloop.com/customview/Matrix_Method
+
+
+
From 0041d3eb5433f6cfdf1012c94c5e76958af8e55b Mon Sep 17 00:00:00 2001
From: sloop
Date: Thu, 17 Nov 2016 17:22:05 +0800
Subject: [PATCH 044/160] Update
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index 85ac6cc0..d372db4c 100644
--- a/README.md
+++ b/README.md
@@ -36,6 +36,7 @@
* [安卓自定义View进阶 - 事件分发机制原理](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B12%5DDispatch-TouchEvent-Theory.md)
* [安卓自定义View进阶 - 事件分发机制详解](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B15%5DDispatch-TouchEvent-Source.md)
* [安卓自定义View进阶 - MotionEvent详解](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B16%5DMotionEvent.md)
+ * [安卓自定义View进阶 - 特殊形状控件事件处理方案](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B17%5Dtouch-matrix-region.md)
******
From 4b537be9b6ef957d9f81c2c3c03a15a12df716f9 Mon Sep 17 00:00:00 2001
From: sloop
Date: Fri, 18 Nov 2016 23:31:40 +0800
Subject: [PATCH 045/160] Update
---
CustomView/README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/CustomView/README.md b/CustomView/README.md
index cb873f7d..6fefef4f 100644
--- a/CustomView/README.md
+++ b/CustomView/README.md
@@ -48,6 +48,7 @@
+
### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop)
From 4ec645d9b2e10d092a9f7f45165cdb7c7c2edd78 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Sat, 19 Nov 2016 23:36:22 +0800
Subject: [PATCH 046/160] Update
---
CustomView/Advance/[17]touch-matrix-region.md | 153 ++++++++----------
1 file changed, 63 insertions(+), 90 deletions(-)
diff --git a/CustomView/Advance/[17]touch-matrix-region.md b/CustomView/Advance/[17]touch-matrix-region.md
index 0803a838..487026a2 100644
--- a/CustomView/Advance/[17]touch-matrix-region.md
+++ b/CustomView/Advance/[17]touch-matrix-region.md
@@ -1,26 +1,31 @@
# 特殊控件的事件处理方案
+
本文带大家了解 Android 特殊形状控件的事件处理方式,主要是利用了 Region 和 Matrix 的一些方法,超级实用的事件处理方案,相信看完本篇之后,任何奇葩控件的事件处理都会变得十分简单。
不得不说,Android 对事件体系封装的非常棒,即便对事件体系不太了解的人,只要简单的调用方法就能使用,而且具有防呆设计,能够保证事件流的完整性和统一性,最大可能性的避免了事件处理的混乱,着实令人佩服。
然而世界上并没有绝对完美的东西,**当【事件处理】遇上【自定义View】,一场好戏就开演了,玩的好叫坐镇军前,指挥千军万马而分毫不乱,玩的不好就是抓耳挠腮,眼见敌人前后包抄而无可奈何。**
+> #### 注意:
+>
+> 本文中所有的 自定义View 均继承自 CustomView ,这是一个自定义的超类,目的是简化 自定义View 部分常用操作,你可以在 [ViewSupport](https://github.com/GcsSloop/ViewSupport/wiki/CustomView) 中找到它以及关于它的简介。
+
## 特殊形状控件
在通常的情况下,自定义 View 直接使用系统的事件体系处理就行,我们也不需要特殊处理,然而当一些特殊的控件出现的时候,麻烦就来了,举个栗子:

-这是一个在遥控器上非常常见的按键布局,注意中间上下左右选择的部分,看起来十分简单,然而当你真正准备在手机上实现的时候麻烦就出现了。因为所有的View默认都是矩形的,所以事件接收区域也是矩形的,如果直接使用系统提供的 View 来组合出一摸一样的布局也很简单,但点击区域该如何处理?显然这样有部分点击区域是在控件外面的,并且控件之间会产生重叠区域,点击的时候容易判断错误,例如:
+这是一个在遥控器上非常常见的按键布局,注意中间上下左右选择的部分,看起来十分简单,然而当你真正准备在手机上实现的时候麻烦就出现了。因为所有的 View 默认都是矩形的,所以事件接收区域也是矩形的,如果直接使用系统提供的 View 来组合出一摸一样的布局也很简单,但点击区域该如何处理?显然有部分点击区域是在控件外面的,并且会产生重叠区域:
-> 红色方框表示用 View 组合的情况下,单个 View 的可点击区域。
+> 红色方框表示 View 的可点击区域。

-当我们面对这样比较奇特的控件的时候,有很多处理办法,比较投机的一种就是背景贴一个静态图,按钮做成透明的,设置小一点,放在对应的位置,这样可以保证不会误触,当然了如果想要点击效果可以在按钮按下的时候更新一下背景图,这样虽然也可以,但可点击区域会变小,体验效果会变差。设计方案变得非常复杂,而且逻辑也不容易处理,可以说是一种非常糟糕的设计。
+当我们面对这样比较奇特的控件的时候,有很多处理办法,比较投机的一种就是背景贴一个静态图,按钮做成透明的,设置小一点,放在对应的位置,这样可以保证不会误触,当然了如果想要点击效果可以在按钮按下的时候更新一下背景图,这样虽然也可以,但是这样会导致可点击区域变小,体验效果变差,设计方案变得复杂,而且逻辑也不容易处理,是一种非常糟糕的设计。
-当然了,看了我这么多文章的小伙伴应该也猜到我接下来要说什么了,没错,就是自定义 View。当我们面对一些奇葩控件的时候,自定义 View 就变成了一种非常好用的处理方案。
+当然了,看了我这么多文章的小伙伴应该也猜到接下来要说什么了,没错,就是自定义 View。当我们面对一些奇葩控件的时候,自定义 View 就变成了一种非常好用的处理方案。
相信小伙伴们看过 [前面的文章][CustomViewIndex] 之后,对各种图形的绘制已经不成问题了,所以我们直接处理重点问题。
@@ -34,44 +39,33 @@ Region 直接翻译的意思是 地域,区域。**在此处应该是区域的
**本文中我们重点要使用到的是 Region 中的 `contains` 方法,这个方法可以判断一个点是否包含在该区域内。**
-下面是一个简单的示例程序:
+接下来是一个简单的示例,**判断手指是否是在圆形区域内按下**:
+
+
+
+代码:
```java
-public class RegionClickView extends View {
- Paint mPaint;
- Region globalRegion;
- Region circleRegion1;
- Region circleRegion2;
- Path circlePath1;
- Path circlePath2;
+public class RegionClickView extends CustomView {
+ Region circleRegion;
+ Path circlePath;
public RegionClickView(Context context) {
super(context);
-
- mPaint = new Paint();
- mPaint.setStyle(Paint.Style.FILL);
- mPaint.setColor(Color.GRAY);
-
- // 用Path创建两个圆
- circlePath1 = new Path();
- circlePath2 = new Path();
- circlePath1.addCircle(100, 100, 50, Path.Direction.CW);
- circlePath2.addCircle(400, 400, 50, Path.Direction.CW);
-
- // 创建 Region
- circleRegion1 = new Region();
- circleRegion2 = new Region();
+ mDeafultPaint.setColor(0xFF4E5268);
+ circlePath = new Path();
+ circleRegion = new Region();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
- // 将剪裁边界设置为视图大小
- globalRegion = new Region(0, 0, w, h);
-
+ // ▼在屏幕中间添加一个圆
+ circlePath.addCircle(w/2, h/2, 300, Path.Direction.CW);
+ // ▼将剪裁边界设置为视图大小
+ Region globalRegion = new Region(-w, -h, w, h);
// ▼将 Path 添加到 Region 中
- circleRegion1.setPath(circlePath1, globalRegion);
- circleRegion2.setPath(circlePath2, globalRegion);
+ circleRegion.setPath(circlePath, globalRegion);
}
@Override
@@ -82,45 +76,39 @@ public class RegionClickView extends View {
int y = (int) event.getY();
// ▼点击区域判断
- if (circleRegion1.contains(x,y)){
- Toast.makeText(this.getContext(),"圆1被点击",Toast.LENGTH_SHORT).show();
- }
- if (circleRegion2.contains(x,y)){
- Toast.makeText(this.getContext(),"圆2被点击",Toast.LENGTH_SHORT).show();
+ if (circleRegion.contains(x,y)){
+ Toast.makeText(this.getContext(),"圆被点击",Toast.LENGTH_SHORT).show();
}
break;
}
- return super.onTouchEvent(event);
+ return true;
}
@Override
protected void onDraw(Canvas canvas) {
- // ▼注意此处将全局变量转化为局部变量,方便 GC 回收 Canvas
- Path circle1 = circlePath1;
- Path circle2 = circlePath2;
-
- // 绘制两个圆
- canvas.drawPath(circle1,mPaint);
- canvas.drawPath(circle2,mPaint);
+ // 绘制圆
+ canvas.drawPath(circle1,mDeafultPaint);
}
}
```
> 代码中比较重要的内容都用 ▼ 符号标记出来了。
-上述代码非常简单,就是创建了两个 Path,在 Path 中添加圆形,之后将 Path 设置到 Region 中,当手指在屏幕上按下的时候判断手指按下位置是否在 Region 区域内。
-
-代码整体逻辑非常简单,大家测试一下就行了。
+上述代码非常简单,就是创建了个 Path 并在其中添加圆形,之后将 Path 设置到 Region 中,当手指在屏幕上按下的时候判断手指位置是否在 Region 区域内。
## 画布变换后坐标转换问题
-还是上述的例子,绘制一个上下左右选择按键,这个控件是上下左右对称的,熟悉我代码风格的小伙伴都知道,如果遇上这种问题,我肯定是要将坐标系平移到这个控件中心的,这样数据比较好计算,然而进行画布变换操作会产生一个新问题:**手指触摸的坐标系和画布坐标系不统一,就可能引起手指触摸位置和绘制位置不统一。**
+还是本文一开始的例子,绘制一个上下左右选择按键,这个控件是上下左右对称的,熟悉我代码风格的小伙伴都知道,如果遇上这种问题,我肯定是要将坐标系平移到这个控件中心的,这样数据比较好计算,然而进行画布变换操作会产生一个新问题:**手指触摸的坐标系和画布坐标系不统一,就可能引起手指触摸位置和绘制位置不统一。**
举个栗子:
-> 在手指按下位置绘制一个圆。
+> 画布移动后在手指按下位置绘制一个圆,可以看到,直接拿手指触摸位置的坐标来绘制会导致绘制位置不正确,**两者坐标是相同的,但是由于坐标系不同,导致实际显示位置不同。**
+
+
+
+代码:
```java
public class CanvasVonvertTouchTest extends CustomView{
@@ -160,24 +148,20 @@ public class CanvasVonvertTouchTest extends CustomView{
float x = down_x;
float y = down_y;
- // 绘制触摸坐标系,颜色为灰色,为了能够显示出坐标系,将坐标系位置稍微偏移了一点
- drawTouchCoordinateSpace(canvas);
-
- canvas.translate(mViewWidth/2, mViewHeight/2); // 画布平移
-
- // 绘制平移后的坐标系,颜色为红色
- drawTranslateCoordinateSpace(canvas);
+ drawTouchCoordinateSpace(canvas); // 绘制触摸坐标系 灰色
+
+ // ▼注意画布平移
+ canvas.translate(mViewWidth/2, mViewHeight/2);
+
+ drawTranslateCoordinateSpace(canvas); // 绘制平移后的坐标系,红色
- // 如果没有就返回
- if (x == -1 && y == -1)
- return;
+ if (x == -1 && y == -1) return; // 如果没有就返回
- // 在触摸位置绘制一个小圆
- canvas.drawCircle(x,y,20,mDeafultPaint);
+ canvas.drawCircle(x,y,20,mDeafultPaint); // 在触摸位置绘制一个小圆
}
/**
- * 绘制触摸坐标系,颜色为灰色,为了能够显示出坐标系,将坐标系位置稍微偏移了一点
+ * 绘制触摸坐标系,灰色,为了能够显示出坐标系,将坐标系位置稍微偏移了一点
*/
private void drawTouchCoordinateSpace(Canvas canvas) {
canvas.save();
@@ -189,7 +173,7 @@ public class CanvasVonvertTouchTest extends CustomView{
}
/**
- * 绘制平移后的坐标系,颜色为红色
+ * 绘制平移后的坐标系,红色
*/
private void drawTranslateCoordinateSpace(Canvas canvas) {
CanvasAidUtils.set2DAxisLength(500, 500, 700, 700);
@@ -200,12 +184,6 @@ public class CanvasVonvertTouchTest extends CustomView{
}
```
-可以看到,直接拿手指触摸位置的坐标来绘制会导致绘制位置不正确,大概会像这样子:
-
-
-
-两者坐标是相同的,但是由于坐标系不同,导致实际显示位置不同。
-
**那么问题来了,我们在之前的文章中讲过,映射不同坐标系的坐标用 什么来着?**
**是 Matrix。**
@@ -214,7 +192,11 @@ public class CanvasVonvertTouchTest extends CustomView{
> **Matrix 是一个矩阵,主要功能是坐标映射,数值转换。**
-那么接下来我们就对上面的示例进行简单的改造一下,让触摸位置和实际绘制绘制重合。
+那么接下来我们就对上面的示例进行简单的改造一下,让触摸位置和实际绘制绘制重合。小白点和黑色的圆没有完全重合是因为系统显示触摸位置的绘制逻辑和我使用的绘制逻辑不太相同导致的。
+
+
+
+代码:
**注意:比较重要的修改位置用▼标记出来了。**
@@ -256,17 +238,13 @@ public class CanvasVonvertTouchTest extends CustomView{
protected void onDraw(Canvas canvas) {
float[] pts = {down_x, down_y};
- // 绘制触摸坐标系,颜色为灰色,为了能够显示出坐标系,将坐标系位置稍微偏移了一点
- drawTouchCoordinateSpace(canvas);
-
- canvas.translate(mViewWidth/2, mViewHeight/2); // 画布平移
-
- // 绘制平移后的坐标系,颜色为红色
- drawTranslateCoordinateSpace(canvas);
-
- // 如果没有就返回
- if (pts[0] == -1 && pts[1] == -1)
- return;
+ drawTouchCoordinateSpace(canvas); // 绘制触摸坐标系,灰色
+ // ▼注意画布平移
+ canvas.translate(mViewWidth/2, mViewHeight/2);
+
+ drawTranslateCoordinateSpace(canvas); // 绘制平移后的坐标系,红色
+
+ if (pts[0] == -1 && pts[1] == -1) return; // 如果没有就返回
// ▼ 获得当前矩阵的逆矩阵
Matrix invertMatrix = new Matrix();
@@ -301,15 +279,8 @@ public class CanvasVonvertTouchTest extends CustomView{
CanvasAidUtils.draw2DCoordinateSpace(canvas);
}
}
-
```
-**绘制结果:**
-
-小白点和黑色的圆没有完全重合是因为系统显示触摸位置的绘制逻辑和我使用的绘制逻辑不太相同,两者的位置是一样的。
-
-
-
其实核心部分就这两点:
```java
@@ -336,6 +307,10 @@ invertMatrix.mapPoints(pts);
在解决了上述两大难题之后,相信不论形状如何奇葩的自定义控件,基本上都难不倒大家了,最后用一个简单的示例作为结尾,还是文章开头所举的例子,核心内容就是上面讲的两个东西。
+
+
+代码:
+
```java
public class RemoteControlMenu extends CustomView {
Path up_p, down_p, left_p, right_p, center_p;
@@ -545,8 +520,6 @@ public class RemoteControlMenu extends CustomView {
当手指在某一区域活动时,该区域会高亮显示,如果注册了监听器,点击某一区域会触发监听器回调。
-
-
## 总结
From a71f8d9f80789d01563686959973d2cecf49d644 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Sun, 20 Nov 2016 21:11:57 +0800
Subject: [PATCH 047/160] Update
---
ChaosCrystal/Markdowm/README.md | 1 -
.../\345\237\272\346\234\254\350\257\255\346\263\225.md" | 3 ---
2 files changed, 4 deletions(-)
delete mode 100644 ChaosCrystal/Markdowm/README.md
delete mode 100644 "ChaosCrystal/Markdowm/\345\237\272\346\234\254\350\257\255\346\263\225.md"
diff --git a/ChaosCrystal/Markdowm/README.md b/ChaosCrystal/Markdowm/README.md
deleted file mode 100644
index 7d82df34..00000000
--- a/ChaosCrystal/Markdowm/README.md
+++ /dev/null
@@ -1 +0,0 @@
-# Markdown
diff --git "a/ChaosCrystal/Markdowm/\345\237\272\346\234\254\350\257\255\346\263\225.md" "b/ChaosCrystal/Markdowm/\345\237\272\346\234\254\350\257\255\346\263\225.md"
deleted file mode 100644
index b711b2b6..00000000
--- "a/ChaosCrystal/Markdowm/\345\237\272\346\234\254\350\257\255\346\263\225.md"
+++ /dev/null
@@ -1,3 +0,0 @@
-# Markdown 基本语法
-
-## 标题
From 4cbf83c34df92880f804112a00a95889e9bbce3a Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Mon, 21 Nov 2016 22:55:19 +0800
Subject: [PATCH 048/160] Update
---
CustomView/Advance/[17]touch-matrix-region.md | 38 +++++++++++++++++--
1 file changed, 35 insertions(+), 3 deletions(-)
diff --git a/CustomView/Advance/[17]touch-matrix-region.md b/CustomView/Advance/[17]touch-matrix-region.md
index 487026a2..026ed5df 100644
--- a/CustomView/Advance/[17]touch-matrix-region.md
+++ b/CustomView/Advance/[17]touch-matrix-region.md
@@ -1,5 +1,4 @@
-# 特殊控件的事件处理方案
-
+# 特殊形状控件的事件处理方案
本文带大家了解 Android 特殊形状控件的事件处理方式,主要是利用了 Region 和 Matrix 的一些方法,超级实用的事件处理方案,相信看完本篇之后,任何奇葩控件的事件处理都会变得十分简单。
@@ -11,6 +10,8 @@
>
> 本文中所有的 自定义View 均继承自 CustomView ,这是一个自定义的超类,目的是简化 自定义View 部分常用操作,你可以在 [ViewSupport](https://github.com/GcsSloop/ViewSupport/wiki/CustomView) 中找到它以及关于它的简介。
+## ⚠️ 警告:测试本文章示例之前请关闭硬件加速。
+
## 特殊形状控件
在通常的情况下,自定义 View 直接使用系统的事件体系处理就行,我们也不需要特殊处理,然而当一些特殊的控件出现的时候,麻烦就来了,举个栗子:
@@ -86,8 +87,10 @@ public class RegionClickView extends CustomView {
@Override
protected void onDraw(Canvas canvas) {
+ // ▼注意此处将全局变量转化为局部变量,方便 GC 回收 canvas
+ Path circle = circlePath;
// 绘制圆
- canvas.drawPath(circle1,mDeafultPaint);
+ canvas.drawPath(circle,mDeafultPaint);
}
}
```
@@ -522,6 +525,35 @@ public class RemoteControlMenu extends CustomView {
+## 关于硬件加速的问题
+
+**硬件加速是个好东西,但是处理不好会引起诸多问题,博主为了怕麻烦我一直关闭硬件加速。**
+
+然而硬件加速在 Android 4.0 以上是默认开启的,这就导致了有好几位魔法师反馈测试结果和我的测试结果不同,我来简单说明一下硬件加速干了什么事情,以及这些文章中的锅是如何产生的,应该由谁来背。
+
+我在 [Matrix 原理][Matrix_Basic] 中说过 Matrix 的作用: **Matrix作用就是坐标映射。**
+其核心功能就是将单个 View 的坐标系转化为屏幕(物理)坐标系,虽然转换一次费不了多少时间,但是当执行动画效果等需要大量快速重绘的情况下,耗费的时间就需要考量一下了,于是乎,硬件加速干了一件非常**精明**的事情,**把所有画布坐标系都设置为屏幕(物理)坐标系**,之后在 View 绘制区域设置一个遮罩,保证绘制内容不会超过 View 自身的大小,**这样就直接跳过坐标转换过程,可以节省坐标系之间数值转换耗费的时间**。因此导致了以下问题:
+
+1. 开启硬件加速情况下 event.getX() 和 不开启情况下 event.getRawX() 等价,获取到的是屏幕(物理)坐标 (本文的锅)。
+2. 开启硬件加速情况下 event.getRawX() 数值是一个错误数值,因为本身就是全局的坐标又叠加了一次 View 的偏移量,所以肯定是不正确的 (本文的锅)。
+3. 从 Canvas 获取到的 Matrix 是全局的,默认情况下 x,y 偏移量始终为0,因此你不能从这里拿到当前 View 的偏移量 ( Matrix系列文章中的锅 )。
+4. 由于其使用的是遮罩来控制绘制区域,所以如果重绘 path 时,如果 path 区域变大,但没有执行单步操作会导致 path 绘制不完整或者看起来比较奇怪 (Path系列文章中的锅)。
+
+很显然,这个硬件加速有点6,制造了各种锅想让我来背,然而智慧的我早已看穿一切,默默的把硬件加速关闭了,因为我不知道它还有多少锅没亮出来。
+
+**这里顺便挖个坑,等我搞明白硬件加速扔锅的逻辑之后,专门写一篇硬件加速的文章,把硬件加速的锅全埋进去,再也不背这口大黑锅了**。
+**(╯°Д°)╯︵ ┻━┻**
+
+
+
+**个人建议:**
+
+1. 如果应用不需要大量重绘,那么硬件加速开启和关闭都无所谓,推荐关闭。
+2. 如果应用要兼容到 3.0 以下,建议不要使用硬件加速的特性,或者进行兼容性处理。
+3. 如果应用存在大量的动画等需要快速重绘的场景,在开启硬件加速时注意测试。
+4. 如果 自定义View 出现与绘图相关的异常,请务必检查一下硬件加速。
+5. 如果想关掉硬件加速看这里: [Android如何关闭硬件加速](https://github.com/GcsSloop/AndroidNote/issues/7) 。
+
## 总结
本文虽然代码比较多,但核心概念非常简单,主要涉及以下两点:
From b262f0d463c2a0a84738d2d14bae973423014900 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Tue, 22 Nov 2016 20:47:36 +0800
Subject: [PATCH 049/160] Update
---
CustomView/Advance/[17]touch-matrix-region.md | 26 +++++++++----------
1 file changed, 12 insertions(+), 14 deletions(-)
diff --git a/CustomView/Advance/[17]touch-matrix-region.md b/CustomView/Advance/[17]touch-matrix-region.md
index 026ed5df..cf23d6cb 100644
--- a/CustomView/Advance/[17]touch-matrix-region.md
+++ b/CustomView/Advance/[17]touch-matrix-region.md
@@ -1,16 +1,9 @@
-# 特殊形状控件的事件处理方案
+# 特殊控件的事件处理方案
本文带大家了解 Android 特殊形状控件的事件处理方式,主要是利用了 Region 和 Matrix 的一些方法,超级实用的事件处理方案,相信看完本篇之后,任何奇葩控件的事件处理都会变得十分简单。
-不得不说,Android 对事件体系封装的非常棒,即便对事件体系不太了解的人,只要简单的调用方法就能使用,而且具有防呆设计,能够保证事件流的完整性和统一性,最大可能性的避免了事件处理的混乱,着实令人佩服。
-
-然而世界上并没有绝对完美的东西,**当【事件处理】遇上【自定义View】,一场好戏就开演了,玩的好叫坐镇军前,指挥千军万马而分毫不乱,玩的不好就是抓耳挠腮,眼见敌人前后包抄而无可奈何。**
-
-> #### 注意:
->
-> 本文中所有的 自定义View 均继承自 CustomView ,这是一个自定义的超类,目的是简化 自定义View 部分常用操作,你可以在 [ViewSupport](https://github.com/GcsSloop/ViewSupport/wiki/CustomView) 中找到它以及关于它的简介。
-
-## ⚠️ 警告:测试本文章示例之前请关闭硬件加速。
+不得不说,Android 对事件体系封装的非常棒,即便对事件体系不太了解的人,只要简单的调用方法就能使用,而且具有防呆设计,能够保证事件流的完整性和统一性,最大可能性的避免了事件处理的混乱,着实令人佩服。
+**然而世界上并没有绝对完美的东西,当"事件处理"遇上"自定义View",一场好戏就开演了。**
## 特殊形状控件
@@ -30,7 +23,10 @@
相信小伙伴们看过 [前面的文章][CustomViewIndex] 之后,对各种图形的绘制已经不成问题了,所以我们直接处理重点问题。
-
+> #### 注意:
+>
+> 本文中所有的 自定义View 均继承自 CustomView ,这是一个自定义的超类,目的是简化 自定义View 部分常用操作,你可以在 [ViewSupport](https://github.com/GcsSloop/ViewSupport/wiki/CustomView) 中找到它以及关于它的简介。
+> **⚠️ 警告:测试本文章示例之前请关闭硬件加速。**
## 特殊形状控的点击区域判断
@@ -548,12 +544,14 @@ public class RemoteControlMenu extends CustomView {
**个人建议:**
-1. 如果应用不需要大量重绘,那么硬件加速开启和关闭都无所谓,推荐关闭。
-2. 如果应用要兼容到 3.0 以下,建议不要使用硬件加速的特性,或者进行兼容性处理。
-3. 如果应用存在大量的动画等需要快速重绘的场景,在开启硬件加速时注意测试。
+1. APP全局关闭硬件加速。
+2. 针对动画较多的 Activity 或者 View 单独开启硬件加速。
+3. 如果应用要兼容到 3.0 以下,不要使用硬件加速的特性,或者进行兼容处理。
4. 如果 自定义View 出现与绘图相关的异常,请务必检查一下硬件加速。
5. 如果想关掉硬件加速看这里: [Android如何关闭硬件加速](https://github.com/GcsSloop/AndroidNote/issues/7) 。
+
+
## 总结
本文虽然代码比较多,但核心概念非常简单,主要涉及以下两点:
From b54a9d3ef0906e842760bee3dd9ce2a3a8770baf Mon Sep 17 00:00:00 2001
From: sloop
Date: Tue, 22 Nov 2016 20:51:31 +0800
Subject: [PATCH 050/160] Update
---
README.md | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index d372db4c..0c492c03 100644
--- a/README.md
+++ b/README.md
@@ -119,7 +119,13 @@
* 商业用途请点击最下面图片联系本人。
-## About Me
+## 博文修复计划
+
+由于自己的知识水平有限,书写的文章难免会出现一些问题。
+
+随着知识增长和知识面的扩大,发现了一些之前未曾注意到的问题,故有此博文修复计划,本次修复不仅会修复之前文章中的瑕疵和纰漏,甚至会对文章的知识结构稍作调整,所有修复的文章和调整后的内容都会在微博重新发布声明,点击下面关注我的微博可以第一时间了解到相关信息,另外据说关注我的微博会变帅哦。
+
+本次博文修复计划主要针对 个人博客 和 GitHub,由于博主精力有限以及某些平台自身限制,对于其他平台仅能修复部分内容。
### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop)
From 41e7a1638594a6328f1fa4f5ddb3f30a279e7d99 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Wed, 23 Nov 2016 23:20:39 +0800
Subject: [PATCH 051/160] Update
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 0c492c03..1887f124 100644
--- a/README.md
+++ b/README.md
@@ -125,7 +125,7 @@
随着知识增长和知识面的扩大,发现了一些之前未曾注意到的问题,故有此博文修复计划,本次修复不仅会修复之前文章中的瑕疵和纰漏,甚至会对文章的知识结构稍作调整,所有修复的文章和调整后的内容都会在微博重新发布声明,点击下面关注我的微博可以第一时间了解到相关信息,另外据说关注我的微博会变帅哦。
-本次博文修复计划主要针对 个人博客 和 GitHub,由于博主精力有限以及某些平台自身限制,对于其他平台仅能修复部分内容。
+**本次博文修复计划主要针对 个人博客 和 GitHub,由于博主精力有限以及某些平台自身限制,对于其他平台仅能修复部分内容。**
### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop)
From 311f9de00f649005714f7c2695767a53284fac19 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Thu, 24 Nov 2016 23:38:49 +0800
Subject: [PATCH 052/160] Update
---
CustomView/Advance/[04]Canvas_PictureText.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CustomView/Advance/[04]Canvas_PictureText.md b/CustomView/Advance/[04]Canvas_PictureText.md
index 9644f4f1..ba7f2c4f 100644
--- a/CustomView/Advance/[04]Canvas_PictureText.md
+++ b/CustomView/Advance/[04]Canvas_PictureText.md
@@ -362,7 +362,7 @@ PS:图片左上角位置默认为坐标原点。
绘制文字部分大致可以分为三类:
-第一类只能指定文本基线位置位置(基线x默认在字符串左侧,基线y默认在字符串下方)。
+第一类只能指定文本基线位置(基线x默认在字符串左侧,基线y默认在字符串下方)。
第二类可以分别指定每个文字的位置。
第三类是指定一个路径,根据路径绘制文字。
From 939fd5c4c1980ccbc2c899067c6a78ac849b67e9 Mon Sep 17 00:00:00 2001
From: sloop
Date: Fri, 25 Nov 2016 23:54:41 +0800
Subject: [PATCH 053/160] Update
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 1887f124..715b72ab 100644
--- a/README.md
+++ b/README.md
@@ -125,7 +125,7 @@
随着知识增长和知识面的扩大,发现了一些之前未曾注意到的问题,故有此博文修复计划,本次修复不仅会修复之前文章中的瑕疵和纰漏,甚至会对文章的知识结构稍作调整,所有修复的文章和调整后的内容都会在微博重新发布声明,点击下面关注我的微博可以第一时间了解到相关信息,另外据说关注我的微博会变帅哦。
-**本次博文修复计划主要针对 个人博客 和 GitHub,由于博主精力有限以及某些平台自身限制,对于其他平台仅能修复部分内容。**
+**本次博文修复计划主要针对 [个人博客](http://www.gcssloop.com/#blog) 和 [GitHub](https://github.com/GcsSloop),由于博主精力有限以及某些平台自身限制,对于其他平台仅能修复部分内容。**
### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop)
From b5713c40746cb7196e0388d6592cd85b5dc8c53d Mon Sep 17 00:00:00 2001
From: sloop
Date: Sat, 26 Nov 2016 16:27:37 +0800
Subject: [PATCH 054/160] Update
---
CustomView/Base/[02]AngleAndRadian.md | 10 ++--------
1 file changed, 2 insertions(+), 8 deletions(-)
diff --git a/CustomView/Base/[02]AngleAndRadian.md b/CustomView/Base/[02]AngleAndRadian.md
index 2d4dd12f..07235471 100644
--- a/CustomView/Base/[02]AngleAndRadian.md
+++ b/CustomView/Base/[02]AngleAndRadian.md
@@ -39,15 +39,9 @@
## 三.角度和弧度的换算关系
-根据角度和弧度的的定义和圆的相关知识非常容易就能得出两者的换算公式:
+**圆一周对应的角度为360度(角度),对应的弧度为2π弧度。**
-先设圆的周长为C. 半径为r
-
-C = 2πr;
-
-一周对应的角度为360度(角度),对应的弧度为2π弧度。
-
-故的等价关系: **180(度) = π(弧度).**
+**故得等价关系:360(角度) = 2π(弧度) ==> 180(角度) = π(弧度)**
由等价关系可得如下换算公式:
From f0e9997628b66b1e567ba165b565d9a5aec47db0 Mon Sep 17 00:00:00 2001
From: sloop
Date: Sun, 27 Nov 2016 23:28:01 +0800
Subject: [PATCH 055/160] Update
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index 715b72ab..51884e98 100644
--- a/README.md
+++ b/README.md
@@ -118,6 +118,7 @@
* 明确署名,即至少注明 `作者:GcsSloop` 字样以及文章的原始链接,且不得使用 `rel="nofollow"` 标记。
* 商业用途请点击最下面图片联系本人。
+*****
## 博文修复计划
From 34083af9b1f2b26befa1c5830b055f99ec66ac8b Mon Sep 17 00:00:00 2001
From: sloop
Date: Sun, 27 Nov 2016 23:31:15 +0800
Subject: [PATCH 056/160] Update
---
README.md | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/README.md b/README.md
index 51884e98..ad505ae6 100644
--- a/README.md
+++ b/README.md
@@ -100,6 +100,16 @@
时空折跃准备完毕,[点击开始传送](https://github.com/GcsSloop/AndroidNote/tree/magic-world)。
+*****
+
+## 博文修复计划
+
+由于自己的知识水平有限,书写的文章难免会出现一些问题。
+
+随着知识增长和知识面的扩大,发现了一些之前未曾注意到的问题,故有此博文修复计划,本次修复不仅会修复之前文章中的瑕疵和纰漏,甚至会对文章的知识结构稍作调整,所有修复的文章和调整后的内容都会在微博重新发布声明,点击下面关注我的微博可以第一时间了解到相关信息,另外据说关注我的微博会变帅哦。
+
+**本次博文修复计划主要针对 [个人博客](http://www.gcssloop.com/#blog) 和 [GitHub](https://github.com/GcsSloop),由于博主精力有限以及某些平台自身限制,对于其他平台仅能修复部分内容。**
+
******
## 版权声明
@@ -118,16 +128,6 @@
* 明确署名,即至少注明 `作者:GcsSloop` 字样以及文章的原始链接,且不得使用 `rel="nofollow"` 标记。
* 商业用途请点击最下面图片联系本人。
-*****
-
-## 博文修复计划
-
-由于自己的知识水平有限,书写的文章难免会出现一些问题。
-
-随着知识增长和知识面的扩大,发现了一些之前未曾注意到的问题,故有此博文修复计划,本次修复不仅会修复之前文章中的瑕疵和纰漏,甚至会对文章的知识结构稍作调整,所有修复的文章和调整后的内容都会在微博重新发布声明,点击下面关注我的微博可以第一时间了解到相关信息,另外据说关注我的微博会变帅哦。
-
-**本次博文修复计划主要针对 [个人博客](http://www.gcssloop.com/#blog) 和 [GitHub](https://github.com/GcsSloop),由于博主精力有限以及某些平台自身限制,对于其他平台仅能修复部分内容。**
-
### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop)
From 7aae549fe655c907c814191c0e48ca86d9d49e11 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Tue, 29 Nov 2016 22:36:39 +0800
Subject: [PATCH 057/160] Update
---
Lecture/gdg-developer-growth-guide.md | 164 ++++++++++++++++++++++++++
1 file changed, 164 insertions(+)
create mode 100755 Lecture/gdg-developer-growth-guide.md
diff --git a/Lecture/gdg-developer-growth-guide.md b/Lecture/gdg-developer-growth-guide.md
new file mode 100755
index 00000000..d98b68c2
--- /dev/null
+++ b/Lecture/gdg-developer-growth-guide.md
@@ -0,0 +1,164 @@
+# 程序员练级指北
+
+
+
+之前非常有幸收到 [脉脉不得语](https://github.com/inferjay) 的邀请参加 郑州GDG[^1] 举办的 DevFest[^2] 活动,并上台分享了一下自己的拙见,回来之后我将自己演讲的内容整理了一下,并分享给大家,希望对一些人有帮助,关于 郑州GDG 更多的活动内容,大家可以到 [GDGZhengzou](https://github.com/GDGZhengzhou/Events) 查看。另外,欢迎关注我的 [GitHub](https://github.com/GcsSloop) 和 [微博](http://weibo.com/GcsSloop) 。
+
+**我在本次活动中的演讲主题是《程序员练级指北》,主要内容如何从零开始,并逐渐成长为一名合格的程序员,里面的内容是基于自身的经历和见解所书写的,并不一定适合所有人,建议选择性采纳。**
+
+**为了演讲不那么枯燥乏味,我用了很多口语化的描述,并将程序员分了等级,从零装备的新手到钻石级别套装的大佬,其实各个等级之间并没有严格的分界点,只是大致的划分,大家理解便好。另外本文和现场演讲可能稍有差别,下面开始正文。**
+
+Hello,大家好,我是 GcsSloop,今天是我第一次在这么多陌生人面前装逼,啊不,演讲,心里很是忐忑,现在我还能感受到自己的心扑通扑通的跳。
+
+我呢,主要学习 Android,爱好装逼,喜欢钱,目前正在向段子手的方向上越走越远。
+
+非常高兴今天能站在这里向大家分享我自己的观点,因为在坐的的技术牛人太多了,作为一个没什么技术实力的新人,不敢在这里班门弄斧讲技术,要不然正讲着哪位大牛突然站起来说我讲的不对,这不就尴尬了么。
+
+由于本人爱好装逼,自然也研究了一些心得,所以我今天分享一下程序员如何在不同阶段正确的装逼。
+
+## 第一阶段 - 新手村
+
+
+
+作为编程的新手,自然是不能在大佬面前装逼的,否则分分钟被吊打。这一阶段我们要先给自己定一个小目标,什么是小目标呢?例如挣他个一百万?这对于我们来说当然是不现实的,作为编程菜鸟,小目标当然是稍微努力一下就能完成的,例如:看完几个新手教程,时间不要太长,两三天能完成的任务量就够了。当然了,光看还不行的,要发在朋友圈,让大家觉得你是一个非常有上进心的人。
+
+**为什么要发在朋友圈呢?装逼是一方面,除此之外,过了两天你就会发现,这两天光顾着打游戏了,新手教程居然忘记看了,这时候去看看朋友圈,再看看上面的赞,心里想如果这要是不弄完以后还如何在朋友面前装逼,自己装的逼跪着也要学完。**
+
+这一阶段注意两点:
+
+1. **任务周期要短,任务量要小**,如果周期太长,例如一个月,到月底发现弄不完了,就会想反正也弄不完了,就这样吧,容易破罐子破摔。
+2. **要形成周围的监督机制**,如果没人看着,反正也没人知道,那就随便学喽,过不了几天自己就放弃了。
+
+如果按照计划执行了第一阶段的任务,那么经过大约一两个月的时间,新手教程基本上就看完了,相当于集齐了一套新手装备,顺利进入下一阶段。
+
+## 第二阶段 - 初级套装
+
+
+
+初级套装虽然攻击和防御力都很弱,但至少能单挑一些野怪了,有了一定的装逼资本,这时候就要在新手村附近刷一些野怪练级。
+
+既然是刷怪升级,自然要选一个怪物比较集中的地方,最好还要有不同等级的怪物,这样方便刷经验,这个地方是哪里呢?个人觉得最适合的地方就是学校OJ题库,不一定要是自己学校的,大部分学校的OJ题库都是开放的,这里面的题目比较多,难度也分了很多等级,是非常适合新手练习的场所。不熟悉OJ系统的建议搜索了解一下。
+
+**这一阶段非常容易卡关,经常发现自己有两点不会,就是这也不会,那也不会。**
+
+然而作为编程界的新手,我们遇到的大部分问题肯定已经有大神踩过坑了,在99%的情况下,用百度或者Google搜索题目描述就能得到攻略方式,但不要偷懒直接复制,要自己亲自写一遍代码,这样有助于自己了解不同类型题目的模式。
+
+如果你发现了一道题的答案搜索不到,但是自己解决了,那么恭喜你捡到宝了,说明你遇到了一个稀有的或者新增的怪物,赶紧整理一下攻略过程发博客吧,又能装逼了。
+
+这样过了一段时间你就会发现,这些野怪大部分都很弱,只会几种常见的套路而已,除了最基础的题型外,归纳起来就是:堆、栈、树、表、图。
+
+每一类野怪都有特定的攻略方法,常用的有:枚举,遍历,分治,贪婪,动态,线性,暴力打表 以及 抖机灵。
+
+我在大一的时候加入了学校的打野攻略组,就是传说中的ACM,在里面待了大约有一年的时间,虽然没拿过大奖项,但好在通过刷题把自己的基础巩固了。
+
+这一阶段反映自己进步的最好标准就是 做题量 和 排行榜,我最高在校内排到前5,两年没有更新的情况下目前依旧在前50之内,不要问我哪个学校的,是我学校的,看一下OJ排行榜自然能认出我,我在排行榜上也叫GcsSloop。
+
+装逼完毕,进入下一阶段。
+
+## 第三阶段 - 青铜套装
+
+
+
+刷野怪时间长了就会发现其实也挺没意思的,不过好在能够练装备,把数据结构知识和一些常见的算法弄明白,熟悉敌人的攻击套路,这样经过一段时间后,装备基本也升级到青铜了,这时候就应该尝试去挑战一下区域小boss了。
+
+什么是挑战区域小boss呢,就是尝试制作一个完整的项目并发布出去,有人使用即可。
+
+挑战小boss可以组团或者单挑,个人虽然喜欢组团,但无奈我们学校这个服务器太弱了,基本找不到合适的队友,尝试了几次组队最终都以失败而告终,无奈只能去单挑。
+
+单挑boss要量力而行,不能找太强的boss,根据我自己的经验,单挑太强的boss会死的很难看,自己曾经有无数个项目胎死腹中或者半路夭折。
+
+选择一个比较弱的boss,例如:发布一个简单的 app 到应用市场,获取到一定的下载量,或者写一个开源库,获得一点 star。
+
+**这一阶段锻炼的是项目构建能力,能够完整的构建一个项目并发布出去,有人使用就算成功了。**
+
+这样既能带来成就感,又能装逼,发完之后记得告诉身边的人自己有内容发布了,再次装一波。
+
+## 第四阶段 - 白银套装
+
+
+
+单挑完一些区域小boss后,将套装升级为白银,有了一定的装逼资本,此时发现总在朋友圈装逼成就感太差了,这时候可以去论坛、贴吧、社区、微博等地方好好的装一把,让那些装备还没有升级起来的小白看看这套锃亮的白银套装是多么耀眼,不过仍要注意不能太过火,在出来帮助新手解决问题的时候顺便展示一下白银套装的威力就行了,装的太狠容易被大佬发现抓起来吊打,这不就尴尬了么。
+
+顺便说一下,如果从培训机构出来的,正常应该是白银初级,自身有基础加上比较努力可能会达到白银中级,能拿到白银套装说明已经达到了职业程序员最基本的标准。
+
+**很多人打到白银就开始迷茫了,去挑战小boss吧太没成就感,那些看起来就很肥美的中级和高级boss基本已经被大公司瓜分了,自己根本无从插手。所以这时候就要考虑加入公司的问题了。**
+
+那些比较好的公司,例如:网易、阿里、腾讯 等肯定有一堆人挤破头都想进,作为一个刚刚拿到白银套装的人想挤进去不能说没有机会,但肯定很难。但作为拥有白银套装的人,想加入一些小公司倒还算比较容易。
+
+个人建议初期有条件优先选择比较大的公司,因为这些公司资源多,有成熟的体系,可以帮助我们快速的在某一方向上获得长足的发展。
+
+后期则优先选择发展型的小公司,如果是从大公司跳向一个小公司,一般都会担任不错的职位,这可以帮助我们锻炼技术之外的能力,另外一个原因是随着公司成长可以分得一些红利。
+
+加入公司自然要考虑自身和公司的关系。
+
+个人觉得最核心的一点就是不要依附于公司,也不要抱有敌意,最好的办法就是和公司成为**利益共同体**,在自己为公司创造价值的同时利用公司强大的资源提升自己的能力。
+
+如果太过于依附公司,认为自己努力工作一定能得到相应回报的,根据经济学的假设,人都是自私而理性的,老板亦是如此,所以这类人大部分情况下都不会得到预期的结果。
+
+对公司抱有敌意,认为自己就是打工赚钱的,想偷懒干最少的活,拿最多的钱,因为在偷懒的同时错失了很多发展机会,所以这类人大部分会随着技术的升级而被淘汰掉。
+
+**如何与公司成为利益共同体呢?首先要明确自己的目标,自己加入公司是为了提升自己的能力,而不仅仅是来打工赚钱的,想要实现这一目标则是想办法干更多的活,这样老板肯定乐意,而我们通过干更多的活来接触更多的信息,并且把接触到的信息沉淀成为自己的经验积累,对自身发展也是有益的。**
+
+当然了,我们也不是傻子,当我们自身产生的价值远远超过公司给我们的薪资并且公司资源已经无法支撑我们继续提升的时候,就该抛弃这家公司跳槽了,这一次跳槽势必能进入一个更好的公司深造,可以利用更庞大的资源来辅助我们发展,接触到更多的信息。在信息时代,信息就是金钱,将信息沉淀成为个人经验并不能第一时间转化为金钱,但是总能在未来的某一时刻爆发出惊人的力量。
+
+## 第五阶段 - 黄金套装
+
+
+
+在白银阶段,就具备了职业程序员的基本素养,但是离出任CTO,迎娶白富美,走上人生巅峰还差的很远呢,想要实现这些目标,首先要拿到黄金套装。
+
+一般来说技术发现有两个方向可走:**深度 和 广度。**
+
+我自己使用的是树形型结构,大树的树,**一到两门追求深度,做树木主干,其余的最求广度,做树木枝叶**。
+
+> 有些人也称为T型知识结构,不过我觉得树形更佳贴切,因为我的所有周边技能都是围绕主干展开的,并且是可生长(拓展)的。
+
+我的主方向 Android,自学了 C、Java、Python、Web、Photoshop 等相关知识,除了主方向Android之外,其他方向作为辅助方向,仅仅了解了最基础的内容,也就是能够靠着搜索引擎写一些简单小程序的程度。
+
+树型知识架构有很多好处:
+
+1. **不被单一语言束缚,在实际场景中可以选择最合适的语言**。
+2. **可以跨领域突破,我在研究 Android 2D 绘图逻辑的时候就借鉴了很多 PS 的知识**。
+3. **预留发展空间大,假设某一天,Android市场饱和了或者前景变差了,我可以快速的从树枝中选择一支比较擅长的发展成为主干,而不用每次都从零开始**。
+
+其实我们大部分人都是俗人,是不是黄金套装都无所谓,能赚钱才行,很多人来做技术也是为了赚钱,所以接下来我说的是如何想办法用最小的代价产生最大的价值。想要赚钱首先要明白最基本的商业逻辑,如果我们通过正常的方式从别人那里赚到钱了,说明我们为别人提供了价值,这样别人才愿意付费给我们。
+
+所以我们要想办法用最小的代价为别人提供最大的价值,我写代码之前会多思考,没错,就是学生时代老师经常说的多思考。不过那时候没有人告诉我们该思考什么,因此我自己摸索出来了一套东西。
+
+1. **思考核心目标**,我们是为了某一件事情而写程序,并不是因为写程序了所以要完成这件事情。**核心目标是完成这件事情,而不是写程序。**
+2. **思考是否可以偷懒**,我是否可以用更简单的方式完成目标,例如**不写代码,少写代码**。
+3. **如果写代码了,是否可以产生附加价值**,例如我写这一段代码是否可以抽取成为一个开源库,是否可以将其中的设计思想整理成文共享出去。
+4. **如何产生更大的价值**,我的开源库或者文章如何才能服务更多人,让更多人了解到我的东西。因为只有服务更多人,才能产生更大的价值,我们也才可能从中拿走一部分利润。
+
+经历过这几步的思考,基本上就已经能将所写的每一段代码的价值潜力都发掘出来,进而用最小的力气服务最多的人。当然了,为了别人服务,只是产生了价值,在通常情况下,这部分价值是无法直接提现的,也就是说仅仅产生了价值但自身并没有收益。
+
+不过非常值得庆幸的是,今年很有可能成为内容付费元年,今年已经出来了很多内容变现工具,我知道的就有,微博长文打赏,简书打赏,知乎live,diycode原创打赏 和 掘金原创打赏。
+
+另外,最重要的是,**随着社会的快速发展,很多人有钱了,开始愿意为自己喜欢的事物打赏,这是一切的基础。**
+
+
+## 第六阶段 - 钻石套装
+
+
+
+能拿到钻石套装的人在全球范围内都是屈指可数的,这一类人通常是某一领域的开拓者,就是我们所说的 XXX之父。这些开拓者已经将自己的影响力渗透进了社会的各行各业,说他们推动了社会的进步也是可以的,我有生之年不说集齐套装,仅仅能拿一件钻石武器便足以令我心满意足了。
+
+## 结语
+
+前面啰嗦了这么多,最后告诉大家一个小秘密,如果你想要研究一个人发展的根本,不要直接去问他,因为别人很少会告诉你自己赖以生存的核心技能,他们只会告诉你看起来很光鲜的皮毛,就是所谓的心灵鸡汤,这些听起来很励志,但落实在自己身上通常没什么用,大多数情况下只会给你带来两三天的激情而已。
+
+正确的方法是观察他在做什么事情,以及分析他做这些事情的背后逻辑。如果你能把这些逻辑想清楚了,就把别人真正的核心技能偷过来了。之后根据自身情况一步一步的学着做就行了。
+
+**所以说,你们如果你们想了解我在做什么,为什么不考虑关注一下我的 [新浪微博](http://weibo.com/GcsSloop) 呢?关注一下又不会吃亏!**
+
+### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop)
+
+
+
+[^1]: GDG 全称 Google Developer Group,中文意思是 **谷歌开发者社区** 。
+[^2]: DevFest 开发者节,今年(2016)的举办时间是 9月1日 到 11月30日 之间,全球大部分谷歌开发者社区都会举办该活动,一年一次。
+
+
+
+
+
From e978659ba03f9680665e3b23ed8ca774e47898f8 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Wed, 30 Nov 2016 20:44:26 +0800
Subject: [PATCH 058/160] Update
---
...20\347\240\201\350\247\243\346\236\220.md" | 275 ++++++++++++++++++
1 file changed, 275 insertions(+)
create mode 100644 "SourceAnalysis/AtomicFile\346\272\220\347\240\201\350\247\243\346\236\220.md"
diff --git "a/SourceAnalysis/AtomicFile\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/SourceAnalysis/AtomicFile\346\272\220\347\240\201\350\247\243\346\236\220.md"
new file mode 100644
index 00000000..9c703a4d
--- /dev/null
+++ "b/SourceAnalysis/AtomicFile\346\272\220\347\240\201\350\247\243\346\236\220.md"
@@ -0,0 +1,275 @@
+# AtomicFile 源码解析
+
+
+
+## 什么是 AtomicFile ?
+
+AtomicFile 在 `android.support.v4.util` 包下,**是一个与文件相关的工具类,其作用是保证文件读写的原子性。** 即文件读写的时候全部成功才会更新文件,如果失败则不会影响文件内容。
+
+看官方对其说明:
+
+> Static library support version of the framework's `AtomicFile`, a helper class for performing atomic operations on a file by creating a backup file until a write has successfully completed.
+>
+> 静态支持库版本的AtomicFile,一个帮助类,用于通过创建备份文件对文件执行原子操作,直到写入成功完成。
+>
+> Atomic file guarantees file integrity by ensuring that a file has been completely written and sync'd to disk before removing its backup. As long as the backup file exists, the original file is considered to be invalid (left over from a previous attempt to write the file).
+>
+> 原子文件通过确保文件在删除其备份之前已经完全写入并同步到磁盘,从而保证文件的完整性。只要备份文件存在,原始文件将被视为无效(会尝试写入备份文件中)。
+>
+> Atomic file does not confer any file locking semantics. Do not use this class when the file may be accessed or modified concurrently by multiple threads or processes. The caller is responsible for ensuring appropriate mutual exclusion invariants whenever it accesses the file.
+>
+> 原子文件不提供任何文件锁定语义。当文件可能被多个线程或进程并发访问或修改时,不要使用此类。每当访问文件时,调用者都负责确保适当的互斥变量。
+
+## 主要方法
+
+| ~ | AtomicFile(File baseName)
构造方法,通过File创建AtomicFile |
+| :--------------: | ---------------------------------------- |
+| FileOutputStream | startWrite()
准备写文件,获得一个向文件写入的字节流(FileOutputStream)。 |
+| void | finishWrite(FileOutputStream str)
写入完毕时调用。 |
+| void | failWrite(FileOutputStream str)
写入失败时调用。 |
+| FileInputStream | openRead()
准备读取文件,获得一个从文件读取的字节流(FileInputStream)。 |
+| byte[] | readFully()
将文件中所有内容读取出来用 byte 数组返回,相当于更方便的 openRead 方法。 |
+| File | getBaseFile()
获得原文件地址。 |
+| void | delete()
删除 AtomicFile,包括原文件和备份文件。 |
+
+## AtomicFile 原理
+
+AtomicFile 原理其实非常简单,下面用一张图简单展示一下其原理:
+
+
+
+## AtomicFile 源码解析
+
+源码解析非常简单,根据上面表格中的内容按顺序一个一个看吧。
+
+
+
+#### 构造函数:
+
+根据传进来的文件创建 AtomicFile 类,同时创建辅助备份文件(空文件)。
+
+```java
+private final File mBaseName;
+private final File mBackupName;
+
+public AtomicFile(File baseName) {
+ mBaseName = baseName; // 记录原始文件
+ mBackupName = new File(baseName.getPath() + ".bak"); // 创建备份文件
+}
+```
+
+
+
+#### 开始写入(startWrite)
+
+在获取向文件写入的输出流之前,它会对原文件进行备份,同时清除原文件内容。
+
+同时根据其原理可知,它并不是线程安全的,如果需要多线程操作等,最好自己加锁,如果一个线程未写完,直接开启了另一个线程进行写入可能会导致文件内容丢失。
+
+**另外使用这种方法获得到的 FileOutputStream 不要直接关闭,写入完成的时候需要调用 `finishWrite` 或者`failWrite` 进行关闭,否则下次读取的时候会因为备份文件存在而使本次写入失效。**
+
+```java
+public FileOutputStream startWrite() throws IOException {
+ // 如果备份文件不存在,将原文件重命名为备份文件,并删除原文件
+ if (mBaseName.exists()) {
+ if (!mBackupName.exists()) {
+ // 如果原文件存在且备份文件不存在,直接将原文件重命名为备份文件
+ if (!mBaseName.renameTo(mBackupName)) {
+ Log.w("AtomicFile", "Couldn't rename file " + mBaseName
+ + " to backup file " + mBackupName);
+ }
+ } else {
+ mBaseName.delete(); // 删除原文件
+ }
+ }
+ // 保证原文件存在,并根据原文件创建一个输出流。
+ FileOutputStream str = null;
+ try {
+ str = new FileOutputStream(mBaseName);
+ } catch (FileNotFoundException e) {
+ File parent = mBaseName.getParentFile();
+ if (!parent.mkdirs()) {
+ throw new IOException("Couldn't create directory " + mBaseName);
+ }
+ FileUtils.setPermissions(
+ parent.getPath(),
+ FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
+ -1, -1);
+ try {
+ str = new FileOutputStream(mBaseName);
+ } catch (FileNotFoundException e2) {
+ throw new IOException("Couldn't create " + mBaseName);
+ }
+ }
+ return str; // 返回输出流。
+}
+```
+
+
+
+#### 写入完成(finishWrite)
+
+写入完成的时候需要用户调用该方法,该方法会关闭 FileOutputStream ,并且会删除备份文件以保证文件的唯一性。
+
+```java
+public void finishWrite(FileOutputStream str) {
+ if (str != null) {
+ FileUtils.sync(str);
+ try {
+ str.close();
+ mBackupName.delete();
+ } catch (IOException e) {
+ Log.w("AtomicFile", "finishWrite: Got exception:", e);
+ }
+ }
+}
+```
+
+
+
+#### 写入失败(failWrite)
+
+写入失败时会调用该方法,该方法会关闭 FileOutputStream,并且从备份文件恢复文件内容。
+
+```java
+public void failWrite(FileOutputStream str) {
+ if (str != null) {
+ FileUtils.sync(str);
+ try {
+ str.close();
+ mBaseName.delete();
+ mBackupName.renameTo(mBaseName);
+ } catch (IOException e) {
+ Log.w("AtomicFile", "failWrite: Got exception:", e);
+ }
+ }
+}
+```
+
+
+
+#### 准备读取(openRead)
+
+该方法会获取到一个读取文件的输入流(FileInputStream),并且在备份文件存在时,会简单粗暴的删除原文件,并从备份文件恢复内容。
+
+```java
+public FileInputStream openRead() throws FileNotFoundException {
+ if (mBackupName.exists()) {
+ mBaseName.delete();
+ mBackupName.renameTo(mBaseName);
+ }
+ return new FileInputStream(mBaseName);
+}
+```
+
+
+
+#### 读取全部(readFully)
+
+该方法会将文件中的所有内容转化为一个 byte 数组并且返回,所以不建议使用该方法读取大文件。
+
+```java
+public byte[] readFully() throws IOException {
+ FileInputStream stream = openRead();
+ try {
+ int pos = 0;
+ int avail = stream.available();
+ byte[] data = new byte[avail];
+ // 通过循环读取的方式将文件所有内容都装入 byte 数组中。
+ while (true) {
+ int amt = stream.read(data, pos, data.length-pos);
+ //Log.i("foo", "Read " + amt + " bytes at " + pos
+ // + " of avail " + data.length);
+ if (amt <= 0) {
+ //Log.i("foo", "**** FINISHED READING: pos=" + pos
+ // + " len=" + data.length);
+ return data;
+ }
+ pos += amt;
+ avail = stream.available();
+ if (avail > data.length-pos) {
+ byte[] newData = new byte[pos+avail];
+ System.arraycopy(data, 0, newData, 0, pos);
+ data = newData;
+ }
+ }
+ } finally {
+ stream.close();
+ }
+}
+```
+
+
+
+#### 获取原文件(getBaseFile)
+
+该方法用于获取原文件路径,但通常情况下并不推荐使用,因为获取到的原文件可能是损坏的或者无效的。
+
+```java
+public File getBaseFile() {
+ return mBaseName;
+}
+```
+
+
+
+#### 删除(delete)
+
+该方法会直接删除原文件和备份文件。
+
+```java
+public void delete() {
+ mBaseName.delete();
+ mBackupName.delete();
+}
+```
+
+
+
+## 简单示例
+
+
+
+## FAQ
+
+**Q:AtmoicFile 适用于哪些文件?**
+
+> A:**AtomicFile 适用一次性写入的文件**。根据 AtomicFile 原理可知,每一次获取写入文件的输出流的时候都会清空原文件的内容,所以是无法给文件追加内容的。
+
+**Q:AtmoicFile 是否是线程安全的?**
+
+> A:根据其原理就可知它没有带线程锁,所以**AtomicFile并不能保证线程安全**。
+
+**Q:文件写入完毕后可以直接关闭输出字节流(FileOutputStream)么?**
+
+> A:AtomicFile写入完毕后是不允许直接关闭字节流的,因为直接关闭字节流会导致备份文件没有删除,因而下次读取的时候会导致读取到的是原文件,而不是更新后的文件。
+> **写入完毕后应调用 finishWrite(正常写入完成) 或者 failWrite(写入失败)。**
+
+**Q:文件读取完毕后可以直接关闭输入字节流(FileInputStream)么?**
+
+> A:可以。
+
+
+
+## 结语
+
+AtomicFile 作为一个工具类,有其方便之处,同时也有一些需要注意的地方,另外,AtomicFile 除了上述方法之外,还有另外几个方法没有在本文中说明,这几个方法由于不安全,已经被标注删除和隐藏,故本文就不在赘述了。
+
+事实上 AtomicFile 逻辑及其简单,应用场景也非常有限,如果需要的话自己徒手写一个也是可以的,个人觉得它的作用并不太大,如果以后大家在源码或者其他地方见到了这个类,知道它是干什么的就行了。
+
+**最后,任何工具都是双刃剑,用好了伤人,用不好伤己,希望大家用之前好好了解一下其利弊,权衡之后再做决定。**
+
+
+
+## 参考资料
+
+[Guide - AtomicFile](https://developer.android.com/reference/android/support/v4/util/AtomicFile.html)
+[源码 - AtomicFile](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/util/AtomicFile.java)
+
+
+
+## About Me
+
+### 作者微博: @GcsSloop
+
+
\ No newline at end of file
From d757a4bddeae39da29cc2b985a5e5a348ebe241d Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Wed, 30 Nov 2016 20:52:29 +0800
Subject: [PATCH 059/160] Update
---
...20\347\240\201\350\247\243\346\236\220.md" | 19 ++++++++++---------
1 file changed, 10 insertions(+), 9 deletions(-)
diff --git "a/SourceAnalysis/AtomicFile\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/SourceAnalysis/AtomicFile\346\272\220\347\240\201\350\247\243\346\236\220.md"
index 9c703a4d..e0e30774 100644
--- "a/SourceAnalysis/AtomicFile\346\272\220\347\240\201\350\247\243\346\236\220.md"
+++ "b/SourceAnalysis/AtomicFile\346\272\220\347\240\201\350\247\243\346\236\220.md"
@@ -22,15 +22,16 @@ AtomicFile 在 `android.support.v4.util` 包下,**是一个与文件相关的
## 主要方法
-| ~ | AtomicFile(File baseName)
构造方法,通过File创建AtomicFile |
-| :--------------: | ---------------------------------------- |
-| FileOutputStream | startWrite()
准备写文件,获得一个向文件写入的字节流(FileOutputStream)。 |
-| void | finishWrite(FileOutputStream str)
写入完毕时调用。 |
-| void | failWrite(FileOutputStream str)
写入失败时调用。 |
-| FileInputStream | openRead()
准备读取文件,获得一个从文件读取的字节流(FileInputStream)。 |
-| byte[] | readFully()
将文件中所有内容读取出来用 byte 数组返回,相当于更方便的 openRead 方法。 |
-| File | getBaseFile()
获得原文件地址。 |
-| void | delete()
删除 AtomicFile,包括原文件和备份文件。 |
+| 返回值 | 方法名称和简介 |
+| :------------------: | ---------------------------------------- |
+| | **AtomicFile(File baseName)**
构造方法,通过File创建AtomicFile |
+| **FileOutputStream** | **startWrite()**
准备写文件,获得一个向文件写入的字节流(FileOutputStream)。 |
+| **void** | **finishWrite(FileOutputStream str)**
写入完毕时调用。 |
+| **void** | **failWrite(FileOutputStream str)**
写入失败时调用。 |
+| **FileInputStream** | **openRead()**
准备读取文件,获得一个从文件读取的字节流(FileInputStream)。 |
+| **byte[]** | **readFully()**
将文件中所有内容读取出来用 byte 数组返回,相当于更方便的 openRead 方法。 |
+| **File** | **getBaseFile()**
获得原文件地址。 |
+| **void** | **delete()**
删除 AtomicFile,包括原文件和备份文件。 |
## AtomicFile 原理
From 920942b15277573471a6a387a0473ef6113cde51 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Wed, 30 Nov 2016 20:58:35 +0800
Subject: [PATCH 060/160] Update
---
...icFile\346\272\220\347\240\201\350\247\243\346\236\220.md" | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git "a/SourceAnalysis/AtomicFile\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/SourceAnalysis/AtomicFile\346\272\220\347\240\201\350\247\243\346\236\220.md"
index e0e30774..6f6d8995 100644
--- "a/SourceAnalysis/AtomicFile\346\272\220\347\240\201\350\247\243\346\236\220.md"
+++ "b/SourceAnalysis/AtomicFile\346\272\220\347\240\201\350\247\243\346\236\220.md"
@@ -254,9 +254,9 @@ public void delete() {
## 结语
-AtomicFile 作为一个工具类,有其方便之处,同时也有一些需要注意的地方,另外,AtomicFile 除了上述方法之外,还有另外几个方法没有在本文中说明,这几个方法由于不安全,已经被标注删除和隐藏,故本文就不在赘述了。
+AtomicFile 作为一个工具类,有其方便之处,同时也有一些需要注意的地方,例如不能追加内容,另外,AtomicFile 除了上述方法之外,还有另外几个方法没有在本文中说明,这几个方法由于不安全,已经被标注删除和隐藏,故本文就不在赘述了。
-事实上 AtomicFile 逻辑及其简单,应用场景也非常有限,如果需要的话自己徒手写一个也是可以的,个人觉得它的作用并不太大,如果以后大家在源码或者其他地方见到了这个类,知道它是干什么的就行了。
+事实上 AtomicFile 逻辑非常简单,应用场景也有限,如果需要的话自己徒手写一个也是可以的,个人觉得它的作用并不太大,如果以后大家在源码或者其他地方见到了这个类,知道它是干什么的就行了。
**最后,任何工具都是双刃剑,用好了伤人,用不好伤己,希望大家用之前好好了解一下其利弊,权衡之后再做决定。**
From d9147bc5725192a628fe0f85052485a843c60119 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Wed, 30 Nov 2016 21:03:19 +0800
Subject: [PATCH 061/160] Update
---
.../AtomicFile.md | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename "SourceAnalysis/AtomicFile\346\272\220\347\240\201\350\247\243\346\236\220.md" => SourceAnalysis/AtomicFile.md (100%)
diff --git "a/SourceAnalysis/AtomicFile\346\272\220\347\240\201\350\247\243\346\236\220.md" b/SourceAnalysis/AtomicFile.md
similarity index 100%
rename from "SourceAnalysis/AtomicFile\346\272\220\347\240\201\350\247\243\346\236\220.md"
rename to SourceAnalysis/AtomicFile.md
From 23f3f0087ee420120103258bf81464efab2520e6 Mon Sep 17 00:00:00 2001
From: sloop
Date: Thu, 1 Dec 2016 15:07:45 +0800
Subject: [PATCH 062/160] Update
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index ad505ae6..93adc842 100644
--- a/README.md
+++ b/README.md
@@ -127,6 +127,7 @@
* 保持文章原文,不作修改。
* 明确署名,即至少注明 `作者:GcsSloop` 字样以及文章的原始链接,且不得使用 `rel="nofollow"` 标记。
* 商业用途请点击最下面图片联系本人。
+* 微信公众号转载一律不授权 原创 标志。
### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop)
From 48e2b94aa52fdfded8b373d3e27380fbd0cc2fc0 Mon Sep 17 00:00:00 2001
From: sloop
Date: Fri, 2 Dec 2016 19:07:48 +0800
Subject: [PATCH 063/160] Update
---
SourceAnalysis/README.md | 2 ++
1 file changed, 2 insertions(+)
create mode 100644 SourceAnalysis/README.md
diff --git a/SourceAnalysis/README.md b/SourceAnalysis/README.md
new file mode 100644
index 00000000..f1aaca6a
--- /dev/null
+++ b/SourceAnalysis/README.md
@@ -0,0 +1,2 @@
+# 源码解析
+
From 7d3e47f4250a77ced140c317019571c972b3166f Mon Sep 17 00:00:00 2001
From: sloop
Date: Sat, 3 Dec 2016 21:59:52 +0800
Subject: [PATCH 064/160] Update
---
SourceAnalysis/README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/SourceAnalysis/README.md b/SourceAnalysis/README.md
index f1aaca6a..6924a8e2 100644
--- a/SourceAnalysis/README.md
+++ b/SourceAnalysis/README.md
@@ -1,2 +1,3 @@
# 源码解析
+* [AtomicFile 源码解析](https://github.com/GcsSloop/AndroidNote/blob/master/SourceAnalysis/AtomicFile.md)
From ce39fd7785abd003f253b21d00fb44302de58694 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Wed, 7 Dec 2016 13:24:41 +0800
Subject: [PATCH 065/160] Update
---
Course/Markdown/markdown-editor.md | 124 +++++++++++++++++++++++++++++
1 file changed, 124 insertions(+)
create mode 100644 Course/Markdown/markdown-editor.md
diff --git a/Course/Markdown/markdown-editor.md b/Course/Markdown/markdown-editor.md
new file mode 100644
index 00000000..cba314c7
--- /dev/null
+++ b/Course/Markdown/markdown-editor.md
@@ -0,0 +1,124 @@
+# Markdown实用技巧-编辑器(Typora)
+
+本次的安利对象是一个 Markdown 编辑器,是会长[^1]见过的最简单,最优雅的编辑器,先来看一下它的界面吧:
+
+
+
+
+
+它的界面非常简单,有多种主题可选,更重要的是**它的预览界面和编辑界面是一体的,而不像其他编辑器那样是左右分开的。**
+
+
+
+从上面的示例中可以看出,其输入模式支持多种,不论是手动输入语法还是使用快捷键,都非常的流畅,实时看到效果变化,除此之外 Typora 还有很多优点。
+
+## Typora 的优点
+
+* 预览和编辑界面一体。
+* 强大的快捷键。
+* 兼容常见扩展语法。
+* 兼容 HTML (新版进行了完善)。
+* 支持 YAML 格式。
+* 支持公式编辑(LaTeX公式)。
+* 表格和代码块编辑起来非常方便。
+* 支持本地图片相对链接。
+* 支持将本地图片拖进编辑页面。
+* 支持多种导出格式(PDF,HTML,Word,LaTeX 等)。
+* 有多种主题可选。
+
+虽然上面的内容在其他编辑器上也可以见到,但做到这么完善的并不多,想要尝试的小伙伴赶紧来试试吧,相信你也会爱上它的。
+
+#### [点击这里进入 Typora 官网](http://www.typora.io/){:target="_blank"}
+
+既然 Typora 这么好,为什么不在一开始就推荐呢,这是因为在 Markdown 系列文章第一篇发布的时候,Typora 还有一些小瑕疵(HTML语法兼容部分)让会长不满意,所以只是放在了推荐首位,在最近更新了新版本之后,这一部分已经修复完善了,基本算是完美了,所以会长我才特地写一篇文章来推荐。
+
+**Typora 支持在 WIndows,Linux,和 Mac 上使用,如果你正在使用的是 Mac 的话,那么恭喜你,除了上述的优点,你可以用到一些 Mac 系统才有的福利。**
+
+## 版本回溯功能
+
+这是 Mac 系统提供的一个小功能,使用 Mac 的时候你可能会注意到一个小细节,**使用系统文本编辑器进行编辑文本的时候,从来不会提示你保存文本,即便编辑后不点保存立即退出里面的内容也不会丢失。**你可能会觉得这不就是一个自动保存功能么,有啥稀奇的?
+
+那么你是否遇到过这一种情况,编辑了半天的内容觉得不太好想放弃保存,继续使用之前的内容,这在Windows上很容易实现,只要编辑的时候没有手动保存,直接点击关闭不保存,再打开的时候就是编辑之前的内容。
+
+然而在 Mac 上自动保存了,如果关闭后再打开,点 `Command + Z` 也没用了,这不就尴尬了,难道说遇到这种情况只能关闭前狂点 `Command + Z` 回退?
+
+作为一个有情怀的操作系统,自然不会意识不到这个问题,它提供了更强大的方法,那就是版本回溯,相当于一个自动的 Git 系统,这就厉害了。
+
+Trpora 也用到了这一特性,点击 `File > Revert To > Browse All Versions` 就能看到了。以我之前发布的一篇文章为例:
+
+
+
+ **上图中左边为当前版本,右边为选中版本,最右侧是时间线,使用这一功能可以将文章回溯到任意时间点,妈妈再也不怕我的文章内容丢失啦,也成功避免了以下这种情况 ▼**
+
+
+
+由于这一功能是 Mac 系统提供的,所以不仅对纯文本有效,对 iWork 也是有效的,iWork 上面也有版本回溯功能,像 key,numbers,pages 都支持版本回溯,中文版点 `文件 > 复原 > 浏览所有版本` 即可看到过去保存的数据。
+
+当然了,这只是福利之一。
+
+## 更便捷的图片插入和上传方式
+
+众所周知,Markdown 插入图片是一个大问题,尤其是在本地编辑的时候,但是这些在遇到 Typora 后就变的越来越简单了。
+
+**对于本地图片,我们可以直接拖进来,再也不用记复杂的语法和查图片地址了,没错,只要拖进来就行,Typora 会自动识别图片并进行插入。**
+
+不过还有一个问题,我们的很多文章都是要发布的,直接拖进来这种方式在本地看起来没问题,但上传到网站上是读取不到本地图片的,通常解决方案就是先上传到图床上,之后再将链接插入到 Markdown 中,这样发布就没问题了。
+
+但是,当一篇文章需要插入很多图片的时候,一次次的上传,一次次的复制链接,一次次的粘贴,也是很麻烦的,虽然有一些脚本可以通过快捷键进行上传,但终归还是多了一些步骤,很不方便,而且对新手很不友好,更何况很多新手根本不知道图床所谓何物。
+
+这时候就要祭出 Mac 上另一个神器了: iPic [(点击此处进行下载)](https://itunes.apple.com/cn/app/ipic-tu-chuang-shen-qi-zhong/id1101244278?mt=12){:target="_blank"},它是一个专业图床管理工具,貌似出来还没多久,目前免费版只能上传到微博图床,专业版支持大部分图床,专业版一年 30RMB,如果经常使用的话,倒也不算贵。
+
+**话说不还是要用图床么?**
+
+**但 Typora 的方便之处就在于可以和 iPic 无缝结合,纯傻瓜式一键操作,能一次性的将文章中所有本地图片上传到图床上。**
+
+**首先安装 iPic,运行 iPic,之后点击 `Edit > Image Tools > Upload Local Images via iPic` 就像下面这样就行了。**
+
+QQ20161202-8](http://ww1.sinaimg.cn/large/006y8lVajw1facuf1yjjkj30fo0dmq4v.jpg)
+
+**如果你觉得这样还是很麻烦的话,还有另外的选项,可以选择插入本地图片的时候自动上传到服务器,当然了,为了保护个人隐私,这一个选项默认是关闭的,首先你要找到首选项,点击 `Typora > Preferences` 或者快捷键 `Command + ,` 都行,之后打开以下两个选项,其中一个选项是后面用到的。**
+
+
+
+**打开之后继续选择 `Edit > Image Tools > When Insert Local Images > Upload Image via iPic` 它的意思是当插入本地图片时自动通过 iPic 上传到服务器上。**
+
+
+
+这种方案虽然方便,但是会长并不推荐大家这么做,主要还是不安全,误操作的话可能会将一些包含个人隐私的照片上传上去。
+
+会长推荐另一种方案,就是 `Copy Image File to Folder`,操作步骤和上面类似,它的意思是插入本地图片时自动归档到某一个文件夹,在文章编辑完成后在点击上传按钮统一上传,这样有以下几点好处:
+
+* 安全,不会因为误操作将涉及隐私照片传到服务器上。
+* 相当于自动建立了一个本地图片档案,方便备份保存。
+* 如果图床上的图片丢失,可以从快速从本地备份中找到并恢复。
+
+
+关于 Typora 的推荐内容暂时就到这里结束啦,更多关于 Typora 的多详情请参阅 [Support](http://support.typora.io/){:target="_blank"}。关于 Markdown 的更多内容请参见之前的文章:
+
+[Markdown实用技巧-快速入门](https://www.gcssloop.com/markdown/markdown-start){:target="_blank"}
+[Markdown实用技巧-基础语法](https://www.gcssloop.com/markdown/markdown-grammar){:target="_blank"}
+[Markdown实用技巧-链接和图片](https://www.gcssloop.com/markdown/markdown-links){:target="_blank"}
+
+## 结语
+
+最后稍微夹带一点个人建议,个人觉得 Typora 除了界面简洁之外,最大的优点就是快捷键方便了,然而,快捷键那么多,要不要专一去记呢?
+
+个人的建议是不要专一去记忆这些快捷键,用到的时候打开菜单看一眼快捷键是什么,之后用快捷键按出来,这样用过几次之后自然就会记住常用的快捷键,而且会记得很牢固,不然每个应用都有快捷键,一个一个的记多麻烦。
+
+本文中涉及到的两个软件下载地址:
+
+[**Typora**](http://www.typora.io/){:target="_blank"}
+[**iPic**](https://itunes.apple.com/cn/app/id1101244278?ls=1&mt=12){:target="_blank"}
+
+**最后留一个课后作业,关注会长的 [新浪微博](http://weibo.com/GcsSloop){:target="_blank"} 。**
+
+## About Me
+
+### 作者微博: @GcsSloop
+
+
+
+[^1]: 会长,就是 GcsSloop 我啦,因为创建了一个名为魔法师协会的交流群,所以自称会长,想要加入魔法师协会的魔法师可以加我的微信 `GcsSloop`,备注加群或者私信我,我会邀请你加入魔法师的大家庭。
+
+
+
From 5e58e99bdac8cf739cfd886de2269e870f101acc Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Wed, 7 Dec 2016 13:27:03 +0800
Subject: [PATCH 066/160] Update
---
Course/Markdown/markdown-editor.md | 24 ++++++++++--------------
1 file changed, 10 insertions(+), 14 deletions(-)
diff --git a/Course/Markdown/markdown-editor.md b/Course/Markdown/markdown-editor.md
index cba314c7..85eb6aae 100644
--- a/Course/Markdown/markdown-editor.md
+++ b/Course/Markdown/markdown-editor.md
@@ -1,6 +1,6 @@
# Markdown实用技巧-编辑器(Typora)
-本次的安利对象是一个 Markdown 编辑器,是会长[^1]见过的最简单,最优雅的编辑器,先来看一下它的界面吧:
+本次的安利对象是一个 Markdown 编辑器,是会长见过的最简单,最优雅的编辑器,先来看一下它的界面吧:

@@ -28,7 +28,7 @@
虽然上面的内容在其他编辑器上也可以见到,但做到这么完善的并不多,想要尝试的小伙伴赶紧来试试吧,相信你也会爱上它的。
-#### [点击这里进入 Typora 官网](http://www.typora.io/){:target="_blank"}
+#### [点击这里进入 Typora 官网](http://www.typora.io/)
既然 Typora 这么好,为什么不在一开始就推荐呢,这是因为在 Markdown 系列文章第一篇发布的时候,Typora 还有一些小瑕疵(HTML语法兼容部分)让会长不满意,所以只是放在了推荐首位,在最近更新了新版本之后,这一部分已经修复完善了,基本算是完美了,所以会长我才特地写一篇文章来推荐。
@@ -66,7 +66,7 @@ Trpora 也用到了这一特性,点击 `File > Revert To > Browse All Versions
但是,当一篇文章需要插入很多图片的时候,一次次的上传,一次次的复制链接,一次次的粘贴,也是很麻烦的,虽然有一些脚本可以通过快捷键进行上传,但终归还是多了一些步骤,很不方便,而且对新手很不友好,更何况很多新手根本不知道图床所谓何物。
-这时候就要祭出 Mac 上另一个神器了: iPic [(点击此处进行下载)](https://itunes.apple.com/cn/app/ipic-tu-chuang-shen-qi-zhong/id1101244278?mt=12){:target="_blank"},它是一个专业图床管理工具,貌似出来还没多久,目前免费版只能上传到微博图床,专业版支持大部分图床,专业版一年 30RMB,如果经常使用的话,倒也不算贵。
+这时候就要祭出 Mac 上另一个神器了: iPic [(点击此处进行下载)](https://itunes.apple.com/cn/app/ipic-tu-chuang-shen-qi-zhong/id1101244278?mt=12),它是一个专业图床管理工具,貌似出来还没多久,目前免费版只能上传到微博图床,专业版支持大部分图床,专业版一年 30RMB,如果经常使用的话,倒也不算贵。
**话说不还是要用图床么?**
@@ -93,11 +93,11 @@ Trpora 也用到了这一特性,点击 `File > Revert To > Browse All Versions
* 如果图床上的图片丢失,可以从快速从本地备份中找到并恢复。
-关于 Typora 的推荐内容暂时就到这里结束啦,更多关于 Typora 的多详情请参阅 [Support](http://support.typora.io/){:target="_blank"}。关于 Markdown 的更多内容请参见之前的文章:
+关于 Typora 的推荐内容暂时就到这里结束啦,更多关于 Typora 的多详情请参阅 [Support](http://support.typora.io/)。关于 Markdown 的更多内容请参见之前的文章:
-[Markdown实用技巧-快速入门](https://www.gcssloop.com/markdown/markdown-start){:target="_blank"}
-[Markdown实用技巧-基础语法](https://www.gcssloop.com/markdown/markdown-grammar){:target="_blank"}
-[Markdown实用技巧-链接和图片](https://www.gcssloop.com/markdown/markdown-links){:target="_blank"}
+[Markdown实用技巧-快速入门](https://www.gcssloop.com/markdown/markdown-start)
+[Markdown实用技巧-基础语法](https://www.gcssloop.com/markdown/markdown-grammar)
+[Markdown实用技巧-链接和图片](https://www.gcssloop.com/markdown/markdown-links)
## 结语
@@ -107,10 +107,10 @@ Trpora 也用到了这一特性,点击 `File > Revert To > Browse All Versions
本文中涉及到的两个软件下载地址:
-[**Typora**](http://www.typora.io/){:target="_blank"}
-[**iPic**](https://itunes.apple.com/cn/app/id1101244278?ls=1&mt=12){:target="_blank"}
+[**Typora**](http://www.typora.io/)
+[**iPic**](https://itunes.apple.com/cn/app/id1101244278?ls=1&mt=12)
-**最后留一个课后作业,关注会长的 [新浪微博](http://weibo.com/GcsSloop){:target="_blank"} 。**
+**最后留一个课后作业,关注会长的 [新浪微博](http://weibo.com/GcsSloop) 。**
## About Me
@@ -118,7 +118,3 @@ Trpora 也用到了这一特性,点击 `File > Revert To > Browse All Versions
-[^1]: 会长,就是 GcsSloop 我啦,因为创建了一个名为魔法师协会的交流群,所以自称会长,想要加入魔法师协会的魔法师可以加我的微信 `GcsSloop`,备注加群或者私信我,我会邀请你加入魔法师的大家庭。
-
-
-
From b3c56ef2ca3857797f49a4ecbf078588d4199a36 Mon Sep 17 00:00:00 2001
From: sloop
Date: Thu, 8 Dec 2016 23:09:23 +0800
Subject: [PATCH 067/160] Update
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index 93adc842..37284147 100644
--- a/README.md
+++ b/README.md
@@ -55,6 +55,7 @@
* [Markdown 快速入门](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-start.md)
* [Markdown 基础语法](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-grammar.md)
* [Markdown 链接图片](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-link.md)
+* [Markdown 编辑器](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-editor.md)
******
From 03bcb466b7c5cacbe7a15ded5d675dd014880fd5 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Sat, 10 Dec 2016 23:27:20 +0800
Subject: [PATCH 068/160] Update
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 37284147..6e1eef7f 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
新开的博客,在博客中可以获得更好的阅读体验,同时在博客的评论区可以更及时的向我反馈文章中的问题。
-
+
******
@@ -132,6 +132,6 @@
### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop)
-
+
[▲ 回到顶部](#top)
From 087d1dcaa72b6452683d1b77eb7157a027ef9ce8 Mon Sep 17 00:00:00 2001
From: sloop
Date: Sun, 11 Dec 2016 23:55:15 +0800
Subject: [PATCH 069/160] Update
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 6e1eef7f..739e3537 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop)
-我的安卓学习笔记,记录学习过程中遇到的问题,以及我的一些经验总结。如果出现链接失效等情况可以提交Issues提醒我修改相关内容。
+我的安卓学习笔记,记录学习过程中遇到的问题,以及我的一些经验总结。如果出现链接失效等情况可以提交 Issues 提醒我修改相关内容。
> #### PS:点击分类标题可以查看该分类的详细信息。
From b0ce08cf98e34b1701fed48b2ed2132a3e61de4b Mon Sep 17 00:00:00 2001
From: sloop
Date: Mon, 12 Dec 2016 18:05:49 +0800
Subject: [PATCH 070/160] Update
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 739e3537..73d11e95 100644
--- a/README.md
+++ b/README.md
@@ -128,7 +128,7 @@
* 保持文章原文,不作修改。
* 明确署名,即至少注明 `作者:GcsSloop` 字样以及文章的原始链接,且不得使用 `rel="nofollow"` 标记。
* 商业用途请点击最下面图片联系本人。
-* 微信公众号转载一律不授权 原创 标志。
+* 微信公众号转载一律不授权 `原创` 标志。
### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop)
From e1adb04a03b5aec63bd268ecac1be4e95a382869 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Tue, 13 Dec 2016 23:12:58 +0800
Subject: [PATCH 071/160] Update
---
CustomView/Advance/[18]multi-touch.md | 2 ++
1 file changed, 2 insertions(+)
create mode 100644 CustomView/Advance/[18]multi-touch.md
diff --git a/CustomView/Advance/[18]multi-touch.md b/CustomView/Advance/[18]multi-touch.md
new file mode 100644
index 00000000..a72ff088
--- /dev/null
+++ b/CustomView/Advance/[18]multi-touch.md
@@ -0,0 +1,2 @@
+# 多点触控
+
From 9a9be4f9cd3a5d0b7e0ffb621a5f10da836e1180 Mon Sep 17 00:00:00 2001
From: sloop
Date: Wed, 14 Dec 2016 23:35:54 +0800
Subject: [PATCH 072/160] Update
---
Course/Markdown/README.md | 1 +
1 file changed, 1 insertion(+)
create mode 100644 Course/Markdown/README.md
diff --git a/Course/Markdown/README.md b/Course/Markdown/README.md
new file mode 100644
index 00000000..dd66a5c7
--- /dev/null
+++ b/Course/Markdown/README.md
@@ -0,0 +1 @@
+# Markdown 实用技巧
From aef35551b2c0c0acfcc63c491caa41b7536c0141 Mon Sep 17 00:00:00 2001
From: sloop
Date: Fri, 16 Dec 2016 23:39:17 +0800
Subject: [PATCH 073/160] Update
---
Course/Markdown/README.md | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/Course/Markdown/README.md b/Course/Markdown/README.md
index dd66a5c7..a35826e3 100644
--- a/Course/Markdown/README.md
+++ b/Course/Markdown/README.md
@@ -1 +1,6 @@
# Markdown 实用技巧
+
+* [Markdown 快速入门](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-start.md)
+* [Markdown 基础语法](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-grammar.md)
+* [Markdown 链接图片](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-link.md)
+* [Markdown 编辑器](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-editor.md)
From 90db239a7f90812c9254588eaf0fba68ac099eaf Mon Sep 17 00:00:00 2001
From: sloop
Date: Sat, 17 Dec 2016 23:14:14 +0800
Subject: [PATCH 074/160] Update
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 73d11e95..501b0674 100644
--- a/README.md
+++ b/README.md
@@ -50,7 +50,7 @@
******
-## Markdown
+## [Markdown](https://github.com/GcsSloop/AndroidNote/tree/master/Course/Markdown)
* [Markdown 快速入门](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-start.md)
* [Markdown 基础语法](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-grammar.md)
From 014c84d84402586e517b8735e91cd96c70d09c45 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Tue, 20 Dec 2016 04:23:06 +0800
Subject: [PATCH 075/160] Update
---
CustomView/Advance/[09]Matrix_Basic.md | 766 +++++++++++++++----------
1 file changed, 459 insertions(+), 307 deletions(-)
diff --git a/CustomView/Advance/[09]Matrix_Basic.md b/CustomView/Advance/[09]Matrix_Basic.md
index b4f4b346..96068632 100644
--- a/CustomView/Advance/[09]Matrix_Basic.md
+++ b/CustomView/Advance/[09]Matrix_Basic.md
@@ -1,51 +1,29 @@
-# Matrix原理
+# Matrix 原理
-### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop)
-### [【本系列相关文章】](https://github.com/GcsSloop/AndroidNote/tree/master/CustomView/README.md)
-
-
-## 目录
-
-- [前言](#qianyan)
-- [Matrix简介](#jianjie)
-- [Matrix基本原理](#jiben)
-- [Matrix复合原理](#fuhe)
-- [Matrix方法表](#fangfa)
-- [总结](#zongjie)
-- [关于作者](#about)
-- [参考资料](#ziliao)
-
-
-## 前言
-
-本文内容偏向理论,和 [画布操作](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B03%5DCanvas_Convert.md) 有重叠的部分,本文会让你更加深入的了解其中的原理。
+本文内容偏向理论,和 [画布操作](http://www.gcssloop.com/customview/Canvas_Convert/) 有重叠的部分,本文会让你更加深入的了解其中的原理。
本篇的主角Matrix,是一个一直在后台默默工作的劳动模范,虽然我们所有看到View背后都有着Matrix的功劳,但我们却很少见到它,本篇我们就看看它是何方神圣吧。
->
->由于Google已经对这一部分已经做了很好的封装,所以跳过本部分对实际开发影响并不会太大,不想深究的粗略浏览即可,下一篇中将会详细讲解Matrix的具体用法和技巧。
+> 由于Google已经对这一部分已经做了很好的封装,所以跳过本部分对实际开发影响并不会太大,不想深究的粗略浏览即可,下一篇中将会详细讲解Matrix的具体用法和技巧。
-******
+> ## ⚠️ 警告:测试本文章示例之前请关闭硬件加速。
-
## Matrix简介
-
**Matrix是一个矩阵,主要功能是坐标映射,数值转换。**
它看起来大概是下面这样:
-
**Matrix作用就是坐标映射,那么为什么需要Matrix呢? 举一个简单的例子:**
@@ -55,7 +33,7 @@ $$)
以下图为例,我们的内容区和屏幕坐标系还相差一个通知栏加一个标题栏的距离,所以两者是不重合的,我们在内容区的坐标系中的内容最终绘制的时候肯定要转换为实际的物理坐标系来绘制,Matrix在此处的作用就是转换这些数值。
>
-假设通知栏高度为20像素,导航栏高度为40像素,那么我们在内容区的(0,0)位置绘制一个点,最终就要转化为在实际坐标系中的(0,60)位置绘制一个点。
+>假设通知栏高度为20像素,导航栏高度为40像素,那么我们在内容区的(0,0)位置绘制一个点,最终就要转化为在实际坐标系中的(0,60)位置绘制一个点。

@@ -66,12 +44,14 @@ $$)
### Matrix特点
* 作用范围更广,Matrix在View,图片,动画效果等各个方面均有运用,相比与之前讲解等画布操作应用范围更广。
+
* 更加灵活,画布操作是对Matrix的封装,Matrix作为更接近底层的东西,必然要比画布操作更加灵活。
+
* 封装很好,Matrix本身对各个方法就做了很好的封装,让开发者可以很方便的操作Matrix。
-*
+
* 难以深入理解,很难理解中各个数值的意义,以及操作规律,如果不了解矩阵,也很难理解前乘,后乘。
-
+
### 常见误解
**1.认为Matrix最下面的一行的三个参数(MPERSP_0、MPERSP_1、MPERSP_2)没有什么太大的作用,在这里只是为了凑数。**
@@ -82,9 +62,7 @@ $$)
的确,更改MPERSP_2的值能够达到类似缩放的效果,但这是因为齐次坐标的缘故,并非这个参数的实际功能。
-******
-
## Matrix基本原理
Matrix 是一个矩阵,最根本的作用就是坐标转换,下面我们就看看几种常见变换的原理:
@@ -105,37 +83,36 @@ Matrix 是一个矩阵,最根本的作用就是坐标转换,下面我们就
### 1.缩放(Scale)
-
+
-
+
用矩阵表示:
-
>
@@ -150,6 +127,7 @@ $$)

+
### 2.错切(Skew)
错切存在两种特殊错切,水平错切(平行X轴)和垂直错切(平行Y轴)。
@@ -162,30 +140,29 @@ $$)
用矩阵表示:
-
图例:
@@ -200,30 +177,29 @@ $$)
用矩阵表示:
-
图例:
@@ -240,30 +216,29 @@ $$)
用矩阵表示:
-
图例:
@@ -274,51 +249,48 @@ $$)
假定一个点 A(x0, y0) ,距离原点距离为 r, 与水平轴夹角为 α 度, 绕原点旋转 θ 度, 旋转后为点 B(x, y) 如下:
-
+
-
+
-
-= r \\cdot cos \\alpha \\cdot cos \\theta - r \\cdot sin \\alpha \\cdot sin \\theta
-= x_0 \\cdot cos \\theta - y_0 \\cdot sin \\theta
+
+= r \cdot cos \alpha \cdot cos \theta - r \cdot sin \alpha \cdot sin \theta
+= x_0 \cdot cos \theta - y_0 \cdot sin \theta
$$)
-
-= r \\cdot sin \\alpha \\cdot cos \\theta + r \\cdot cos \\alpha \\cdot sin \\theta
-= y_0 \\cdot cos \\theta + x_0 \\cdot sin \\theta
+
+= r \cdot sin \alpha \cdot cos \theta + r \cdot cos \alpha \cdot sin \theta
+= y_0 \cdot cos \theta + x_0 \cdot sin \theta
$$)
用矩阵表示:
- & -sin(\\theta) & 0 \\\\
-sin(\\theta) & cos(\\theta) & 0 \\\\
+\left [
+\begin{matrix}
+cos(\theta) & -sin(\theta) & 0 \\
+sin(\theta) & cos(\theta) & 0 \\
0 & 0 & 1
-\\end{1}
-\\right ]
+\end{1}
+\right ]
.
-\\left [
-\\begin{matrix}
-x_0\\\\
-y_0\\\\
+\left [
+\begin{matrix}
+x_0\\
+y_0\\
1
-\\end{1}
-\\right ]
+\end{1}
+\right ]
$$)
图例:
@@ -331,37 +303,36 @@ $$)
>
> 此处也是使用齐次坐标的优点体现之一,实际上前面的三个操作使用 2x2 的矩阵也能满足需求,但是使用 2x2 的矩阵,无法将平移操作加入其中,而将坐标扩展为齐次坐标后,将矩阵扩展为 3x3 就可以将算法统一,四种算法均可以使用矩阵乘法完成。
-
+
-
+
用矩阵表示:
-
图例:
@@ -369,7 +340,6 @@ $$)

-
## Matrix复合原理
其实Matrix的多种复合操作都是使用矩阵乘法实现的,从原理上理解很简单,但是,使用矩阵乘法也有其弱点,后面的操作可能会影响到前面到操作,所以在构造Matrix时顺序很重要。
@@ -379,14 +349,16 @@ $$)
### 前乘(pre)
前乘相当于矩阵的右乘:
-
+
+
> 这表示一个矩阵与一个特殊矩阵前乘后构造出结果矩阵。
### 后乘(post)
前乘相当于矩阵的左乘:
-
+
+
> 这表示一个矩阵与一个特殊矩阵后乘后构造出结果矩阵。
@@ -396,63 +368,245 @@ $$)
## 组合
-我们使用Matrix最终目的就是让视图显示为我们想要的状态,为此我们可能需要多种操作结合使用。
+**关于 Matrix 的文章终有一个问题,就是 pre 和 post 这一部分的理论非常别扭,国内大多数文章都是这样的,看起来貌似是对的但很难理解,部分内容违背直觉。**
-我发现很多讲解Matrix的文章喜欢用绕某一个点缩放(旋转)的示例来讲解,如下:
+**我由于也受到了这些文章的影响,自然而然的继承了这一理论,直到在评论区有一位小伙伴提出了一个问题,才让我重新审视了这一部分的内容,并进行了一定反思。**
+我思考的主要有以下部分:
+1. pre 和 post 的具体含义和作用。
+2. 矩阵构造过程中的正确影响顺序。
+3. 误解产生的具体源头。
+
+经过良久的思考之后,我决定推翻之前的理论和结论,重新构建了这一部分的理论知识,也许仍有疏漏,如有发现请指正。
+
+**首先澄清两个错误结论,记住,是错误结论,错误结论,错误结论。**
+
+### ~~错误结论一:pre 是顺序执行,post 是逆序执行。~~
+
+这个结论很具有迷惑性,因为这个结论并非是完全错误的,你很容易就能证明这个结论,例如下面这样:
+
+```java
+// 第一段 pre 顺序执行,先平移(T)后旋转(R)
+Matrix matrix = new Matrix();
+matrix.preTranslate(pivotX,pivotY);
+matrix.preRotate(angle);
+Log.e("Matrix", matrix.toShortString());
+
+// 第二段 post 逆序执行,先平移(T)后旋转(R)
+Matrix matrix = new Matrix();
+matrix.postRotate(angle);
+matrix.postTranslate(pivotX,pivotY)
+Log.e("Matrix", matrix.toShortString());
+```
+
+**这两段代码最终结果是等价的,于是轻松证得这个结论的正确性,但事实真是这样么?**
+
+首先,从数学角度分析,pre 和 post 就是右乘或者左乘的区别,其次,它们不可能实际影响运算顺序(程序执行顺序)。以上这两段代码等价也仅仅是因为最终化简公式一样而已。
+
+> 设原始矩阵为 M,平移为 T ,旋转为 R ,单位矩阵为 I ,最终结果为 M'
>
- 那么我们如果想让它基于图片中心缩放,应该该怎么办?要用到组合变换,
- 1)先将图片由中心平移到原点,这是应用变换 T
- 2)对图应用缩放变换 S
- 3)再将图片平移回到中心,应用变换 -T
-
->
- 对应代码:
- matrix.postScale(0.5f, 0.5f);
- matrix.preTranslate(-pivotX, -pivotY);
- matrix.postTranslate(pivotX, pivotY);
->
- PS: 此段文字引用自其它文章。
+> * 矩阵乘法不满足交换律,即 A\*B ≠ B\*A
+> * 矩阵乘法满足结合律,即 (A\*B)\*C = A\*(B\*C)
+> * 矩阵与单位矩阵相乘结果不变,即 A * I = A
-首先,**这个思路是没有任何问题的,也是实现绕某一点操作的核心原理**,但这可能会对一部分小白造成误解,认为只能这样实现,然而查看一下Matrix的方法表就能知道四大操作都可以指定中心点,所以,上面的三行代码用一行就能完成:
+```
+由于上面例子中原始矩阵(M)是一个单位矩阵(I),所以可得:
+
+// 第一段 pre
+M' = (M*T)*R = I*T*R = T*R
+
+// 第二段 post
+M' = T*(R*M) = T*R*I = T*R
+```
+
+由于两者最终的化简公式是相同的,所以两者是等价的,但是,这结论不具备普适性。
+
+**即原始矩阵不为单位矩阵的时候,两者无法化简为相同的公式,结果自然也会不同。另外,执行顺序就是程序书写顺序,不存在所谓的正序逆序。**
+
+### ~~错误结论二:pre 是先执行,而 post 是后执行。~~
+
+这一条结论比上一条更加扯淡,同样是错误的,而且错的更离谱。
+
+之所以产生这个错误完全是因为写文章的人懂英语。
+
+```
+pre :先,和 before 相似。
+post :后,和 after 相似。
+```
+
+所以就得出了 pre 先执行,而 post 后执行这一说法,但从严谨的数学和程序角度来分析,完全是扯淡的,还是上面所说的,**pre 和 post 不能影响程序执行顺序,而程序每执行一条语句都会得出一个确定的结果,所以,它根本不能控制先后执行,属于完全扯淡型。**
+
+**如果非要用这套理论强行解释的话,反而看起来像是 post 先执行,例如:**
```java
-matrix.postScale(0.5f, 0.5f, pivotX, pivotY);
+matrix.preRotate(angle);
+matrix.postTranslate(pivotX,pivotY);
```
-**组合操作构造Matrix时,个人建议尽量全部使用后乘或者全部使用前乘,这样操作顺序容易确定,出现问题也比较容易排查。
当然,由于矩阵乘法不满足交换律,前乘和后乘的结果是不同的,使用时应结合具体情景分析使用。**
+同样化简公式:
+
+```
+// 矩阵乘法满足结合律
+M‘ = T*(M*R) = T*M*R = (T*M)*R
+```
+
+从实际上来说,由于矩阵乘法满足结合律,所以不论你说是靠右先执行还是靠左先执行,从结果上来说都没有错。
+
+**之前基于这条错误的结论我进行了一次错误的证明:**
-### Pre与Post的区别
+> **(这段内容注定要成为我写作历程中不可抹灭的耻辱,既然是公开文章,就应该对读者负责,虽然我在发表每一篇文章之前都竭力的求证其中的问题,各种细节,避免出现这种错误,但终究还是留下了这样一段内容,在此我诚挚的向我所有的读者道歉,并且我会尽力的修复在其他平台上遗留的错误副本。)**
+>
+> 关注我的读者请尽量看我在 [个人博客](http://www.gcssloop.com/#blog) 和 [GitHub](https://github.com/GcsSloop/AndroidNote/blob/master/README.md) 发布的版本,这两个平台都在博文修复计划之内,有任何错误或者纰漏,都会首先修复这两个平台的文章。另外,所有进行修复过的文章都会在我的微博 [@GcsSloop](http://weibo.com/GcsSloop) 重新发布说明,关注我的微博可以第一时间得到博文更新或者修复的消息。
+>
+> ----
+>
+> ## 以下是错误证明:
+>
+> 在实际操作中,我们每一步操作都会得出准确的计算结果,但是为什么还会用存在先后的说法? 难道真的能够用pre和post影响计算顺序? 实则不然,下面我们用一个例子说明:
+>
+> ```java
+> Matrix matrix = new Matrix();
+> matrix.postScale(0.5f, 0.8f);
+> matrix.preTranslate(1000, 1000);
+> Log.e(TAG, "MatrixTest" + matrix.toShortString());
+> ```
+>
+> 在上面的操作中,如果按照正常的思路,先缩放,后平移,缩放操作执行在前,不会影响到后续的平移操作,但是执行结果却发现平移距离变成了(500, 800)。
+>
+> 在上面例子中,计算顺序是没有问题的,先计算的缩放,然后计算的平移,而缩放影响到平移则是因为前一步缩放后的结果矩阵右乘了平移矩阵,这是符合矩阵乘法的运算规律的,也就是说缩放操作虽然在前却影响到了平移操作,**相当于先执行了平移操作,然后执行的缩放操作,因此才有pre操作会先执行,而post操作会后执行这一说法**。
+>
+> ----
-主要区别其实就是矩阵的乘法顺序不同,pre相当于矩阵的右乘,而post相当于矩阵的左乘。
+在上面例子中犯了一个非常隐蔽的错误,这个错误基于一条基本的理论,**到底是前面操作状态会影响后面操作状态,还是后面操作状态会影响前面操作状态?**
-以下观点存在歧义,故做删除标注:
+在上面的错误论证中使用的是,后续操作状态会影响前面操作状态,但经过我实际测试情况来说应该是,**前面操作状态会影响到后续操作状态。** 即:
-
-在图像处理中,越靠近右边的矩阵越先执行,所以pre操作会先执行,而post操作会后执行。
-
+```
+操作A
+操作B
+```
-在实际操作中,我们每一步操作都会得出准确的计算结果,但是为什么还会用存在先后的说法? 难道真的能够用pre和post影响计算顺序? 实则不然,下面我们用一个例子说明:
+那么 操作A 不应当受到 操作B 的影响,而 操作A 执行后的状态会影响到 操作B 的执行状态。
->
```java
Matrix matrix = new Matrix();
matrix.postScale(0.5f, 0.8f);
matrix.preTranslate(1000, 1000);
-Log.e(TAG, "MatrixTest:3" + matrix.toShortString());
+Log.e(TAG, "MatrixTest" + matrix.toShortString());
```
->
-在上面的操作中,如果按照正常的思路,先缩放,后平移,缩放操作执行在前,不会影响到后续的平移操作,但是执行结果却发现平移距离变成了(500, 800)。
-> 在上面例子中,计算顺序是没有问题的,先计算的缩放,然后计算的平移,而缩放影响到平移则是因为前一步缩放后的结果矩阵右乘了平移矩阵,这是符合矩阵乘法的运算规律的,也就是说缩放操作虽然在前却影响到了平移操作,**相当于先执行了平移操作,然后执行的缩放操作,因此才有pre操作会先执行,而post操作会后执行这一说法**。
+在这个例子中,先执行的是缩放(Scale),而并非平移(Translate),即便将 postScale 换为 preScale,执行的结果也不会有变化,即下面的示例和上面是等价的:
+
+```java
+Matrix matrix = new Matrix();
+matrix.preScale(0.5f, 0.8f);
+matrix.preTranslate(1000, 1000);
+Log.e(TAG, "MatrixTest" + matrix.toShortString());
+```
+
+之所以平移距离是 MTRANS\_X = 500,MTRANS\_Y = 800,那是因为之前执行了缩放操作。
+Matrix 只是起到一种坐标转换的作用,并不能实际改变坐标系,所以说在缩放状态下执行平移操作,平移的距离也会被缩放,这样在绘制的时候才看起来像是缩放后的。
+
+## 如何理解和使用 pre 和 post ?
+
+理解非常简单,不要去管什么先后论,顺序论,就按照最基本的矩阵乘法理解。
+
+```
+pre : 右乘, M‘ = M*A
+post : 左乘, M’ = A*M
+```
+
+**那么如何使用?**
+
+正确使用方式就是先构造正常的 Matrix 乘法顺序,之后根据情况使用 pre 和 post 来把这个顺序实现。
+
+还是用一个最简单的例子理解,假设需要围绕某一点旋转。
+
+可以用这个方法 `xxxRotate(angle, pivotX, pivotY)` ,由于我们这里需要组合构造一个 Matrix,所以不直接使用这个方法。
+
+首先,有两条基本定理:
+
+* 所有的操作(旋转、平移、缩放、错切)默认都是以坐标原点为基准点的。
+
+* 之前操作的坐标系状态会保留,并且影响到后续状态。
+
+
+基于这两条基本定理,我们可以推算出要基于某一个点进行旋转需要如下步骤:
+
+```
+1. 先将坐标系原点移动到指定位置,使用平移 T
+2. 对坐标系进行旋转,使用旋转 S (围绕原点旋转)
+3. 再将坐标系平移回原来位置,使用平移 -T
+```
+
+具体公式如下:
+
+> M 为原始矩阵,是一个单位矩阵, M‘ 为结果矩阵, T 为平移, R为旋转
+
+```
+M' = M*T*R*-T = T*R*-T
+```
+
+按照公式写出来的伪代码如下:
+
+```java
+Matrix matrix = new Matrix();
+matrix.preTranslate(pivotX,pivotY);
+matrix.preRotate(angle);
+matrix.preTranslate(-pivotX, -pivotY);
+```
+
+
+
+
+
+围绕某一点操作可以拓展为通用情况,即:
+
+```java
+Matrix matrix = new Matrix();
+matrix.preTranslate(pivotX,pivotY);
+// 各种操作,旋转,缩放,错切等,可以执行多次。
+matrix.preTranslate(-pivotX, -pivotY);
+```
+
+公式为:
+
+```
+M' = M*T* ... *-T = T* ... *-T
+```
+
+但是这种方式,两个调整中心的平移函数就拉的太开了,所以通常采用这种写法:
+
+```java
+Matrix matrix = new Matrix();
+// 各种操作,旋转,缩放,错切等,可以执行多次。
+matrix.postTranslate(pivotX,pivotY);
+matrix.preTranslate(-pivotX, -pivotY);
+```
+
+这样公式为:
+
+```
+M' = T*M* ... *-T = T* ... *-T
+```
+
+可以看到最终化简结果是相同的。
+
+所以说,pre 和 post 就是用来调整乘法顺序的,正常情况下应当正向进行构建出乘法顺序公式,之后根据实际情况调整书写即可。
+
+**在构造 Matrix 时,个人建议尽量使用一种乘法,前乘或者后乘,这样操作顺序容易确定,出现问题也比较容易排查。**
+**当然,由于矩阵乘法不满足交换律,前乘和后乘的结果是不同的,使用时应结合具体情景分析使用。**
+
+
### 下面我们用不同对方式来构造一个矩阵:
-**假设我们需要先缩放再平移。**
+**假设我们需要先平移,再缩放,即平移不会受到缩放的影响。**
注意:
+
* 1.由于矩阵乘法不满足交换律,请保证使用初始矩阵(Initial Matrix),否则可能导致运算结果不同。
* 2.注意构造顺序,顺序是会影响结果的。
* 3.Initial Matrix是指new出来的新矩阵,或者reset后的矩阵,是一个单位矩阵。
@@ -461,98 +615,99 @@ Log.e(TAG, "MatrixTest:3" + matrix.toShortString());
#### 1.仅用pre:
``` java
+// 使用pre, M' = M*T*S = T*S
Matrix m = new Matrix();
m.reset();
-m.preTranslate(tx, ty); //使用pre,越靠后越先执行。
+m.preTranslate(tx, ty);
m.preScale(sx, sy);
```
用矩阵表示:
-
#### 2.仅用post:
``` java
+// 使用post, M‘ = T*S*M = T*S
Matrix m = new Matrix();
m.reset();
-m.postScale(sx, sy); //使用post,越靠前越先执行。
+m.postScale(sx, sy); //,越靠前越先执行。
m.postTranslate(tx, ty);
```
用矩阵表示:
-
#### 3.混合:
``` java
+// 混合 M‘ = T*M*S = T*S
Matrix m = new Matrix();
m.reset();
m.preScale(sx, sy);
@@ -562,6 +717,7 @@ m.postTranslate(tx, ty);
或:
``` java
+// 混合 M‘ = T*M*S = T*S
Matrix m = new Matrix();
m.reset();
m.postTranslate(tx, ty);
@@ -572,46 +728,45 @@ m.preScale(sx, sy);
用矩阵表示:
-
+**注意: 由于矩阵乘法不满足交换律,请保证初始矩阵为单位矩阵,如果初始矩阵不为单位矩阵,则导致运算结果不同。**
-**注意: 由于矩阵乘法不满足交换律,请保证初始矩阵为空,如果初始矩阵不为空,则导致运算结果不同。**
+上面虽然用了很多不同的写法,但最终的化简公式是一样的,这些不同的写法,都是根据同一个公式反向推算出来的。
-
## Matrix方法表
这个方法表,暂时放到这里让大家看看,方法的使用讲解放在下一篇文章中。
@@ -627,22 +782,20 @@ $$)
| 特殊方法 | setPolyToPoly setRectToRect rectStaysRect setSinCos | 一些特殊操作 |
| 矩阵相关 | invert isAffine isIdentity | 求逆矩阵、 是否为仿射矩阵、 是否为单位矩阵 ... |
-
## 总结
对于Matrix重在理解,理解了其中的原理之后用起来将会更加得心应手。
-**学完了本篇之后,推荐配合鸿洋大大的视频课程 [
-打造个性的图片预览与多点触控](http://www.imooc.com/learn/239) 食用,定然能够让你对Matrix对理解更上一层楼。**
+学完了本篇之后,推荐配合鸿洋大大的视频课程 [
+打造个性的图片预览与多点触控](http://www.imooc.com/learn/239) 食用,定然能够让你对Matrix对理解更上一层楼。
+
+## About
-
-## About Me
+[本系列相关文章](http://www.gcssloop.com/customview/CustomViewIndex/)
-### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop)
+作者微博: [GcsSloop](http://weibo.com/GcsSloop)
-
-
## 参考资料
[Matrix](https://developer.android.com/reference/android/graphics/Matrix.html)
@@ -653,4 +806,3 @@ $$)
[维基百科-线性映射](https://zh.wikipedia.org/wiki/%E7%BA%BF%E6%80%A7%E6%98%A0%E5%B0%84)
[齐次坐标系入门级思考](https://oncemore2020.github.io/blog/homogeneous/)
[仿射变换与齐次坐标](https://guangchun.wordpress.com/2011/10/12/affineandhomogeneous/)
-[]()
From 5dc140f3f9d27032eb56c9dea8dd153f1f19872c Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Tue, 20 Dec 2016 06:04:12 +0800
Subject: [PATCH 076/160] Update
---
CustomView/Advance/[09]Matrix_Basic.md | 610 +++++++++++++------------
1 file changed, 306 insertions(+), 304 deletions(-)
diff --git a/CustomView/Advance/[09]Matrix_Basic.md b/CustomView/Advance/[09]Matrix_Basic.md
index 96068632..b8a2d453 100644
--- a/CustomView/Advance/[09]Matrix_Basic.md
+++ b/CustomView/Advance/[09]Matrix_Basic.md
@@ -1,11 +1,9 @@
-# Matrix 原理
-
本文内容偏向理论,和 [画布操作](http://www.gcssloop.com/customview/Canvas_Convert/) 有重叠的部分,本文会让你更加深入的了解其中的原理。
本篇的主角Matrix,是一个一直在后台默默工作的劳动模范,虽然我们所有看到View背后都有着Matrix的功劳,但我们却很少见到它,本篇我们就看看它是何方神圣吧。
> 由于Google已经对这一部分已经做了很好的封装,所以跳过本部分对实际开发影响并不会太大,不想深究的粗略浏览即可,下一篇中将会详细讲解Matrix的具体用法和技巧。
-
+>
> ## ⚠️ 警告:测试本文章示例之前请关闭硬件加速。
## Matrix简介
@@ -15,15 +13,15 @@
它看起来大概是下面这样:

**Matrix作用就是坐标映射,那么为什么需要Matrix呢? 举一个简单的例子:**
@@ -32,8 +30,7 @@ $$)
以下图为例,我们的内容区和屏幕坐标系还相差一个通知栏加一个标题栏的距离,所以两者是不重合的,我们在内容区的坐标系中的内容最终绘制的时候肯定要转换为实际的物理坐标系来绘制,Matrix在此处的作用就是转换这些数值。
->
->假设通知栏高度为20像素,导航栏高度为40像素,那么我们在内容区的(0,0)位置绘制一个点,最终就要转化为在实际坐标系中的(0,60)位置绘制一个点。
+> 假设通知栏高度为20像素,导航栏高度为40像素,那么我们在内容区的(0,0)位置绘制一个点,最终就要转化为在实际坐标系中的(0,60)位置绘制一个点。

@@ -43,14 +40,10 @@ $$)
### Matrix特点
-* 作用范围更广,Matrix在View,图片,动画效果等各个方面均有运用,相比与之前讲解等画布操作应用范围更广。
-
-* 更加灵活,画布操作是对Matrix的封装,Matrix作为更接近底层的东西,必然要比画布操作更加灵活。
-
-* 封装很好,Matrix本身对各个方法就做了很好的封装,让开发者可以很方便的操作Matrix。
-
-* 难以深入理解,很难理解中各个数值的意义,以及操作规律,如果不了解矩阵,也很难理解前乘,后乘。
-
+- 作用范围更广,Matrix在View,图片,动画效果等各个方面均有运用,相比与之前讲解等画布操作应用范围更广。
+- 更加灵活,画布操作是对Matrix的封装,Matrix作为更接近底层的东西,必然要比画布操作更加灵活。
+- 封装很好,Matrix本身对各个方法就做了很好的封装,让开发者可以很方便的操作Matrix。
+- 难以深入理解,很难理解中各个数值的意义,以及操作规律,如果不了解矩阵,也很难理解前乘,后乘。
### 常见误解
@@ -62,7 +55,6 @@ $$)
的确,更改MPERSP_2的值能够达到类似缩放的效果,但这是因为齐次坐标的缘故,并非这个参数的实际功能。
-
## Matrix基本原理
Matrix 是一个矩阵,最根本的作用就是坐标转换,下面我们就看看几种常见变换的原理:
@@ -87,47 +79,44 @@ Matrix 是一个矩阵,最根本的作用就是坐标转换,下面我们就

-
用矩阵表示:

+> 你可能注意到了,我们坐标多了一个1,这是使用了齐次坐标系的缘故,在数学中我们的点和向量都是这样表示的(x, y),两者看起来一样,计算机无法区分,为此让计算机也可以区分它们,增加了一个标志位,增加之后看起来是这样:
>
-> 你可能注意到了,我们坐标多了一个1,这是使用了齐次坐标系的缘故,在数学中我们的点和向量都是这样表示的(x, y),两者看起来一样,计算机无法区分,为此让计算机也可以区分它们,增加了一个标志位,增加之后看起来是这样:
+> (x, y, 1) - 点
+> (x, y, 0) - 向量
>
-> (x, y, 1) - 点
-> (x, y, 0) - 向量
->
-> 另外,齐次坐标具有等比的性质,(2,3,1)、(4,6,2)...(2N,3N,N)表示的均是(2,3)这一个点。(**将MPERSP_2解释为scale这一误解就源于此**)。
+> 另外,齐次坐标具有等比的性质,(2,3,1)、(4,6,2)...(2N,3N,N)表示的均是(2,3)这一个点。(**将MPERSP_2解释为scale这一误解就源于此**)。
图例:

-
### 2.错切(Skew)
错切存在两种特殊错切,水平错切(平行X轴)和垂直错切(平行Y轴)。
@@ -141,28 +130,28 @@ $$)
用矩阵表示:

图例:
@@ -178,28 +167,28 @@ $$)
用矩阵表示:

图例:
@@ -217,28 +206,28 @@ $$)
用矩阵表示:

图例:
@@ -249,97 +238,98 @@ $$)
假定一个点 A(x0, y0) ,距离原点距离为 r, 与水平轴夹角为 α 度, 绕原点旋转 θ 度, 旋转后为点 B(x, y) 如下:
-
+
-
+

-= r \cdot cos \alpha \cdot cos \theta - r \cdot sin \alpha \cdot sin \theta
-= x_0 \cdot cos \theta - y_0 \cdot sin \theta
+x = r \\cdot cos( \\alpha + \\theta)
+= r \\cdot cos \\alpha \\cdot cos \\theta - r \\cdot sin \\alpha \\cdot sin \\theta
+= x_0 \\cdot cos \\theta - y_0 \\cdot sin \\theta
$$)

-= r \cdot sin \alpha \cdot cos \theta + r \cdot cos \alpha \cdot sin \theta
-= y_0 \cdot cos \theta + x_0 \cdot sin \theta
+y = r \\cdot sin( \\alpha + \\theta)
+= r \\cdot sin \\alpha \\cdot cos \\theta + r \\cdot cos \\alpha \\cdot sin \\theta
+= y_0 \\cdot cos \\theta + x_0 \\cdot sin \\theta
$$)
用矩阵表示:
 & -sin(\theta) & 0 \\
-sin(\theta) & cos(\theta) & 0 \\
- 0 & 0 & 1
-\end{1}
-\right ]
+\\left [
+\\begin{matrix}
+cos(\\theta) & -sin(\\theta) & 0 \\\\
+sin(\\theta) & cos(\\theta) & 0 \\\\
+
+```
+ 0 & 0 & 1
+```
+
+\\end{1}
+\\right ]
.
-\left [
-\begin{matrix}
-x_0\\
-y_0\\
+\\left [
+\\begin{matrix}
+x_0\\\\
+y_0\\\\
1
-\end{1}
-\right ]
+\\end{1}
+\\right ]
$$)
图例:

-
### 4.平移(Translate)
->
-> 此处也是使用齐次坐标的优点体现之一,实际上前面的三个操作使用 2x2 的矩阵也能满足需求,但是使用 2x2 的矩阵,无法将平移操作加入其中,而将坐标扩展为齐次坐标后,将矩阵扩展为 3x3 就可以将算法统一,四种算法均可以使用矩阵乘法完成。
+> 此处也是使用齐次坐标的优点体现之一,实际上前面的三个操作使用 2x2 的矩阵也能满足需求,但是使用 2x2 的矩阵,无法将平移操作加入其中,而将坐标扩展为齐次坐标后,将矩阵扩展为 3x3 就可以将算法统一,四种算法均可以使用矩阵乘法完成。
-
+
-
+
用矩阵表示:

图例:

-
## Matrix复合原理
其实Matrix的多种复合操作都是使用矩阵乘法实现的,从原理上理解很简单,但是,使用矩阵乘法也有其弱点,后面的操作可能会影响到前面到操作,所以在构造Matrix时顺序很重要。
@@ -350,7 +340,7 @@ $$)
前乘相当于矩阵的右乘:
-
+
> 这表示一个矩阵与一个特殊矩阵前乘后构造出结果矩阵。
@@ -358,7 +348,7 @@ $$)
前乘相当于矩阵的左乘:
-
+
> 这表示一个矩阵与一个特殊矩阵后乘后构造出结果矩阵。
@@ -372,13 +362,7 @@ $$)
**我由于也受到了这些文章的影响,自然而然的继承了这一理论,直到在评论区有一位小伙伴提出了一个问题,才让我重新审视了这一部分的内容,并进行了一定反思。**
-我思考的主要有以下部分:
-
-1. pre 和 post 的具体含义和作用。
-2. 矩阵构造过程中的正确影响顺序。
-3. 误解产生的具体源头。
-
-经过良久的思考之后,我决定推翻之前的理论和结论,重新构建了这一部分的理论知识,也许仍有疏漏,如有发现请指正。
+经过良久的思考之后,我决定抛弃国内大部分文章的那套理论和结论,只用严谨的数学逻辑和程序逻辑来阐述这一部分的理论,也许仍有疏漏,如有发现请指正。
**首先澄清两个错误结论,记住,是错误结论,错误结论,错误结论。**
@@ -406,9 +390,9 @@ Log.e("Matrix", matrix.toShortString());
> 设原始矩阵为 M,平移为 T ,旋转为 R ,单位矩阵为 I ,最终结果为 M'
>
-> * 矩阵乘法不满足交换律,即 A\*B ≠ B\*A
-> * 矩阵乘法满足结合律,即 (A\*B)\*C = A\*(B\*C)
-> * 矩阵与单位矩阵相乘结果不变,即 A * I = A
+> - 矩阵乘法不满足交换律,即 A\\*B ≠ B\\*A
+> - 矩阵乘法满足结合律,即 (A\\*B)\\*C = A\\*(B\\*C)
+> - 矩阵与单位矩阵相乘结果不变,即 A * I = A
```
由于上面例子中原始矩阵(M)是一个单位矩阵(I),所以可得:
@@ -426,7 +410,7 @@ M' = T*(R*M) = T*R*I = T*R
### ~~错误结论二:pre 是先执行,而 post 是后执行。~~
-这一条结论比上一条更加扯淡,同样是错误的,而且错的更离谱。
+这一条结论比上一条更离谱。
之所以产生这个错误完全是因为写文章的人懂英语。
@@ -435,7 +419,7 @@ pre :先,和 before 相似。
post :后,和 after 相似。
```
-所以就得出了 pre 先执行,而 post 后执行这一说法,但从严谨的数学和程序角度来分析,完全是扯淡的,还是上面所说的,**pre 和 post 不能影响程序执行顺序,而程序每执行一条语句都会得出一个确定的结果,所以,它根本不能控制先后执行,属于完全扯淡型。**
+所以就得出了 pre 先执行,而 post 后执行这一说法,但从严谨的数学和程序角度来分析,完全是不可能的,还是上面所说的,**pre 和 post 不能影响程序执行顺序,而程序每执行一条语句都会得出一个确定的结果,所以,它根本不能控制先后执行,属于完全扯淡型。**
**如果非要用这套理论强行解释的话,反而看起来像是 post 先执行,例如:**
@@ -451,19 +435,19 @@ matrix.postTranslate(pivotX,pivotY);
M‘ = T*(M*R) = T*M*R = (T*M)*R
```
-从实际上来说,由于矩阵乘法满足结合律,所以不论你说是靠右先执行还是靠左先执行,从结果上来说都没有错。
+**从实际上来说,由于矩阵乘法满足结合律,所以不论你说是靠右先执行还是靠左先执行,从结果上来说都没有错。**
**之前基于这条错误的结论我进行了一次错误的证明:**
-> **(这段内容注定要成为我写作历程中不可抹灭的耻辱,既然是公开文章,就应该对读者负责,虽然我在发表每一篇文章之前都竭力的求证其中的问题,各种细节,避免出现这种错误,但终究还是留下了这样一段内容,在此我诚挚的向我所有的读者道歉,并且我会尽力的修复在其他平台上遗留的错误副本。)**
+> **(这段内容注定要成为我写作历程中不可抹灭的耻辱,既然是公开文章,就应该对读者负责,虽然我在发表每一篇文章之前都竭力的求证其中的问题,各种细节,避免出现这种错误,但终究还是留下了这样一段内容,在此我诚挚的向我所有的读者道歉。)**
>
> 关注我的读者请尽量看我在 [个人博客](http://www.gcssloop.com/#blog) 和 [GitHub](https://github.com/GcsSloop/AndroidNote/blob/master/README.md) 发布的版本,这两个平台都在博文修复计划之内,有任何错误或者纰漏,都会首先修复这两个平台的文章。另外,所有进行修复过的文章都会在我的微博 [@GcsSloop](http://weibo.com/GcsSloop) 重新发布说明,关注我的微博可以第一时间得到博文更新或者修复的消息。
>
-> ----
+> ------
>
> ## 以下是错误证明:
>
-> 在实际操作中,我们每一步操作都会得出准确的计算结果,但是为什么还会用存在先后的说法? 难道真的能够用pre和post影响计算顺序? 实则不然,下面我们用一个例子说明:
+> ~~在实际操作中,我们每一步操作都会得出准确的计算结果,但是为什么还会用存在先后的说法? 难道真的能够用pre和post影响计算顺序? 实则不然,下面我们用一个例子说明:~~
>
> ```java
> Matrix matrix = new Matrix();
@@ -472,45 +456,70 @@ M‘ = T*(M*R) = T*M*R = (T*M)*R
> Log.e(TAG, "MatrixTest" + matrix.toShortString());
> ```
>
-> 在上面的操作中,如果按照正常的思路,先缩放,后平移,缩放操作执行在前,不会影响到后续的平移操作,但是执行结果却发现平移距离变成了(500, 800)。
+> ~~在上面的操作中,如果按照正常的思路,先缩放,后平移,缩放操作执行在前,不会影响到后续的平移操作,但是执行结果却发现平移距离变成了(500, 800)。~~
>
-> 在上面例子中,计算顺序是没有问题的,先计算的缩放,然后计算的平移,而缩放影响到平移则是因为前一步缩放后的结果矩阵右乘了平移矩阵,这是符合矩阵乘法的运算规律的,也就是说缩放操作虽然在前却影响到了平移操作,**相当于先执行了平移操作,然后执行的缩放操作,因此才有pre操作会先执行,而post操作会后执行这一说法**。
+> ~~在上面例子中,计算顺序是没有问题的,先计算的缩放,然后计算的平移,而缩放影响到平移则是因为前一步缩放后的结果矩阵右乘了平移矩阵,这是符合矩阵乘法的运算规律的,也就是说缩放操作虽然在前却影响到了平移操作,**相当于先执行了平移操作,然后执行的缩放操作,因此才有pre操作会先执行,而post操作会后执行这一说法**。~~
>
-> ----
-
-在上面例子中犯了一个非常隐蔽的错误,这个错误基于一条基本的理论,**到底是前面操作状态会影响后面操作状态,还是后面操作状态会影响前面操作状态?**
-
-在上面的错误论证中使用的是,后续操作状态会影响前面操作状态,但经过我实际测试情况来说应该是,**前面操作状态会影响到后续操作状态。** 即:
-
-```
-操作A
-操作B
-```
-
-那么 操作A 不应当受到 操作B 的影响,而 操作A 执行后的状态会影响到 操作B 的执行状态。
-
-```java
-Matrix matrix = new Matrix();
-matrix.postScale(0.5f, 0.8f);
-matrix.preTranslate(1000, 1000);
-Log.e(TAG, "MatrixTest" + matrix.toShortString());
-```
-
-在这个例子中,先执行的是缩放(Scale),而并非平移(Translate),即便将 postScale 换为 preScale,执行的结果也不会有变化,即下面的示例和上面是等价的:
-
-```java
-Matrix matrix = new Matrix();
-matrix.preScale(0.5f, 0.8f);
-matrix.preTranslate(1000, 1000);
-Log.e(TAG, "MatrixTest" + matrix.toShortString());
-```
-
-之所以平移距离是 MTRANS\_X = 500,MTRANS\_Y = 800,那是因为之前执行了缩放操作。
-Matrix 只是起到一种坐标转换的作用,并不能实际改变坐标系,所以说在缩放状态下执行平移操作,平移的距离也会被缩放,这样在绘制的时候才看起来像是缩放后的。
+> ------
+>
+> 上面的论证是完全错误的,因为可以轻松举出反例:
+>
+> ```java
+> Matrix matrix = new Matrix();
+> matrix.preScale(0.5f, 0.8f);
+> matrix.preTranslate(1000, 1000);
+> Log.e(TAG, "MatrixTest" + matrix.toShortString());
+> ```
+>
+> 反例中,虽然将 `postScale` 改为了 `preScale` ,但两者结果是完全相同的,所以先后论根本就是错误的。
+>
+> 他们结果相同是因为最终化简公式是相同的,都是 S*T
+>
+> 之所以平移距离是 MTRANS\\_X = 500,MTRANS\\_Y = 800,那是因为执行 Translate 之前 Matrix 已经具有了一个缩放比例。在右乘的时候影响到了具体的数值计算,可以用矩阵乘法计算一下。
+>
+> 
+>
+> 最终结果为:
+>
+> 
+>
+> 当 T*S 的时候,缩放比例则不会影响到 MTRANS\\_X 和 MTRANS\\_Y ,具体可以使用矩阵乘法自己计算一遍。
## 如何理解和使用 pre 和 post ?
-理解非常简单,不要去管什么先后论,顺序论,就按照最基本的矩阵乘法理解。
+不要去管什么先后论,顺序论,就按照最基本的矩阵乘法理解。
```
pre : 右乘, M‘ = M*A
@@ -527,10 +536,8 @@ post : 左乘, M’ = A*M
首先,有两条基本定理:
-* 所有的操作(旋转、平移、缩放、错切)默认都是以坐标原点为基准点的。
-
-* 之前操作的坐标系状态会保留,并且影响到后续状态。
-
+- 所有的操作(旋转、平移、缩放、错切)默认都是以坐标原点为基准点的。
+- 之前操作的坐标系状态会保留,并且影响到后续状态。
基于这两条基本定理,我们可以推算出要基于某一个点进行旋转需要如下步骤:
@@ -595,26 +602,21 @@ M' = T*M* ... *-T = T* ... *-T
所以说,pre 和 post 就是用来调整乘法顺序的,正常情况下应当正向进行构建出乘法顺序公式,之后根据实际情况调整书写即可。
-**在构造 Matrix 时,个人建议尽量使用一种乘法,前乘或者后乘,这样操作顺序容易确定,出现问题也比较容易排查。**
-**当然,由于矩阵乘法不满足交换律,前乘和后乘的结果是不同的,使用时应结合具体情景分析使用。**
-
+**在构造 Matrix 时,个人建议尽量使用一种乘法,前乘或者后乘,这样操作顺序容易确定,出现问题也比较容易排查。当然,由于矩阵乘法不满足交换律,前乘和后乘的结果是不同的,使用时应结合具体情景分析使用。**
-### 下面我们用不同对方式来构造一个矩阵:
-
-**假设我们需要先平移,再缩放,即平移不会受到缩放的影响。**
+### 下面我们用不同对方式来构造一个相同的矩阵:
注意:
-* 1.由于矩阵乘法不满足交换律,请保证使用初始矩阵(Initial Matrix),否则可能导致运算结果不同。
-* 2.注意构造顺序,顺序是会影响结果的。
-* 3.Initial Matrix是指new出来的新矩阵,或者reset后的矩阵,是一个单位矩阵。
-
+- 1.由于矩阵乘法不满足交换律,请保证使用初始矩阵(Initial Matrix),否则可能导致运算结果不同。
+- 2.注意构造顺序,顺序是会影响结果的。
+- 3.Initial Matrix是指new出来的新矩阵,或者reset后的矩阵,是一个单位矩阵。
#### 1.仅用pre:
-``` java
+```java
// 使用pre, M' = M*T*S = T*S
Matrix m = new Matrix();
m.reset();
@@ -625,42 +627,42 @@ m.preScale(sx, sy);
用矩阵表示:

#### 2.仅用post:
-``` java
+```java
// 使用post, M‘ = T*S*M = T*S
Matrix m = new Matrix();
m.reset();
@@ -671,42 +673,42 @@ m.postTranslate(tx, ty);
用矩阵表示:

#### 3.混合:
-``` java
+```java
// 混合 M‘ = T*M*S = T*S
Matrix m = new Matrix();
m.reset();
@@ -716,7 +718,7 @@ m.postTranslate(tx, ty);
或:
-``` java
+```java
// 混合 M‘ = T*M*S = T*S
Matrix m = new Matrix();
m.reset();
@@ -729,44 +731,43 @@ m.preScale(sx, sy);
用矩阵表示:

**注意: 由于矩阵乘法不满足交换律,请保证初始矩阵为单位矩阵,如果初始矩阵不为单位矩阵,则导致运算结果不同。**
上面虽然用了很多不同的写法,但最终的化简公式是一样的,这些不同的写法,都是根据同一个公式反向推算出来的。
-
## Matrix方法表
这个方法表,暂时放到这里让大家看看,方法的使用讲解放在下一篇文章中。
@@ -789,13 +790,14 @@ $$)
学完了本篇之后,推荐配合鸿洋大大的视频课程 [
打造个性的图片预览与多点触控](http://www.imooc.com/learn/239) 食用,定然能够让你对Matrix对理解更上一层楼。
+由于个人水平有限,文章中可能会出现错误,如果你觉得哪一部分有错误,或者发现了错别字等内容,欢迎在评论区告诉我,另外,据说关注 [作者微博](http://weibo.com/GcsSloop) 不仅能第一时间收到新文章消息,还能变帅哦。
+
## About
[本系列相关文章](http://www.gcssloop.com/customview/CustomViewIndex/)
作者微博: [GcsSloop](http://weibo.com/GcsSloop)
-
## 参考资料
[Matrix](https://developer.android.com/reference/android/graphics/Matrix.html)
@@ -805,4 +807,4 @@ $$)
[维基百科-齐次坐标](https://zh.wikipedia.org/wiki/%E9%BD%90%E6%AC%A1%E5%9D%90%E6%A0%87)
[维基百科-线性映射](https://zh.wikipedia.org/wiki/%E7%BA%BF%E6%80%A7%E6%98%A0%E5%B0%84)
[齐次坐标系入门级思考](https://oncemore2020.github.io/blog/homogeneous/)
-[仿射变换与齐次坐标](https://guangchun.wordpress.com/2011/10/12/affineandhomogeneous/)
+[仿射变换与齐次坐标](https://guangchun.wordpress.com/2011/10/12/affineandhomogeneous/)
\ No newline at end of file
From 95db7f113884a291f24f8fe507cd8fc02a230ada Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Tue, 20 Dec 2016 06:05:04 +0800
Subject: [PATCH 077/160] Update
---
CustomView/Advance/[09]Matrix_Basic.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CustomView/Advance/[09]Matrix_Basic.md b/CustomView/Advance/[09]Matrix_Basic.md
index b8a2d453..3e714fe7 100644
--- a/CustomView/Advance/[09]Matrix_Basic.md
+++ b/CustomView/Advance/[09]Matrix_Basic.md
@@ -15,7 +15,7 @@
, 1 deletion(-)
diff --git a/CustomView/Advance/[09]Matrix_Basic.md b/CustomView/Advance/[09]Matrix_Basic.md
index 3e714fe7..862995af 100644
--- a/CustomView/Advance/[09]Matrix_Basic.md
+++ b/CustomView/Advance/[09]Matrix_Basic.md
@@ -17,7 +17,7 @@
\\begin{matrix}
MSCALE\\_X & MSKEW\\_X & MTRANS\\_X \\\\
\\
-MSKEW\\_Y & MSCALE\\_Y & MTRANS\\_Y \\
+MSKEW\\_Y & MSCALE\\_Y & MTRANS\\_Y \\\\
\\
MPERSP\\_0 & MPERSP\\_1 & MPERSP\\_2
\\end{1}
From ee8c16b875761fcdb76154ffeeb708279d23e149 Mon Sep 17 00:00:00 2001
From: sloop
Date: Tue, 20 Dec 2016 06:14:40 +0800
Subject: [PATCH 079/160] Update
---
CustomView/Advance/[09]Matrix_Basic.md | 116 ++++++++++++-------------
1 file changed, 56 insertions(+), 60 deletions(-)
diff --git a/CustomView/Advance/[09]Matrix_Basic.md b/CustomView/Advance/[09]Matrix_Basic.md
index 862995af..6e874fb8 100644
--- a/CustomView/Advance/[09]Matrix_Basic.md
+++ b/CustomView/Advance/[09]Matrix_Basic.md
@@ -269,11 +269,7 @@ y\\\\
\\begin{matrix}
cos(\\theta) & -sin(\\theta) & 0 \\\\
sin(\\theta) & cos(\\theta) & 0 \\\\
-
-```
0 & 0 & 1
-```
-
\\end{1}
\\right ]
.
@@ -461,61 +457,61 @@ M‘ = T*(M*R) = T*M*R = (T*M)*R
> ~~在上面例子中,计算顺序是没有问题的,先计算的缩放,然后计算的平移,而缩放影响到平移则是因为前一步缩放后的结果矩阵右乘了平移矩阵,这是符合矩阵乘法的运算规律的,也就是说缩放操作虽然在前却影响到了平移操作,**相当于先执行了平移操作,然后执行的缩放操作,因此才有pre操作会先执行,而post操作会后执行这一说法**。~~
>
> ------
->
-> 上面的论证是完全错误的,因为可以轻松举出反例:
->
-> ```java
-> Matrix matrix = new Matrix();
-> matrix.preScale(0.5f, 0.8f);
-> matrix.preTranslate(1000, 1000);
-> Log.e(TAG, "MatrixTest" + matrix.toShortString());
-> ```
->
-> 反例中,虽然将 `postScale` 改为了 `preScale` ,但两者结果是完全相同的,所以先后论根本就是错误的。
->
-> 他们结果相同是因为最终化简公式是相同的,都是 S*T
->
-> 之所以平移距离是 MTRANS\\_X = 500,MTRANS\\_Y = 800,那是因为执行 Translate 之前 Matrix 已经具有了一个缩放比例。在右乘的时候影响到了具体的数值计算,可以用矩阵乘法计算一下。
->
-> 
->
-> 最终结果为:
->
-> 
->
-> 当 T*S 的时候,缩放比例则不会影响到 MTRANS\\_X 和 MTRANS\\_Y ,具体可以使用矩阵乘法自己计算一遍。
+
+上面的论证是完全错误的,因为可以轻松举出反例:
+
+```java
+Matrix matrix = new Matrix();
+matrix.preScale(0.5f, 0.8f);
+matrix.preTranslate(1000, 1000);
+Log.e(TAG, "MatrixTest" + matrix.toShortString());
+```
+
+反例中,虽然将 `postScale` 改为了 `preScale` ,但两者结果是完全相同的,所以先后论根本就是错误的。
+
+他们结果相同是因为最终化简公式是相同的,都是 S*T
+
+之所以平移距离是 MTRANS\\_X = 500,MTRANS\\_Y = 800,那是因为执行 Translate 之前 Matrix 已经具有了一个缩放比例。在右乘的时候影响到了具体的数值计算,可以用矩阵乘法计算一下。
+
+
+
+最终结果为:
+
+
+
+当 T*S 的时候,缩放比例则不会影响到 MTRANS\\_X 和 MTRANS\\_Y ,具体可以使用矩阵乘法自己计算一遍。
## 如何理解和使用 pre 和 post ?
@@ -807,4 +803,4 @@ $$)
[维基百科-齐次坐标](https://zh.wikipedia.org/wiki/%E9%BD%90%E6%AC%A1%E5%9D%90%E6%A0%87)
[维基百科-线性映射](https://zh.wikipedia.org/wiki/%E7%BA%BF%E6%80%A7%E6%98%A0%E5%B0%84)
[齐次坐标系入门级思考](https://oncemore2020.github.io/blog/homogeneous/)
-[仿射变换与齐次坐标](https://guangchun.wordpress.com/2011/10/12/affineandhomogeneous/)
\ No newline at end of file
+[仿射变换与齐次坐标](https://guangchun.wordpress.com/2011/10/12/affineandhomogeneous/)
From 2ba1ef8c8bf0dfe6731c3b6e4258b0a1e8754ce7 Mon Sep 17 00:00:00 2001
From: sloop
Date: Tue, 20 Dec 2016 06:17:33 +0800
Subject: [PATCH 080/160] Update
---
CustomView/Advance/[09]Matrix_Basic.md | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/CustomView/Advance/[09]Matrix_Basic.md b/CustomView/Advance/[09]Matrix_Basic.md
index 6e874fb8..852ee6d9 100644
--- a/CustomView/Advance/[09]Matrix_Basic.md
+++ b/CustomView/Advance/[09]Matrix_Basic.md
@@ -471,7 +471,7 @@ Log.e(TAG, "MatrixTest" + matrix.toShortString());
他们结果相同是因为最终化简公式是相同的,都是 S*T
-之所以平移距离是 MTRANS\\_X = 500,MTRANS\\_Y = 800,那是因为执行 Translate 之前 Matrix 已经具有了一个缩放比例。在右乘的时候影响到了具体的数值计算,可以用矩阵乘法计算一下。
+之所以平移距离是 MTRANS\_X = 500,MTRANS\_Y = 800,那是因为执行 Translate 之前 Matrix 已经具有了一个缩放比例。在右乘的时候影响到了具体的数值计算,可以用矩阵乘法计算一下。
);
0 & 1 & 1000 \\\\
0 & 0 & 1
\\end{1}
-\\right ]
-=
+\\right ] =
\\left [
\\begin{matrix}
0.5*1+0*0+0*0 & 0.5*0+0*1+0*0 & 0.5*1000+0*1000+0*1\\\\
From 5e5fac3b9080c997b62358e95c2fafe14c80f0a3 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Wed, 21 Dec 2016 01:58:33 +0800
Subject: [PATCH 081/160] Update
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 501b0674..d9935e07 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
## [博客](http://www.gcssloop.com/#blog "GcsSloop的博客")
-新开的博客,在博客中可以获得更好的阅读体验,同时在博客的评论区可以更及时的向我反馈文章中的问题。
+我的个人博客,在博客中可以获得更好的阅读体验,同时在博客的评论区可以更及时的向我反馈文章中的问题。
From debec143199b3a635f3ee7391b29ef1543ea79cc Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Thu, 22 Dec 2016 02:01:10 +0800
Subject: [PATCH 082/160] Update
---
CustomView/CustomViewRule.md | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 CustomView/CustomViewRule.md
diff --git a/CustomView/CustomViewRule.md b/CustomView/CustomViewRule.md
new file mode 100644
index 00000000..e69de29b
From c38ce87426f0c17719c18cb342c5abae5c143249 Mon Sep 17 00:00:00 2001
From: sloop
Date: Fri, 23 Dec 2016 23:23:08 +0800
Subject: [PATCH 083/160] Update
---
Tools/MarkdownEditor.md | 4 ----
1 file changed, 4 deletions(-)
delete mode 100644 Tools/MarkdownEditor.md
diff --git a/Tools/MarkdownEditor.md b/Tools/MarkdownEditor.md
deleted file mode 100644
index 96108ea1..00000000
--- a/Tools/MarkdownEditor.md
+++ /dev/null
@@ -1,4 +0,0 @@
-# Markdown编辑器推荐
-
-markdown是一个轻量级标记型书写语言,由于其学习门槛低和简洁性而被很多人喜爱。
-
From 06cb762648f31d51446225a0bc913241caa7b10e Mon Sep 17 00:00:00 2001
From: sloop
Date: Sat, 24 Dec 2016 23:20:31 +0800
Subject: [PATCH 084/160] Update
---
README.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index d9935e07..039775f2 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,8 @@
* [安卓自定义View进阶 - 特殊形状控件事件处理方案](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B17%5Dtouch-matrix-region.md)
+* [ViewSupport - 自定义View工具包](https://github.com/GcsSloop/ViewSupport)
+
******
## [教程类](https://github.com/GcsSloop/AndroidNote/tree/master/Course/README.md)
@@ -90,7 +92,6 @@
## 开源库
* [FontsManager - 快速替换字体](https://github.com/GcsSloop/FontsManager)
-* [ViewSupport - 自定义View工具包](https://github.com/GcsSloop/ViewSupport)
* [Rocker - 自定义摇杆](https://github.com/GcsSloop/Rocker)
* [LeafLoading - 进度条](https://github.com/GcsSloop/LeafLoading)
* [Rotate3dAnimation - 3D旋转动画(修正版)](https://github.com/GcsSloop/Rotate3dAnimation)
From 4a705285ca935e5cb04832686a2113472513f1bb Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Sun, 25 Dec 2016 00:13:46 +0800
Subject: [PATCH 085/160] Update
---
Course/Markdown/markdown-html.md | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 Course/Markdown/markdown-html.md
diff --git a/Course/Markdown/markdown-html.md b/Course/Markdown/markdown-html.md
new file mode 100644
index 00000000..e69de29b
From d56f57bffb32ce5debc08255d810e9982a442b54 Mon Sep 17 00:00:00 2001
From: sloop
Date: Tue, 27 Dec 2016 01:16:37 +0800
Subject: [PATCH 086/160] Update
---
Course/Markdown/markdown-html.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/Course/Markdown/markdown-html.md b/Course/Markdown/markdown-html.md
index e69de29b..e62c3b86 100644
--- a/Course/Markdown/markdown-html.md
+++ b/Course/Markdown/markdown-html.md
@@ -0,0 +1,2 @@
+Markdown 网页格式兼容
+
From 9a93508bd8568ec74fed077687cba8b2e9e56856 Mon Sep 17 00:00:00 2001
From: sloop
Date: Wed, 28 Dec 2016 23:58:13 +0800
Subject: [PATCH 087/160] Update
---
Lecture/gdg-developer-growth-guide.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/Lecture/gdg-developer-growth-guide.md b/Lecture/gdg-developer-growth-guide.md
index d98b68c2..0a383423 100755
--- a/Lecture/gdg-developer-growth-guide.md
+++ b/Lecture/gdg-developer-growth-guide.md
@@ -156,6 +156,7 @@ Hello,大家好,我是 GcsSloop,今天是我第一次在这么多陌生人
[^1]: GDG 全称 Google Developer Group,中文意思是 **谷歌开发者社区** 。
+
[^2]: DevFest 开发者节,今年(2016)的举办时间是 9月1日 到 11月30日 之间,全球大部分谷歌开发者社区都会举办该活动,一年一次。
From 008a7a5b6dd55a1b48ed89a634d01f8a336f0f8b Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Thu, 29 Dec 2016 01:23:30 +0800
Subject: [PATCH 088/160] Update
---
OpenGL/README.md | 3 +++
1 file changed, 3 insertions(+)
create mode 100644 OpenGL/README.md
diff --git a/OpenGL/README.md b/OpenGL/README.md
new file mode 100644
index 00000000..8f38aed2
--- /dev/null
+++ b/OpenGL/README.md
@@ -0,0 +1,3 @@
+# OpenGL
+
+OpenGL 全称 Open Graphics Library。
\ No newline at end of file
From 1a2ce5933027b34cb8709da64d933842d9fef7d3 Mon Sep 17 00:00:00 2001
From: sloop
Date: Sat, 31 Dec 2016 23:46:08 +0800
Subject: [PATCH 089/160] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=AE=80=E4=BB=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
OpenGL/README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/OpenGL/README.md b/OpenGL/README.md
index 8f38aed2..3f834828 100644
--- a/OpenGL/README.md
+++ b/OpenGL/README.md
@@ -1,3 +1,3 @@
# OpenGL
-OpenGL 全称 Open Graphics Library。
\ No newline at end of file
+OpenGL 全称 Open Graphics Library,是用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口(API)。OpenGL 常用于CAD、虚拟实境、科学可视化程序和电子游戏开发。
From d87e54ba4cc3d8a2ff7bdbfdb14a94fecfc9ee71 Mon Sep 17 00:00:00 2001
From: sloop
Date: Mon, 2 Jan 2017 22:01:25 +0800
Subject: [PATCH 090/160] Update
---
Lecture/README.md | 1 +
1 file changed, 1 insertion(+)
create mode 100644 Lecture/README.md
diff --git a/Lecture/README.md b/Lecture/README.md
new file mode 100644
index 00000000..adeb5d56
--- /dev/null
+++ b/Lecture/README.md
@@ -0,0 +1 @@
+# 演讲稿
From df488f2ccdfc32aa7cc71581f3ae89a1fd02250f Mon Sep 17 00:00:00 2001
From: sloop
Date: Wed, 11 Jan 2017 23:53:14 +0800
Subject: [PATCH 091/160] Update
---
CustomView/Advance/[18]multi-touch.md | 3 +++
1 file changed, 3 insertions(+)
diff --git a/CustomView/Advance/[18]multi-touch.md b/CustomView/Advance/[18]multi-touch.md
index a72ff088..a37c0d7f 100644
--- a/CustomView/Advance/[18]multi-touch.md
+++ b/CustomView/Advance/[18]multi-touch.md
@@ -1,2 +1,5 @@
# 多点触控
+在前面的几篇文章中我们大致了解了 Android 中的事件处理流程和一些简单的处理方案,本次带大家了解 Android 多点触控相关的一些知识。
+
+**多点触控** ( **Multitouch**,也称 **Multi-touch** ),即同时接受屏幕上多个点的人机交互操作,多点触控是从 Android 2.0 开始引入的功能,在 Android 2.2 时对这一部分进行了重新设计。
From d72e78ae7315c31f05462f6461e3fe08d00517c9 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Mon, 16 Jan 2017 06:22:58 +0800
Subject: [PATCH 092/160] Update
---
CustomView/Advance/[18]multi-touch.md | 613 +++++++++++++++++++++++++-
1 file changed, 612 insertions(+), 1 deletion(-)
diff --git a/CustomView/Advance/[18]multi-touch.md b/CustomView/Advance/[18]multi-touch.md
index a72ff088..d0034d9b 100644
--- a/CustomView/Advance/[18]multi-touch.md
+++ b/CustomView/Advance/[18]multi-touch.md
@@ -1,2 +1,613 @@
-# 多点触控
+# 多点触控详解
+Android 多点触控详解,在前面的几篇文章中我们大致了解了 Android 中的事件处理流程和一些简单的处理方案,本次带大家了解 Android 多点触控相关的一些知识。
+
+**多点触控** ( **Multitouch**,也称 **Multi-touch** ),即同时接受屏幕上多个点的人机交互操作,多点触控是从 Android 2.0 开始引入的功能,在 Android 2.2 时对这一部分进行了重新设计。
+
+在本文开始之前,先回顾一下 [MotionEvent详解][motionevent] 中提到过的内容:
+
+- Android 将所有的事件都封装进了 `Motionvent` 中。
+- 我们可以通过复写 `onTouchEvent` 或者设置 `OnTouchListener` 来获取 View 的事件。
+- 多点触控获取事件类型请使用 `getActionMasked()` 。
+- 追踪事件流请使用 `PointId`。
+
+**多点触控相关的事件:**
+
+| 事件 | 简介 |
+| --------------------------- | ------------------------------ |
+| ACTION_DOWN | **第一个** 手指 **初次接触到屏幕** 时触发。 |
+| ACTION_MOVE | 手指 **在屏幕上滑动** 时触发,会多次触发。 |
+| ACTION_UP | **最后一个** 手指 **离开屏幕** 时触发。 |
+| **ACTION_POINTER_DOWN** | 有非主要的手指按下(**即按下之前已经有手指在屏幕上**)。 |
+| **ACTION_POINTER_UP** | 有非主要的手指抬起(**即抬起之后仍然有手指在屏幕上**)。 |
+| 以下事件类型不推荐使用 | ---以下事件在 2.2 版本以上被标记为废弃--- |
+| ~~ACTION_POINTER\_1\_DOWN~~ | 第 2 个手指按下,已废弃,不推荐使用。 |
+| ~~ACTION_POINTER\_2\_DOWN~~ | 第 3 个手指按下,已废弃,不推荐使用。 |
+| ~~ACTION_POINTER\_3\_DOWN~~ | 第 4 个手指按下,已废弃,不推荐使用。 |
+| ~~ACTION_POINTER\_1\_UP~~ | 第 2 个手指抬起,已废弃,不推荐使用。 |
+| ~~ACTION_POINTER\_2\_UP~~ | 第 3 个手指抬起,已废弃,不推荐使用。 |
+| ~~ACTION_POINTER\_3\_UP~~ | 第 4 个手指抬起,已废弃,不推荐使用。 |
+
+**多点触控相关的方法:**
+
+| 方法 | 简介 |
+| ------------------------------- | ---------------------------------------- |
+| getActionMasked() | 与 `getAction()` 类似,**多点触控需要使用这个方法获取事件类型**。 |
+| getActionIndex() | 获取该事件是哪个指针(手指)产生的。 |
+| getPointerCount() | 获取在屏幕上手指的个数。 |
+| getPointerId(int pointerIndex) | 获取一个指针(手指)的唯一标识符ID,在手指按下和抬起之间ID始终不变。 |
+| findPointerIndex(int pointerId) | 通过PointerId获取到当前状态下PointIndex,之后通过PointIndex获取其他内容。 |
+| getX(int pointerIndex) | 获取某一个指针(手指)的X坐标 |
+| getY(int pointerIndex) | 获取某一个指针(手指)的Y坐标 |
+
+回顾完毕,开始正文。
+
+
+
+## 一、多点触控相关问题
+
+在引入多点触控之前,事件的类型很少,基本事件类型只有按下(down)、移动(move) 和 抬起(up),即便加上那些特殊的事件类型也只有几种而已,所以我们可以用几个常量来标记这些事件,在使用的时候使用 `getAction()` 方法来获取具体的事件,之后和这些常量进行对比就行了。
+
+在 Android 2.0 版本的时候,开始引入多点触控技术,由于技术上并不成熟,硬件和驱动也跟不上,很数设备只能支持追踪两三个点而已,因此在设计 API 上采取了一种简单粗暴的方案,添加了几个常量作为多点触控的事件类型。
+
+| 事件 | 简介 |
+| ----------------------- | -------------------- |
+| ACTION_POINTER\_1\_DOWN | 第 2 个手指按下,已废弃,不推荐使用。 |
+| ACTION_POINTER\_2\_DOWN | 第 3 个手指按下,已废弃,不推荐使用。 |
+| ACTION_POINTER\_3\_DOWN | 第 4 个手指按下,已废弃,不推荐使用。 |
+| ACTION_POINTER\_1\_UP | 第 2 个手指抬起,已废弃,不推荐使用。 |
+| ACTION_POINTER\_2\_UP | 第 3 个手指抬起,已废弃,不推荐使用。 |
+| ACTION_POINTER\_3\_UP | 第 4 个手指抬起,已废弃,不推荐使用。 |
+
+这些事件类型是用来判断非主要手指(第一个按下的称为主要手指)的按下和抬起事件。使用起来大概是这样子:
+
+```java
+switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN: break;
+ case MotionEvent.ACTION_UP: break;
+ case MotionEvent.ACTION_MOVE: break;
+ case MotionEvent.ACTION_POINTER_1_DOWN: break;
+ case MotionEvent.ACTION_POINTER_2_DOWN: break;
+ case MotionEvent.ACTION_POINTER_3_DOWN: break;
+ case MotionEvent.ACTION_POINTER_1_UP: break;
+ case MotionEvent.ACTION_POINTER_2_UP: break;
+ case MotionEvent.ACTION_POINTER_3_UP: break;
+}
+```
+
+看到这里可能会产生以下的一些疑问?
+
+### 1.为什么没有 ACTION_POINTER_X_MOVE ?
+
+在多指触控中所有的移动事件都是使用 `ACTION_MOVE`, 并没有追踪某一个手指的 move 事件类型,个人猜测主要是因为:**很难无歧义的实现单独追踪每一个手指。**
+
+要理解这个,首先要明白设备是如何识别多点触控的,设备没有眼睛,不能像我们人一样看到有几个手指(或者触控笔)在屏幕上。
+目前大多数 Android 设备都是电容屏,它们感知触摸是利用手指(触控笔)与屏幕接触产生的微小电流变化,之后通过计算这些电流变化来得出具体的触摸位置,在多点触控中,当两个触摸点足够靠近时,设备实际上是无法分清这两个点的。因此当两个触摸点靠近(重合)后再分开,设备很可能就无法正确的追踪两个点了,所以也很难实现无歧义的追踪每一个点。
+
+并且从软件上来说,事件的编号产生和复用也是一个大问题,例如下面的场景:
+
+| 事件 | 手指数量 | 编号变化 |
+| ------------ | :--: | ------------------------- |
+| 一个手指按下(命名为A) | 1 | A手指的编号为0,id为0 |
+| 一个手指按下(命名为B) | 2 | B手指的编号为1,id为1 |
+| A手指抬起 | 1 | B手指编号变更为0,id不变为1 |
+| 一个手指按下(命名为C) | 2 | C手指编号为0,id为0,B手指编号为1,id为1 |
+
+注意观察上面编号和id的变化,有两个问题,**1、B手指的编号变化了。2、A手指和C手指id是相同的(A手指抬起后,C手指按下替代了A手指)。**所以这就引出了一个问题:如果存在 ACTION_POINTER_X_MOVE,那么X应该用什么标志呢?编号会变化,id虽然不会变化,但id会被复用,例如A手指抬起后C手指按下,C手指复用了A手指的id。所以不论使用哪一个都不能保证唯一性。
+
+当然了,解决问题最好的方式就是把问题抛出去,既然从硬件和软件上都不能保证唯一性和不变性,就不做区分了,因此所有的 move 事件都是 `ACTION_MOVE`, 具体是哪个手指产生的 move 用户可以根据其他事件(按下和抬起)来综合判断。
+
+### 2.超过4个手指怎么办?
+
+**2.0 兼容版**,在2.2 之前的设计中,其提供的常量最多能判断四个手指的抬起和落下,当超过四个手指时怎么办呢?
+
+由于在 2.2 版本之前,由于没有 `getActionMasked` 方法,我们可以自己自己手动进行计算,例如下面这样 :
+
+```java
+String TAG = "Gcs";
+
+int action = event.getAction() & MotionEvent.ACTION_MASK;
+int index = (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
+ >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+
+switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ Log.e(TAG,"第1个手指按下");
+ break;
+ case MotionEvent.ACTION_UP:
+ Log.e(TAG,"最后1个手指抬起");
+ break;
+ case MotionEvent.ACTION_POINTER_1_DOWN: // 此时相当于 ACTION_POINTER_DOWN
+ Log.e(TAG,"第"+(index+1)+"个手指按下");
+ break;
+ case MotionEvent.ACTION_POINTER_1_UP: // 此时相当于 ACTION_POINTER_UP
+ Log.e(TAG,"第"+(index+1)+"个手指抬起");
+ break;
+}
+```
+
+在上面的例子中有几点比较关键:
+
+#### 2.1、action 与 Index 的获得
+
+我们在 [MotionEvent详解][motionevent] 中了解过,Android中的事件一般用最后8位来表示事件类型,再往前8位来表示Index。
+
+例如多指触控的按下事件,其事件类型是 0x000000**05**, 其Index标志位是 0x0000**00**05,随着更多的手指按下,其中变化的部分是 Index 标志位,最后两位是始终不变的,所以我们只要能将这两个分离开就行了。
+
+**取得事件类型(action)**
+
+```java
+// 获取事件类型
+int action = event.getAction() & MotionEvent.ACTION_MASK;
+```
+
+这个非常简单,ACTION_MASK=0x000000ff, 与 getAction() 进行按位与操作后保留最后8位内容(十六进制每一个字符转化为二进制是4位)。
+
+例如:
+0x000001**05** & 0x000000ff = 0x000000**01**
+
+**取得事件索引(index)**
+
+```java
+// 获取index编号
+int index = (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
+ >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+```
+
+ACTION_POINTER_INDEX_MASK = 0x0000ff00
+ACTION_POINTER_INDEX_SHIFT = 8
+首先让 getAction() 与 ACTION_POINTER_INDEX_MASK 按位与之后,只保留 Index 那8位,之后再右移8位,最终就拿到了 Index 的真实数值。
+
+例如:
+0x0000**01**05 & 0x0000ff00 = 0x0000**01**00
+0x0000**01**00 >> 8 = 0x000000**01**
+
+#### 2.2、用 ACTION\_POINTER\_1\_DOWN 代替 ACTION\_POINTER\_DOWN
+
+这是因为在 2.0 版本的时候还没有 ACTION\_POINTER\_DOWN 的这个常量,但是它们两个点数值是相同的,都是 0x00000005,这个你可以查看官方文档或者源码,甚至你直接写 `case 0x00000005` 也行,抬起也是同理。
+
+#### 2.3、只考虑兼容 2.2 以上的版本
+
+当然了,如果你不需要兼容 2.0 版本,只需要兼容到 2.2 以上的话就很简单了,像下面这样:
+
+```java
+String TAG = "Gcs";
+
+int index = event.getActionIndex();
+
+switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ Log.e(TAG,"第1个手指按下");
+ break;
+ case MotionEvent.ACTION_UP:
+ Log.e(TAG,"最后1个手指抬起");
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ Log.e(TAG,"第"+(index+1)+"个手指按下");
+ break;
+ case MotionEvent.ACTION_POINTER_UP:
+ Log.e(TAG,"第"+(index+1)+"个手指抬起");
+ break;
+}
+```
+
+### 3. index 和 pointId 的变化规则
+
+在 2.2 版本以上,我们可以通过 getActionIndex() 轻松获取到事件的索引(Index),但是这个事件索引的变化还是有点意思的,Index 变化有以下几个特点:
+
+1、从 0 开始,自动增长。
+2、如果之前落下的手指抬起,后面手指的 Index 会随之减小。
+3、Index 变化趋向于第一次落下的数值(落下手指时,前面有空缺会优先填补空缺)。
+4、对 move 事件无效。
+
+下面我们逐条解释一下具体含义。
+
+#### 3.1、从 0 开始,自动增长。
+
+这一条非常简单,也很容易理解,而且在 [MotionEvent详解][motionevent] 中讲解 getAction() 与 getActionMasked() 也简单说过。
+
+| 手指按下 | 触发事件(数值) |
+| :-----: | :--------------------------------------- |
+| 第1个手指按下 | ACTION_DOWN (0x0000**00**00) |
+| 第2个手指按下 | ACTION_POINTER_DOWN (0x0000**01**05) |
+| 第3个手指按下 | ACTION_POINTER_DOWN (0x0000**02**05) |
+| 第4个手指按下 | ACTION_POINTER_DOWN (0x0000**03**05) |
+
+注意加粗的位置,数值随着手指按下而不断变大。
+
+#### 3.2、如果之前落下的手指抬起,后面手指的 Index 会随之减小。
+
+这个也比较容易理解,像下面这样:
+
+| 手指按下 | 触发事件(数值) |
+| :-----: | :--------------------------------------- |
+| 第1个手指按下 | ACTION_DOWN (0x0000**00**00) |
+| 第2个手指按下 | ACTION_POINTER_DOWN (0x0000**01**05) |
+| 第3个手指按下 | ACTION_POINTER_DOWN (0x0000**02**05) |
+| 第2个手指抬起 | ACTION_POINTER_UP (0x0000**01**06) |
+| 第3个手指抬起 | ACTION_POINTER_UP (0x0000**01**06) |
+
+注意最后两次触发的事件,它的 Index 都是 1,这样也比较容易解释,当原本的第 2 个手指抬起后,屏幕上就只剩下两个手指了,之前的第 3 个手指就变成了第 2 个,于是抬起时触发事件的 Index 为 1,即之前落下的手指抬起,后面手指的 Index 会随之减小。
+
+#### 3.3、Index 变化趋向于第一次落下的数值(落下手指时,前面有空缺会优先填补空缺)。
+
+这个就有点神奇了,通过上一条规则,我们知道,某一个手指的 Index 可能会随着其他手指的抬起而变小,这次我们用 4 个手指测试一下 Index 的变化趋势。
+
+| 手指按下 | 触发事件(数值) |
+| :---------: | :--------------------------------------- |
+| 第1个手指按下 | ACTION_DOWN (0x0000**00**00) |
+| 第2个手指按下 | ACTION_POINTER_DOWN (0x0000**01**05) |
+| **第3个手指按下** | ACTION_POINTER_DOWN (0x0000**02**05) |
+| 第2个手指抬起 | ACTION_POINTER_UP (0x0000**01**06) |
+| ~~第3个手指抬起~~ | ~~ACTION_POINTER_UP~~ ~~(0x0000**01**06)~~ |
+| 第4个手指按下 | ACTION_POINTER_DOWN (0x0000**01**05) |
+| **第3个手指抬起** | ACTION_POINTER_UP (0x0000**02**06) |
+
+这个要和上一个对比这看,**重点观察第 3 个手指所触发事件区别**,在上一个示例中,随着第 2 个手指的抬起,第 3 个手指变化为第 2 个,所以抬起时触发的是第 2 根手指的抬起事件(删除线部分)。
+
+但是,如果第 2 个手指抬起后,落在屏幕上另外一个手指会怎样?经过测试,发现另外**落下的手指会替代之前第 2 个手指的位置,系统判定为 2,而不是顺延下去变成 3**,但是如果继续落下其他的手指,数值则会顺延。
+
+**即手指抬起时的 Index 会趋向于和按下时相同,虽然在手指数量不足时,Index 会变小,但是当手指变多时,Index 会保持和按下时一样。**
+
+> PS:由于程序是从0开始计数的,所以 0 就是 1, 1 就是 2 ...
+
+#### 3.4、对 move 事件无效。
+
+这个也比较容易理解,我们所取得的 Index 属性实际上是从事件上分离下来的,但是 move 事件始终为 0x0000**00**02,也就是说,在 move 时不论你移动哪个手指,使用 ` getActionIndex()` 获取到的始终是数值 0。
+
+既然 move 事件无法用事件索引(Index)区别,那么该如何区分 move 是那个手指发出的呢?这就要用到 pointId 了,**pointId 和 index 最大的区别就是 pointId 是不变的,始终为第一次落下时生成的数值,不会受到其他手指抬起和落下的影响。**
+
+#### 3.5、pointId 与 index 的异同。
+
+相同点:
+
+* 从 0 开始,自动增长。
+* 落下手指时优先填补空缺(填补之前抬起手指的编号)。
+
+不同点:
+
+* Index 会变化,pointId 始终不变。
+
+### 4. Move 相关事件
+
+#### 4.1 actionIndex 与 pointerIndex
+
+在 move 中无法取得 actionIndex 的,我们需要使用 pointerIndex 来获取更多的信息,例如某个手指的坐标:
+
+```java
+getX(int pointerIndex)
+getY(int pointerIndex)
+```
+
+**但是这个 pointerIndex 又是什么呢?和 actionIndex 有区别么?**
+
+实际上这个 pointerIndex 和 actionIndex 区别并不大,两者的数值是相同的,你可以认为 pointerIndex 是特地为 move 事件准备的 actionIndex。
+
+#### 4.2 pointerIndex 与 pointerId
+
+| 类型 | 简介 |
+| ------------ | ----------------------------- |
+| pointerIndex | 用于获取具体事件,可能会随着其他手指的抬起和落下而变化 |
+| pointerId | 用于识别手指,手指按下时产生,手指抬起时回收,期间始终不变 |
+
+这两个数值使用以下两个方法相互转换。
+
+| 方法 | 简介 |
+| ------------------------------- | ---------------------------------------- |
+| getPointerId(int pointerIndex) | 获取一个指针(手指)的唯一标识符ID,在手指按下和抬起之间ID始终不变。 |
+| findPointerIndex(int pointerId) | 通过 pointerId 获取到当前状态下 pointIndex,之后通过 pointIndex 获取其他内容。 |
+
+> 通常情况下,pointerIndex 和 pointerId 是相同的,但也可能会因为某些手指的抬起而变得不同。
+
+#### 4.3 遍历多点触控
+
+先来一个简单的,遍历出多个手指的 move 事件:
+
+```java
+String TAG = "Gcs";
+switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_MOVE:
+ for (int i = 0; i < event.getPointerCount(); i++) {
+ Log.i("TAG", "pointerIndex="+i+", pointerId="+event.getPointerId(i));
+ // TODO
+ }
+}
+```
+
+通过遍历 pointerCount 获取到所有的 pointerIndex,同时通过 pointerIndex 来获取 pointerId,可以通过不同手指抬起和按下后移动来观察 pointerIndex 和 pointerId 的变化。
+
+#### 4.4 在多点触控中追踪单个手指
+
+要实现追踪单个手指还是有些麻烦的,需要同时使用上 actionIndex, pointerId 和 pointerIndex,例如,我们只追踪第2个手指,并画出其位置:
+
+```java
+/**
+ * 绘制出第二个手指第位置
+ */
+public class MultiTouchTest extends CustomView {
+ String TAG = "Gcs";
+
+ // 用于判断第2个手指是否存在
+ boolean haveSecondPoint = false;
+
+ // 记录第2个手指第位置
+ PointF point = new PointF(0, 0);
+
+ public MultiTouchTest(Context context) {
+ this(context, null);
+ }
+
+ public MultiTouchTest(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ mDeafultPaint.setAntiAlias(true);
+ mDeafultPaint.setTextAlign(Paint.Align.CENTER);
+ mDeafultPaint.setTextSize(30);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ int index = event.getActionIndex();
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_POINTER_DOWN:
+ // 判断是否是第2个手指按下
+ if (event.getPointerId(index) == 1){
+ haveSecondPoint = true;
+ point.set(event.getY(), event.getX());
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_UP:
+ // 判断抬起的手指是否是第2个
+ if (event.getPointerId(index) == 1){
+ haveSecondPoint = false;
+ point.set(0, 0);
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (haveSecondPoint) {
+ // 通过 pointerId 来获取 pointerIndex
+ int pointerIndex = event.findPointerIndex(1);
+ // 通过 pointerIndex 来取出对应的坐标
+ point.set(event.getX(pointerIndex), event.getY(pointerIndex));
+ }
+ break;
+ }
+
+ invalidate(); // 刷新
+
+ return true;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ canvas.save();
+ canvas.translate(mViewWidth/2, mViewHeight/2);
+ canvas.drawText("追踪第2个按下手指的位置", 0, 0, mDeafultPaint);
+ canvas.restore();
+
+ // 如果屏幕上有第2个手指则绘制出来其位置
+ if (haveSecondPoint) {
+ canvas.drawCircle(point.x, point.y, 50, mDeafultPaint);
+ }
+ }
+}
+```
+
+这段代码也非常短,其核心就是通过判断数值为 1 的 pointerId 是否存在,如果存在就在 move 的时候取出其坐标,并绘制出来。
+
+
+
+> 虽然逻辑简单,但个人感觉写起来还是有些麻烦,如果有更简单的方案欢迎告诉我。
+
+
+
+## 二、如何使用多点触控
+
+多点触控应用还是比较广泛的,至少目前大部分的图片查看都需要用到多点触控技术(用于拖动和缩放图片)。
+
+但是在某些看似不需要多触控的地方也需要对多点触控进行判断,只要是多点触控可能引起错误的地方都应该加上多点触控的判断。例如使用到 move 事件的时候,由于 move 事件可能由多个手指同时触发,所以可能会出现同时被多个手指控制的情况,如果不适当的处理,这个 move 就可能由任何一个手指触发。
+
+举一个简单的例子:
+
+如果我们需要一个**可以用单指拖动的图片**。加入我们不进行多指触控的判断,像下面这样:
+
+**没有针对多指触控处理版本:**
+
+```java
+/**
+ * 一个可以拖图片动的 View
+ */
+public class DragView1 extends CustomView {
+ String TAG = "Gcs";
+
+ Bitmap mBitmap; // 图片
+ RectF mBitmapRectF; // 图片所在区域
+ Matrix mBitmapMatrix; // 控制图片的 matrix
+
+ boolean canDrag = false;
+ PointF lastPoint = new PointF(0, 0);
+
+ public DragView1(Context context) {
+ this(context, null);
+ }
+
+ public DragView1(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ // 调整图片大小
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.outWidth = 960/2;
+ options.outHeight = 800/2;
+
+ mBitmap = BitmapFactory.decodeResource(this.getResources(), R.drawable.drag_test, options);
+ mBitmapRectF = new RectF(0,0,mBitmap.getWidth(), mBitmap.getHeight());
+ mBitmapMatrix = new Matrix();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ // 判断按下位置是否包含在图片区域内
+ if (mBitmapRectF.contains((int)event.getX(), (int)event.getY())){
+ canDrag = true;
+ lastPoint.set(event.getX(), event.getY());
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ canDrag = false;
+ case MotionEvent.ACTION_MOVE:
+ if (canDrag) {
+ // 移动图片
+ mBitmapMatrix.postTranslate(event.getX() - lastPoint.x, event.getY() - lastPoint.y);
+ // 更新上一次点位置
+ lastPoint.set(event.getX(), event.getY());
+
+ // 更新图片区域
+ mBitmapRectF = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
+ mBitmapMatrix.mapRect(mBitmapRectF);
+
+ invalidate();
+ }
+ break;
+ }
+
+ return true;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ canvas.drawBitmap(mBitmap, mBitmapMatrix, mDeafultPaint);
+ }
+}
+```
+
+这个版本非常简单,当然了,如果正常使用(只使用一个手指)的话也不会出问题,但是当使用多个手指,且有抬起和按下的时候就可能出问题,下面用一个典型的场景演示一下:
+
+
+
+注意在第二个手指按下,第一个手指抬起时,此时原本的第二个手指会被识别为第一个,所以图片会直接跳动到第二个手指位置。
+
+为了不出现这种情况,我们可以判断一下 pointId 并且只获取第一个手指的数据,这样就能避免这种情况发生了,如下。
+
+**针对多指触控处理后版本:**
+
+```java
+/**
+ * 一个可以拖图片动的 View
+ */
+public class DragView extends CustomView {
+ String TAG = "Gcs";
+
+ Bitmap mBitmap; // 图片
+ RectF mBitmapRectF; // 图片所在区域
+ Matrix mBitmapMatrix; // 控制图片的 matrix
+
+ boolean canDrag = false;
+ PointF lastPoint = new PointF(0, 0);
+
+ public DragView(Context context) {
+ this(context, null);
+ }
+
+ public DragView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.outWidth = 960/2;
+ options.outHeight = 800/2;
+
+ mBitmap = BitmapFactory.decodeResource(this.getResources(), R.drawable.drag_test, options);
+ mBitmapRectF = new RectF(0,0,mBitmap.getWidth(), mBitmap.getHeight());
+ mBitmapMatrix = new Matrix();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_POINTER_DOWN:
+ // ▼ 判断是否是第一个手指 && 是否包含在图片区域内
+ if (event.getPointerId(event.getActionIndex()) == 0 && mBitmapRectF.contains((int)event.getX(), (int)event.getY())){
+ canDrag = true;
+ lastPoint.set(event.getX(), event.getY());
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_POINTER_UP:
+ // ▼ 判断是否是第一个手指
+ if (event.getPointerId(event.getActionIndex()) == 0){
+ canDrag = false;
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ // 如果存在第一个手指,且这个手指的落点在图片区域内
+ if (canDrag) {
+ // ▼ 注意 getX 和 getY
+ int index = event.findPointerIndex(0);
+ // Log.i(TAG, "index="+index);
+ mBitmapMatrix.postTranslate(event.getX(index)-lastPoint.x, event.getY(index)-lastPoint.y);
+ lastPoint.set(event.getX(index), event.getY(index));
+
+ mBitmapRectF = new RectF(0,0,mBitmap.getWidth(), mBitmap.getHeight());
+ mBitmapMatrix.mapRect(mBitmapRectF);
+
+ invalidate();
+ }
+ break;
+ }
+
+ return true;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ canvas.drawBitmap(mBitmap, mBitmapMatrix, mDeafultPaint);
+ }
+}
+```
+
+可以看到,比起上一个版本并么有多多少代码,但是更加“智能”了,可以准确识别某一个手指,不会因为手指抬起而认错手指。
+
+
+
+**重点注意最后,第一个手指抬起之后,图片并没有跳跃到第二个手指的位置。**
+
+上面的两个对比示例都精简到了极致,其核心依旧是正确的追踪某一个手指,建议大家自己写一遍体会一下。
+
+----
+
+我感觉很多人看到这里依旧是不明所以的,一些简单的东西还好弄,但是复杂一些,如同时处理多个手指的数值就有些困难了,**假如说你之前没有接触过多点触控的处理,此时让你实现用两个手指来缩放图片还是有些困难的。**
+
+因为这不仅要追踪两个手指的位置,还要根据位置变化来计算缩放比例和缩放中心,单单这两个非常简单的数学问题就能难倒一大批人。
+
+
+
+当然了,很多麻烦问题都有简单的解决方案,假如说我们真的要实现一个可以用两个或者多个手指缩放的控件,何必要自己算呢,可以尝试一下 Android 自带的解决方案:**手势检测(GestureDetector)**,不仅能自动帮你计算好缩放比例和缩放中心,而且还可以检测出 单击、长按、滑屏 等不同的手势,不过这就不是本篇的事情了,以后有时间会写一下有关手势检测的用法(继续挖坑)。
+
+## 三、总结
+
+前段时间因为各种事情比较忙,这篇文章也没时间去写,所以就一直拖到了现在,期间收到不少读者催更,实在是抱歉了。今后在会尽量保证稳定更新的,争取尽快把自定义View系列这一个大坑填完。 ˊ_>ˋ
+
+关于多点触控,个人认为还算一个比较重要的知识点。尤其是随着 Android 的发展,很多炫酷的交互操作可能会需要用户进行拖拽操作。在进行这类操作的时候进行一下手指的判断还是相当重要的。
+
+本文中需要注意的几个知识点:
+
+* 如何兼容 2.0 版本的多点触控(目前大部分都不需要兼容 2.0 了吧)。
+* actionIndex、pointIndex 与 pointId 的区别和用法。
+* 如何在多点触控中正确的追踪一个手指。
+
+## About Me
+
+### 作者微博: @GcsSloop
+
+
+
+## 参考资料
+
+[MotionEvent ](https://developer.android.com/reference/android/view/MotionEvent.html)
+
+
+
+[motionevent]: http://www.gcssloop.com/customview/motionevent
\ No newline at end of file
From 2fdd7695e1ff08742b2a2c57628ffd2fd21968e4 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Mon, 16 Jan 2017 06:56:10 +0800
Subject: [PATCH 093/160] Update
---
CustomView/Advance/[18]multi-touch.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/CustomView/Advance/[18]multi-touch.md b/CustomView/Advance/[18]multi-touch.md
index 044b59fe..3f534ec9 100644
--- a/CustomView/Advance/[18]multi-touch.md
+++ b/CustomView/Advance/[18]multi-touch.md
@@ -1,4 +1,4 @@
-# 多点触控详解
+# 多点触控详解
Android 多点触控详解,在前面的几篇文章中我们大致了解了 Android 中的事件处理流程和一些简单的处理方案,本次带大家了解 Android 多点触控相关的一些知识。
@@ -48,7 +48,7 @@ Android 多点触控详解,在前面的几篇文章中我们大致了解了 An
在引入多点触控之前,事件的类型很少,基本事件类型只有按下(down)、移动(move) 和 抬起(up),即便加上那些特殊的事件类型也只有几种而已,所以我们可以用几个常量来标记这些事件,在使用的时候使用 `getAction()` 方法来获取具体的事件,之后和这些常量进行对比就行了。
-在 Android 2.0 版本的时候,开始引入多点触控技术,由于技术上并不成熟,硬件和驱动也跟不上,很数设备只能支持追踪两三个点而已,因此在设计 API 上采取了一种简单粗暴的方案,添加了几个常量作为多点触控的事件类型。
+在 Android 2.0 版本的时候,开始引入多点触控技术,由于技术上并不成熟,硬件和驱动也跟不上,很数设备只能支持追踪两三个点而已,因此在设计 API 上采取了一种简单粗暴的方案,添加了几个常量用语多点触控的事件类型的判断。
| 事件 | 简介 |
| ----------------------- | -------------------- |
@@ -59,7 +59,7 @@ Android 多点触控详解,在前面的几篇文章中我们大致了解了 An
| ACTION_POINTER\_2\_UP | 第 3 个手指抬起,已废弃,不推荐使用。 |
| ACTION_POINTER\_3\_UP | 第 4 个手指抬起,已废弃,不推荐使用。 |
-这些事件类型是用来判断非主要手指(第一个按下的称为主要手指)的按下和抬起事件。使用起来大概是这样子:
+这些事件类型是用来判断非主要手指(第一个按下的称为主要手指)的按下和抬起,使用起来大概是这样子:
```java
switch (event.getAction()) {
From 95245d55889872341c8f72de14f780009efcfadc Mon Sep 17 00:00:00 2001
From: sloop
Date: Mon, 16 Jan 2017 14:19:23 +0800
Subject: [PATCH 094/160] Update
---
CustomView/Advance/[18]multi-touch.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/CustomView/Advance/[18]multi-touch.md b/CustomView/Advance/[18]multi-touch.md
index 3f534ec9..ff59eacc 100644
--- a/CustomView/Advance/[18]multi-touch.md
+++ b/CustomView/Advance/[18]multi-touch.md
@@ -144,7 +144,7 @@ int action = event.getAction() & MotionEvent.ACTION_MASK;
这个非常简单,ACTION_MASK=0x000000ff, 与 getAction() 进行按位与操作后保留最后8位内容(十六进制每一个字符转化为二进制是4位)。
例如:
-0x000001**05** & 0x000000ff = 0x000000**01**
+0x000001**05** & 0x000000ff = 0x000000**05**
**取得事件索引(index)**
@@ -610,4 +610,4 @@ public class DragView extends CustomView {
-[motionevent]: http://www.gcssloop.com/customview/motionevent
\ No newline at end of file
+[motionevent]: http://www.gcssloop.com/customview/motionevent
From 26850480a04c09698cad444f08bfc901e5c3616e Mon Sep 17 00:00:00 2001
From: sloop
Date: Tue, 17 Jan 2017 13:16:28 +0800
Subject: [PATCH 095/160] Update
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index 039775f2..6228a394 100644
--- a/README.md
+++ b/README.md
@@ -37,6 +37,7 @@
* [安卓自定义View进阶 - 事件分发机制详解](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B15%5DDispatch-TouchEvent-Source.md)
* [安卓自定义View进阶 - MotionEvent详解](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B16%5DMotionEvent.md)
* [安卓自定义View进阶 - 特殊形状控件事件处理方案](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B17%5Dtouch-matrix-region.md)
+ * [安卓自定义View进阶 - 多点触控详解](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B18%5Dmulti-touch.md)
* [ViewSupport - 自定义View工具包](https://github.com/GcsSloop/ViewSupport)
From d1d658167e5f71246edc4f3171fd50f8ffa08e81 Mon Sep 17 00:00:00 2001
From: sloop
Date: Tue, 17 Jan 2017 13:17:09 +0800
Subject: [PATCH 096/160] Update
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 6228a394..bc434206 100644
--- a/README.md
+++ b/README.md
@@ -36,7 +36,7 @@
* [安卓自定义View进阶 - 事件分发机制原理](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B12%5DDispatch-TouchEvent-Theory.md)
* [安卓自定义View进阶 - 事件分发机制详解](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B15%5DDispatch-TouchEvent-Source.md)
* [安卓自定义View进阶 - MotionEvent详解](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B16%5DMotionEvent.md)
- * [安卓自定义View进阶 - 特殊形状控件事件处理方案](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B17%5Dtouch-matrix-region.md)
+ * [安卓自定义View进阶 - 特殊形状控件事件处理方案](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B17%5Dtouch-matrix-region.md)
* [安卓自定义View进阶 - 多点触控详解](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B18%5Dmulti-touch.md)
From 56aafd8238fe33bed895eea6a30a6ff10322a88f Mon Sep 17 00:00:00 2001
From: sloop
Date: Tue, 17 Jan 2017 13:17:56 +0800
Subject: [PATCH 097/160] Update
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index bc434206..3b7c74ac 100644
--- a/README.md
+++ b/README.md
@@ -37,7 +37,7 @@
* [安卓自定义View进阶 - 事件分发机制详解](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B15%5DDispatch-TouchEvent-Source.md)
* [安卓自定义View进阶 - MotionEvent详解](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B16%5DMotionEvent.md)
* [安卓自定义View进阶 - 特殊形状控件事件处理方案](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B17%5Dtouch-matrix-region.md)
- * [安卓自定义View进阶 - 多点触控详解](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B18%5Dmulti-touch.md)
+ * [安卓自定义View进阶 - 多点触控详解](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B18%5Dmulti-touch.md)
* [ViewSupport - 自定义View工具包](https://github.com/GcsSloop/ViewSupport)
From 8d382604f04176a3f72692da01090d1e61b60385 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Wed, 18 Jan 2017 02:51:13 +0800
Subject: [PATCH 098/160] Update
---
CustomView/Advance/[08]Path_Play.md | 388 +++++++++++++++-------------
1 file changed, 212 insertions(+), 176 deletions(-)
diff --git a/CustomView/Advance/[08]Path_Play.md b/CustomView/Advance/[08]Path_Play.md
index 3c6e342c..7b4285a8 100644
--- a/CustomView/Advance/[08]Path_Play.md
+++ b/CustomView/Advance/[08]Path_Play.md
@@ -1,13 +1,9 @@
# Path之玩出花样(PathMeasure)
-### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop)
-### [【本系列相关文章】](https://github.com/GcsSloop/AndroidNote/tree/master/CustomView/README.md)
-
-
可以看到,在经过
-[Path之基本操作](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B05%5DPath_Basic.md)
-[Path之贝塞尔曲线](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B06%5DPath_Bezier.md) 和
-[Path之完结篇(伪)](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B07%5DPath_Over.md) 后, Path中各类方法基本上都讲完了,表格中还没有讲解到到方法就是矩阵变换了,难道本篇终于要讲矩阵了?
+[Path之基本操作](http://www.gcssloop.com/customview/Path_Basic/)
+[Path之贝塞尔曲线](http://www.gcssloop.com/customview/Path_Bezier/) 和
+[Path之完结篇](http://www.gcssloop.com/customview/Path_Over/) 后, Path中各类方法基本上都讲完了,表格中还没有讲解到到方法就是矩阵变换了,难道本篇终于要讲矩阵了?
非也,矩阵这一部分仍在后面单独讲解,本篇主要讲解 PathMeasure 这个类与 Path 的一些使用技巧。
> PS:不要问我为什么不讲 PathEffect,因为这个方法在后面的Paint系列中。
@@ -16,9 +12,9 @@

-******
+------
-## Path & PathMeasure
+## Path & PathMeasure
顾名思义,PathMeasure是一个用来测量Path的类,主要有以下方法:
@@ -43,8 +39,7 @@
PathMeasure的方法也不多,接下来我们就逐一的讲解一下。
-******
-
+------
### 1.构造函数
@@ -52,7 +47,7 @@ PathMeasure的方法也不多,接下来我们就逐一的讲解一下。
**无参构造函数:**
-``` java
+```java
PathMeasure ()
```
@@ -60,7 +55,7 @@ PathMeasure的方法也不多,接下来我们就逐一的讲解一下。
**有参构造函数:**
-``` java
+```java
PathMeasure (Path path, boolean forceClosed)
```
@@ -71,33 +66,35 @@ PathMeasure的方法也不多,接下来我们就逐一的讲解一下。
**在这里有两点需要明确:**
>
-* 1. 不论 forceClosed 设置为何种状态(true 或者 false), 都不会影响原有Path的状态,**即 Path 与 PathMeasure 关联之后,之前的的 Path 不会有任何改变。**
-* 2. forceClosed 的设置状态可能会影响测量结果,**如果 Path 未闭合但在与 PathMeasure 关联的时候设置 forceClosed 为 true 时,测量结果可能会比 Path 实际长度稍长一点,获取到到是该 Path 闭合时的状态。**
+
+- 1. 不论 forceClosed 设置为何种状态(true 或者 false), 都不会影响原有Path的状态,**即 Path 与 PathMeasure 关联之后,之前的的 Path 不会有任何改变。**
+- 1. forceClosed 的设置状态可能会影响测量结果,**如果 Path 未闭合但在与 PathMeasure 关联的时候设置 forceClosed 为 true 时,测量结果可能会比 Path 实际长度稍长一点,获取到到是该 Path 闭合时的状态。**
下面我们用一个例子来验证一下:
-```
- canvas.translate(mViewWidth/2,mViewHeight/2);
+```java
+canvas.translate(mViewWidth/2,mViewHeight/2);
- Path path = new Path();
+Path path = new Path();
- path.lineTo(0,200);
- path.lineTo(200,200);
- path.lineTo(200,0);
+path.lineTo(0,200);
+path.lineTo(200,200);
+path.lineTo(200,0);
- PathMeasure measure1 = new PathMeasure(path,false);
- PathMeasure measure2 = new PathMeasure(path,true);
+PathMeasure measure1 = new PathMeasure(path,false);
+PathMeasure measure2 = new PathMeasure(path,true);
- Log.e("TAG", "forceClosed=false---->"+measure1.getLength());
- Log.e("TAG", "forceClosed=true----->"+measure2.getLength());
+Log.e("TAG", "forceClosed=false---->"+measure1.getLength());
+Log.e("TAG", "forceClosed=true----->"+measure2.getLength());
- canvas.drawPath(path,mDeafultPaint);
+canvas.drawPath(path,mDeafultPaint);
```
log如下:
-```
- 25521-25521/com.gcssloop.canvas E/TAG: forceClosed=false---->600.0
- 25521-25521/com.gcssloop.canvas E/TAG: forceClosed=true----->800.0
+
+```shell
+com.gcssloop.canvas E/TAG: forceClosed=false---->600.0
+com.gcssloop.canvas E/TAG: forceClosed=true----->800.0
```
绘制在界面上的效果如下:
@@ -107,10 +104,9 @@ log如下:
我们所创建的 Path 实际上是一个边长为 200 的正方形的三条边,通过上面的示例就能验证以上两个问题。
>
-* 1.我们将 Path 与两个的 PathMeasure 进行关联,并给 forceClosed 设置了不同的状态,之后绘制再绘制出来的 Path 没有任何变化,所以与 Path 与 PathMeasure进行关联并不会影响 Path 状态。
-* 2.我们可以看到,设置 forceClosed 为 true 的方法比设置为 false 的方法测量出来的长度要长一点,这是由于 Path 没有闭合的缘故,多出来的距离正是 Path 最后一个点与最开始一个点之间点距离。**forceClosed 为 false 测量的是当前 Path 状态的长度, forceClosed 为 true,则不论Path是否闭合测量的都是 Path 的闭合长度。**
-
+- 1.我们将 Path 与两个的 PathMeasure 进行关联,并给 forceClosed 设置了不同的状态,之后绘制再绘制出来的 Path 没有任何变化,所以与 Path 与 PathMeasure进行关联并不会影响 Path 状态。
+- 2.我们可以看到,设置 forceClosed 为 true 的方法比设置为 false 的方法测量出来的长度要长一点,这是由于 Path 没有闭合的缘故,多出来的距离正是 Path 最后一个点与最开始一个点之间点距离。**forceClosed 为 false 测量的是当前 Path 状态的长度, forceClosed 为 true,则不论Path是否闭合测量的都是 Path 的闭合长度。**
@@ -126,14 +122,12 @@ getLength 用于获取 Path 的总长度,在之前的测试中已经用过了
-
-
### 3.getSegment
getSegment 用于获取Path的一个片段,方法如下:
-``` java
- boolean getSegment (float startD, float stopD, Path dst, boolean startWithMoveTo)
+```java
+boolean getSegment (float startD, float stopD, Path dst, boolean startWithMoveTo)
```
方法各个参数释义:
@@ -147,8 +141,9 @@ getSegment 用于获取Path的一个片段,方法如下:
| startWithMoveTo | 起始点是否使用 moveTo | 用于保证截取的 Path 第一个点位置不变 |
>
-* 如果 startD、stopD 的数值不在取值范围 [0, getLength] 内,或者 startD == stopD 则返回值为 false,不会改变 dst 内容。
-* 如果在安卓4.4或者之前的版本,在默认开启硬件加速的情况下,更改 dst 的内容后可能绘制会出现问题,请关闭硬件加速或者给 dst 添加一个单个操作,例如: dst.rLineTo(0, 0)
+
+- 如果 startD、stopD 的数值不在取值范围 [0, getLength] 内,或者 startD == stopD 则返回值为 false,不会改变 dst 内容。
+- 如果在安卓4.4或者之前的版本,在默认开启硬件加速的情况下,更改 dst 的内容后可能绘制会出现问题,请关闭硬件加速或者给 dst 添加一个单个操作,例如: dst.rLineTo(0, 0)
我们先看看这个方法如何使用:
@@ -160,20 +155,20 @@ getSegment 用于获取Path的一个片段,方法如下:
代码:
-``` java
- canvas.translate(mViewWidth / 2, mViewHeight / 2); // 平移坐标系
+```java
+canvas.translate(mViewWidth / 2, mViewHeight / 2); // 平移坐标系
- Path path = new Path(); // 创建Path并添加了一个矩形
- path.addRect(-200, -200, 200, 200, Path.Direction.CW);
+Path path = new Path(); // 创建Path并添加了一个矩形
+path.addRect(-200, -200, 200, 200, Path.Direction.CW);
- Path dst = new Path(); // 创建用于存储截取后内容的 Path
+Path dst = new Path(); // 创建用于存储截取后内容的 Path
- PathMeasure measure = new PathMeasure(path, false); // 将 Path 与 PathMeasure 关联
+PathMeasure measure = new PathMeasure(path, false); // 将 Path 与 PathMeasure 关联
- // 截取一部分存入dst中,并使用 moveTo 保持截取得到的 Path 第一个点的位置不变
- measure.getSegment(200, 600, dst, true);
+// 截取一部分存入dst中,并使用 moveTo 保持截取得到的 Path 第一个点的位置不变
+measure.getSegment(200, 600, dst, true);
- canvas.drawPath(dst, mDeafultPaint); // 绘制 dst
+canvas.drawPath(dst, mDeafultPaint); // 绘制 dst
```
结果如下:
@@ -182,20 +177,20 @@ getSegment 用于获取Path的一个片段,方法如下:
从上图可以看到我们成功到将需要到片段截取了出来,然而当 dst 中有内容时会怎样呢?
-``` java
- canvas.translate(mViewWidth / 2, mViewHeight / 2); // 平移坐标系
+```java
+canvas.translate(mViewWidth / 2, mViewHeight / 2); // 平移坐标系
- Path path = new Path(); // 创建Path并添加了一个矩形
- path.addRect(-200, -200, 200, 200, Path.Direction.CW);
+Path path = new Path(); // 创建Path并添加了一个矩形
+path.addRect(-200, -200, 200, 200, Path.Direction.CW);
- Path dst = new Path(); // 创建用于存储截取后内容的 Path
- dst.lineTo(-300, -300); // <--- 在 dst 中添加一条线段
+Path dst = new Path(); // 创建用于存储截取后内容的 Path
+dst.lineTo(-300, -300); // <--- 在 dst 中添加一条线段
- PathMeasure measure = new PathMeasure(path, false); // 将 Path 与 PathMeasure 关联
+PathMeasure measure = new PathMeasure(path, false); // 将 Path 与 PathMeasure 关联
- measure.getSegment(200, 600, dst, true); // 截取一部分 并使用 moveTo 保持截取得到的 Path 第一个点的位置不变
+measure.getSegment(200, 600, dst, true); // 截取一部分 并使用 moveTo 保持截取得到的 Path 第一个点的位置不变
- canvas.drawPath(dst, mDeafultPaint); // 绘制 Path
+canvas.drawPath(dst, mDeafultPaint); // 绘制 Path
```
结果如下:
@@ -206,20 +201,20 @@ getSegment 用于获取Path的一个片段,方法如下:
前面两个例子中 startWithMoveTo 均为 true, 如果设置为false会怎样呢?
-``` java
- canvas.translate(mViewWidth / 2, mViewHeight / 2); // 平移坐标系
+```java
+canvas.translate(mViewWidth / 2, mViewHeight / 2); // 平移坐标系
- Path path = new Path(); // 创建Path并添加了一个矩形
- path.addRect(-200, -200, 200, 200, Path.Direction.CW);
+Path path = new Path(); // 创建Path并添加了一个矩形
+path.addRect(-200, -200, 200, 200, Path.Direction.CW);
- Path dst = new Path(); // 创建用于存储截取后内容的 Path
- dst.lineTo(-300, -300); // 在 dst 中添加一条线段
+Path dst = new Path(); // 创建用于存储截取后内容的 Path
+dst.lineTo(-300, -300); // 在 dst 中添加一条线段
- PathMeasure measure = new PathMeasure(path, false); // 将 Path 与 PathMeasure 关联
+PathMeasure measure = new PathMeasure(path, false); // 将 Path 与 PathMeasure 关联
- measure.getSegment(200, 600, dst, false); // <--- 截取一部分 不使用 startMoveTo, 保持 dst 的连续性
+measure.getSegment(200, 600, dst, false); // <--- 截取一部分 不使用 startMoveTo, 保持 dst 的连续性
- canvas.drawPath(dst, mDeafultPaint); // 绘制 Path
+canvas.drawPath(dst, mDeafultPaint); // 绘制 Path
```
结果如下:
@@ -237,8 +232,6 @@ getSegment 用于获取Path的一个片段,方法如下:
-
-
### 4.nextContour
我们知道 Path 可以由多条曲线构成,但不论是 getLength , getgetSegment 或者是其它方法,都只会在其中第一条线段上运行,而这个 `nextContour` 就是用于跳转到下一条曲线到方法,_如果跳转成功,则返回 true, 如果跳转失败,则返回 false。_
@@ -249,49 +242,47 @@ getSegment 用于获取Path的一个片段,方法如下:
代码:
-``` java
- canvas.translate(mViewWidth / 2, mViewHeight / 2); // 平移坐标系
+```java
+canvas.translate(mViewWidth / 2, mViewHeight / 2); // 平移坐标系
+
+Path path = new Path();
- Path path = new Path();
+path.addRect(-100, -100, 100, 100, Path.Direction.CW); // 添加小矩形
+path.addRect(-200, -200, 200, 200, Path.Direction.CW); // 添加大矩形
- path.addRect(-100, -100, 100, 100, Path.Direction.CW); // 添加小矩形
- path.addRect(-200, -200, 200, 200, Path.Direction.CW); // 添加大矩形
+canvas.drawPath(path,mDeafultPaint); // 绘制 Path
- canvas.drawPath(path,mDeafultPaint); // 绘制 Path
-
- PathMeasure measure = new PathMeasure(path, false); // 将Path与PathMeasure关联
+PathMeasure measure = new PathMeasure(path, false); // 将Path与PathMeasure关联
- float len1 = measure.getLength(); // 获得第一条路径的长度
+float len1 = measure.getLength(); // 获得第一条路径的长度
- measure.nextContour(); // 跳转到下一条路径
+measure.nextContour(); // 跳转到下一条路径
- float len2 = measure.getLength(); // 获得第二条路径的长度
+float len2 = measure.getLength(); // 获得第二条路径的长度
- Log.i("LEN","len1="+len1); // 输出两条路径的长度
- Log.i("LEN","len2="+len2);
+Log.i("LEN","len1="+len1); // 输出两条路径的长度
+Log.i("LEN","len2="+len2);
```
log输出结果:
-```
-05-30 02:00:33.899 19879-19879/com.gcssloop.canvas I/LEN: len1=800.0
-05-30 02:00:33.899 19879-19879/com.gcssloop.canvas I/LEN: len2=1600.0
+
+```shell
+com.gcssloop.canvas I/LEN: len1=800.0
+com.gcssloop.canvas I/LEN: len2=1600.0
```
通过测试,我们可以得到以下内容:
-* 1.曲线的顺序与 Path 中添加的顺序有关。
-* 2.getLength 获取到到是当前一条曲线分长度,而不是整个 Path 的长度。
-* 3.getLength 等方法是针对当前的曲线(其它方法请自行验证)。
-
-
-
-
+- 1.曲线的顺序与 Path 中添加的顺序有关。
+- 2.getLength 获取到到是当前一条曲线分长度,而不是整个 Path 的长度。
+- 3.getLength 等方法是针对当前的曲线(其它方法请自行验证)。
#### 5.getPosTan
这个方法是用于得到路径上某一长度的位置以及该位置的正切值:
-``` java
- boolean getPosTan (float distance, float[] pos, float[] tan)
+
+```java
+boolean getPosTan (float distance, float[] pos, float[] tan)
```
方法各个参数释义:
@@ -300,88 +291,140 @@ log输出结果:
| ------------ | ------------- | ---------------------------------------- |
| 返回值(boolean) | 判断获取是否成功 | true表示成功,数据会存入 pos 和 tan 中,
false 表示失败,pos 和 tan 不会改变 |
| distance | 距离 Path 起点的长度 | 取值范围: 0 <= distance <= getLength |
-| pos | 该点的坐标值 | 坐标值: (x==[0], y==[1]) |
-| tan | 该点的正切值 | 正切值: (x==[0], y==[1]) |
+| pos | 该点的坐标值 | 当前点在画布上的位置,有两个数值,分别为x,y坐标。 |
+| tan | 该点的正切值 | 当前点在曲线上的方向,使用 Math.atan2(tan[1], tan[0]) 获取到正切角的弧度值。 |
这个方法也不难理解,除了其中 `tan` 这个东东,这个东西是干什么的呢?
-`tan` 是用来判断 Path 的趋势的,即在这个位置上曲线的走向,请看下图示例,注意箭头的方向:
+`tan` 是用来判断 Path 上趋势的,即在这个位置上曲线的走向,请看下图示例,注意箭头的方向:

**[点击这里下载箭头图片](http://ww1.sinaimg.cn/large/005Xtdi2jw1f4gam21ktoj3069069jre.jpg)**
-可以看到 上图中箭头在沿着 Path 运动时,方向始终与 Path 走向保持一致,下面我们来看看代码是如何实现的:
+可以看到 上图中箭头在沿着 Path 运动时,方向始终与 Path 走向保持一致,保持方向主要就是依靠 `tan` 。
-首先我们需要定义几个必要的变量:
+下面我们来看看代码是如何实现的,首先我们需要定义几个必要的变量:
-``` java
- private float currentValue = 0; // 用于纪录当前的位置,取值范围[0,1]映射Path的整个长度
+```java
+private float currentValue = 0; // 用于纪录当前的位置,取值范围[0,1]映射Path的整个长度
- private float[] pos; // 当前点的实际位置
- private float[] tan; // 当前点的tangent值,用于计算图片所需旋转的角度
- private Bitmap mBitmap; // 箭头图片
- private Matrix mMatrix; // 矩阵,用于对图片进行一些操作
+private float[] pos; // 当前点的实际位置
+private float[] tan; // 当前点的tangent值,用于计算图片所需旋转的角度
+private Bitmap mBitmap; // 箭头图片
+private Matrix mMatrix; // 矩阵,用于对图片进行一些操作
```
初始化这些变量(在构造函数中调用这个方法):
-``` java
- private void init(Context context) {
- pos = new float[2];
- tan = new float[2];
- BitmapFactory.Options options = new BitmapFactory.Options();
- options.inSampleSize = 2; // 缩放图片
- mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.arrow, options);
- mMatrix = new Matrix();
- }
+```java
+private void init(Context context) {
+ pos = new float[2];
+ tan = new float[2];
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = 2; // 缩放图片
+ mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.arrow, options);
+ mMatrix = new Matrix();
+}
```
具体绘制:
-``` java
+```java
+canvas.translate(mViewWidth / 2, mViewHeight / 2); // 平移坐标系
- canvas.translate(mViewWidth / 2, mViewHeight / 2); // 平移坐标系
+Path path = new Path(); // 创建 Path
- Path path = new Path(); // 创建 Path
+path.addCircle(0, 0, 200, Path.Direction.CW); // 添加一个圆形
- path.addCircle(0, 0, 200, Path.Direction.CW); // 添加一个圆形
+PathMeasure measure = new PathMeasure(path, false); // 创建 PathMeasure
- PathMeasure measure = new PathMeasure(path, false); // 创建 PathMeasure
+currentValue += 0.005; // 计算当前的位置在总长度上的比例[0,1]
+if (currentValue >= 1) {
+ currentValue = 0;
+}
- currentValue += 0.005; // 计算当前的位置在总长度上的比例[0,1]
- if (currentValue >= 1) {
- currentValue = 0;
- }
+measure.getPosTan(measure.getLength() * currentValue, pos, tan); // 获取当前位置的坐标以及趋势
- measure.getPosTan(measure.getLength() * currentValue, pos, tan); // 获取当前位置的坐标以及趋势
+mMatrix.reset(); // 重置Matrix
+float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI); // 计算图片旋转角度
- mMatrix.reset(); // 重置Matrix
- float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI); // 计算图片旋转角度
+mMatrix.postRotate(degrees, mBitmap.getWidth() / 2, mBitmap.getHeight() / 2); // 旋转图片
+mMatrix.postTranslate(pos[0] - mBitmap.getWidth() / 2, pos[1] - mBitmap.getHeight() / 2); // 将图片绘制中心调整到与当前点重合
- mMatrix.postRotate(degrees, mBitmap.getWidth() / 2, mBitmap.getHeight() / 2); // 旋转图片
- mMatrix.postTranslate(pos[0] - mBitmap.getWidth() / 2, pos[1] - mBitmap.getHeight() / 2); // 将图片绘制中心调整到与当前点重合
+canvas.drawPath(path, mDeafultPaint); // 绘制 Path
+canvas.drawBitmap(mBitmap, mMatrix, mDeafultPaint); // 绘制箭头
- canvas.drawPath(path, mDeafultPaint); // 绘制 Path
- canvas.drawBitmap(mBitmap, mMatrix, mDeafultPaint); // 绘制箭头
-
- invalidate(); // 重绘页面
+invalidate(); // 重绘页面
```
**核心要点:**
>
-* 1.**通过 `tan` 得值计算出图片旋转的角度**,tan 是 tangent 的缩写,即中学中常见的正切, 其中tan[0](x)是邻边边长,tan[1](y)是对边边长,而Math中 `atan2` 方法是根据正切是数值计算出该角度的大小,得到的单位是弧度,所以上面又将弧度转为了角度。
-* 2.**通过 `Matrix` 来设置图片对旋转角度和位移**,这里使用的方法与前面讲解过对 canvas操作 有些类似,对于 `Matrix` 会在后面专一进行讲解,敬请期待。
-* 3.**页面刷新**,页面刷新此处是在 onDraw 里面调用了 invalidate 方法来保持界面不断刷新,但并不提倡这么做,正确对做法应该是使用 线程 或者 ValueAnimator 来控制界面的刷新,关于控制页面刷新这一部分会在后续的 动画部分 详细讲解,同样敬请期待。
+- 1.**通过 `tan` 得值计算出图片旋转的角度**,tan 是 tangent 的缩写,即中学中常见的正切, 其中tan[0]是邻边边长,tan[1]是对边边长,而Math中 `atan2` 方法是根据正切是数值计算出该角度的大小,得到的单位是弧度(取值范围是 -pi 到 pi),所以上面又将弧度转为了角度。
+- 2.**通过 `Matrix` 来设置图片对旋转角度和位移**,这里使用的方法与前面讲解过对 canvas操作 有些类似,对于 `Matrix` 会在后面专一进行讲解,敬请期待。
+- 3.**页面刷新**,页面刷新此处是在 onDraw 里面调用了 invalidate 方法来保持界面不断刷新,但并不提倡这么做,正确对做法应该是使用 线程 或者 ValueAnimator 来控制界面的刷新,关于控制页面刷新这一部分会在后续的 动画部分 详细讲解,同样敬请期待。
+
+关于`tan`这个参数有很多魔法师不理解,特此拉出来详述一下,`tan` 在数学中被称为正切,在直角三角形中,一个锐角的**正切**定义为它的对边(Opposite side)与邻边(Adjacent side)的比值(来自维基百科):
+
+
+
+我们此处用 `tan` 来描述 Path 上某一点的切线方向,**主要用了两个数值 tan[0] 和 tan[1] 来描述这个切线的方向(切线方向与x轴夹角)** ,看上面公式可知 `tan` 既可以用 `对边/邻边` 来表述,也可以用 `sin/cos` 来表述,此处用两种理解方式均可以(**注意下面等价关系**):
+
+> **tan[0] = cos = 邻边(单位圆x坐标)**
+> **tan[1] = sin = 对边(单位圆y坐标)**
+
+
+
+**以 `sin/cos`理解:**
+
+
+
+
+
+在圆上最右侧点的切线方向向下(动图中小飞机朝向和切线朝向一致),切线角度为90度.
+sin90 = 1,cos90 = 0
+tan[0] = cos = 0
+tan[1] = sin = 1
+
+
+
+**以 `对边/邻边` 理解(单位圆上坐标):**
+
+按照这种理解方式需要借助一个单位圆,单位圆上任意一点到圆心到距离均为 1,以下图30度为例:
+
+
+
+tan30 = 对边/邻边 = AB/OA = B点y坐标/B点x坐标
+
+> **另外根据单位圆性质同样可以证得:**
+> sin30 = 对边/斜边 = AB/OB = AB = B点y坐标 (单位圆边上任意一点距离圆心距离均为1,故OB = 1)
+> cos30 = 邻边/斜边 = OA/OB = OA = B点x坐标
+>
+> **化为通用公式即为:**
+> sin = 该角度在单位圆上对应点的y坐标
+> cos = 该角度在单位圆上对应点的x坐标
+>
+> 即 tan = sin/cos = y/x
+> tan[0] = x
+> tan[1] = y
+>
+> 另外注意,这个单位圆与小飞机路径没有半毛钱关系,例如上一个例子中的90度切线,不要在单位圆上找对应位置,**要找对应角度的位置,90度对应的位置是(0,1)**,所以:
+> tan[0] = x = 0
+> tan[1] = y = 1
+>
+> 其实绕来绕去全是等价的 (╯°Д°)╯︵ ┻━┻
+
+**PS: 使用 Math.atan2(tan[1], tan[0]) 将 `tan` 转化为角(单位为弧度)的时候要注意参数顺序。**
### 6.getMatrix
这个方法是用于得到路径上某一长度的位置以及该位置的正切值的矩阵:
-``` java
+
+```java
boolean getMatrix (float distance, Matrix matrix, int flags)
```
@@ -399,6 +442,7 @@ boolean getMatrix (float distance, Matrix matrix, int flags)
但是我们看到最后到 `flags` 选项可以选择 `位置` 或者 `正切` ,如果我们两个选项都想选择怎么办?
如果两个选项都想选择,可以将两个选项之间用 `|` 连接起来,如下:
+
```
measure.getMatrix(distance, matrix, PathMeasure.TANGENT_MATRIX_FLAG | PathMeasure.POSITION_MATRIX_FLAG);
```
@@ -407,40 +451,40 @@ measure.getMatrix(distance, matrix, PathMeasure.TANGENT_MATRIX_FLAG | PathMeasur
具体绘制:
-``` java
- Path path = new Path(); // 创建 Path
+```java
+Path path = new Path(); // 创建 Path
- path.addCircle(0, 0, 200, Path.Direction.CW); // 添加一个圆形
+path.addCircle(0, 0, 200, Path.Direction.CW); // 添加一个圆形
- PathMeasure measure = new PathMeasure(path, false); // 创建 PathMeasure
+PathMeasure measure = new PathMeasure(path, false); // 创建 PathMeasure
- currentValue += 0.005; // 计算当前的位置在总长度上的比例[0,1]
- if (currentValue >= 1) {
- currentValue = 0;
- }
+currentValue += 0.005; // 计算当前的位置在总长度上的比例[0,1]
+if (currentValue >= 1) {
+ currentValue = 0;
+}
- // 获取当前位置的坐标以及趋势的矩阵
- measure.getMatrix(measure.getLength() * currentValue, mMatrix, PathMeasure.TANGENT_MATRIX_FLAG | PathMeasure.POSITION_MATRIX_FLAG);
-
- mMatrix.preTranslate(-mBitmap.getWidth() / 2, -mBitmap.getHeight() / 2); // <-- 将图片绘制中心调整到与当前点重合(注意:此处是前乘pre)
+// 获取当前位置的坐标以及趋势的矩阵
+measure.getMatrix(measure.getLength() * currentValue, mMatrix, PathMeasure.TANGENT_MATRIX_FLAG | PathMeasure.POSITION_MATRIX_FLAG);
- canvas.drawPath(path, mDeafultPaint); // 绘制 Path
- canvas.drawBitmap(mBitmap, mMatrix, mDeafultPaint); // 绘制箭头
+mMatrix.preTranslate(-mBitmap.getWidth() / 2, -mBitmap.getHeight() / 2); // <-- 将图片绘制中心调整到与当前点重合(注意:此处是前乘pre)
- invalidate(); // 重绘页面
+canvas.drawPath(path, mDeafultPaint); // 绘制 Path
+canvas.drawBitmap(mBitmap, mMatrix, mDeafultPaint); // 绘制箭头
+
+invalidate(); // 重绘页面
```
> 由于此处代码运行结果与上面一样,便不再贴图片了,请参照上面一个示例的效果图。
可以看到使用 getMatrix 方法的确可以节省一些代码,不过这里依旧需要注意一些内容:
->
-* 1.对 `matrix` 的操作必须要在 `getMatrix` 之后进行,否则会被 `getMatrix` 重置而导致无效。
-* 2.矩阵对旋转角度默认为图片的左上角,我们此处需要使用 `preTranslate` 调整为图片中心。
-* 3.pre(矩阵前乘) 与 post(矩阵后乘) 的区别,此处请等待后续的文章或者自行搜索。
+>
-*****
+- 1.对 `matrix` 的操作必须要在 `getMatrix` 之后进行,否则会被 `getMatrix` 重置而导致无效。
+- 2.矩阵对旋转角度默认为图片的左上角,我们此处需要使用 `preTranslate` 调整为图片中心。
+- 3.pre(矩阵前乘) 与 post(矩阵后乘) 的区别,此处请等待后续的文章或者自行搜索。
+------
## Path & SVG
@@ -455,20 +499,18 @@ Path 和 SVG 结合通常能诞生出一些奇妙的东西,如下:


->
->**该图片来自这个开源库 ->[PathView](https://github.com/geftimov/android-pathview)**
->**SVG 转 Path 的解析可以用这个库 -> [AndroidSVG](https://bigbadaboom.github.io/androidsvg/)**
+> **该图片来自这个开源库 ->[PathView](https://github.com/geftimov/android-pathview)**
+> **SVG 转 Path 的解析可以用这个库 -> [AndroidSVG](https://bigbadaboom.github.io/androidsvg/)**
限于篇幅以及本人精力,这一部分就暂不详解了,感兴趣的可以直接看源码,或者搜索一些相关的解析文章。
-*****
+------
## Path使用技巧
**话说本篇文章的名字不是叫 玩出花样么?怎么只见前面啰啰嗦嗦的扯了一大堆不明所以的东西,花样在哪里?**
->
->**前面的内容虽然啰嗦繁杂,但却是重中之重的基础,如果在修仙界,这叫根基,而下面讲述的内容的是招式,有了根基才能演化出千变万化的招式,而没有根基只学招式则是徒有其表,只能学一样会一样,很难适应千变万化的需求。**
+> **前面的内容虽然啰嗦繁杂,但却是重中之重的基础,如果在修仙界,这叫根基,而下面讲述的内容的是招式,有了根基才能演化出千变万化的招式,而没有根基只学招式则是徒有其表,只能学一样会一样,很难适应千变万化的需求。**
先放一个效果图,然后分析一下实现过程:
@@ -476,12 +518,12 @@ Path 和 SVG 结合通常能诞生出一些奇妙的东西,如下:
这是一个搜索的动效图,通过分析可以得到它应该有四种状态,分别如下:
-| 状态 | 概述 |
-| ---- | --------------------------- |
-| 初始状态 | 初始状态,没有任何动效,只显示一个搜索标志 :mag: |
-| 准备搜索 | 放大镜图标逐渐变化为一个点 |
-| 正在搜索 | 围绕这一个圆环运动,并且线段长度会周期性变化 |
-| 准备结束 | 从一个点逐渐变化成为放大镜图标 |
+| 状态 | 概述 |
+| ---- | ------------------------ |
+| 初始状态 | 初始状态,没有任何动效,只显示一个搜索标志 🔍 |
+| 准备搜索 | 放大镜图标逐渐变化为一个点 |
+| 正在搜索 | 围绕这一个圆环运动,并且线段长度会周期性变化 |
+| 准备结束 | 从一个点逐渐变化成为放大镜图标 |
这些状态是有序转换的,转换流程以及转换条件如下:
@@ -525,28 +567,22 @@ Path 和 SVG 结合通常能诞生出一些奇妙的东西,如下:
> PS: 本代码仅作为示例使用,还有诸多不足,如 自定义属性,视图大小, 点击事件, 监听回调 等,并不适合直接使用,有需要的可以自行补足相关内容。
-
## 总结
**本文中虽然后面的内容看起来比较高大上一点,但前面"啰嗦"的废话才是真正的干货,把前面的东西学会了,后面的各种效果都能信手拈来,如果只研究后面的东西,则是取其形,而难以会其意。**
#### PS: 由于本人水平有限,某些地方可能存在误解或不准确,如果你对此有疑问可以提交Issues进行反馈。
-## About Me
+## About
-### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop)
+[本系列相关文章](http://www.gcssloop.com/customview/CustomViewIndex/)
-
+作者微博: [GcsSloop](http://weibo.com/GcsSloop)
## 参考资料
+
[PathMeasure](https://developer.android.com/reference/android/graphics/PathMeasure.html)
[AndroidSVG](https://bigbadaboom.github.io/androidsvg/)
[android-pathview](https://github.com/geftimov/android-pathview)
[android Path 和 PathMeasure 进阶](http://blog.csdn.net/cquwentao/article/details/51436852)
-[]()
-
-
-
-
-
From c6d292abb776c2a4cd68dea37eaa7ced699f3469 Mon Sep 17 00:00:00 2001
From: sloop
Date: Wed, 18 Jan 2017 20:02:49 +0800
Subject: [PATCH 099/160] Update
---
README.md | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/README.md b/README.md
index 3b7c74ac..0f783914 100644
--- a/README.md
+++ b/README.md
@@ -132,6 +132,11 @@
* 商业用途请点击最下面图片联系本人。
* 微信公众号转载一律不授权 `原创` 标志。
+## 交流群
+
+QQ群:612310796
+微信群:加我个人微信 GcsSloop,备注加群。
+
### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop)
From 6d7d1fd557912fd667bba0bbdef2fa443d1edf4d Mon Sep 17 00:00:00 2001
From: sloop
Date: Wed, 18 Jan 2017 20:03:14 +0800
Subject: [PATCH 100/160] Update
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 0f783914..1209fb05 100644
--- a/README.md
+++ b/README.md
@@ -134,7 +134,7 @@
## 交流群
-QQ群:612310796
+QQ群:612310796
微信群:加我个人微信 GcsSloop,备注加群。
### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop)
From 5e3feb1716ce61de35d05a8fc2f711a278194cb9 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Thu, 19 Jan 2017 23:49:35 +0800
Subject: [PATCH 101/160] Update
---
CustomView/Advance/[18]multi-touch.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CustomView/Advance/[18]multi-touch.md b/CustomView/Advance/[18]multi-touch.md
index ff59eacc..651fce26 100644
--- a/CustomView/Advance/[18]multi-touch.md
+++ b/CustomView/Advance/[18]multi-touch.md
@@ -568,7 +568,7 @@ public class DragView extends CustomView {
}
```
-可以看到,比起上一个版本并么有多多少代码,但是更加“智能”了,可以准确识别某一个手指,不会因为手指抬起而认错手指。
+可以看到,比起上一个版本,只添加了少量代码,就变得更加“智能”了,可以准确识别某一个手指,不会因为手指抬起而认错手指。

From db51d37e10378f4a1be27f8a1242491bd3585288 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Fri, 20 Jan 2017 00:10:39 +0800
Subject: [PATCH 102/160] Update
---
CustomView/Advance/[18]multi-touch.md | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/CustomView/Advance/[18]multi-touch.md b/CustomView/Advance/[18]multi-touch.md
index 651fce26..9e6a7f28 100644
--- a/CustomView/Advance/[18]multi-touch.md
+++ b/CustomView/Advance/[18]multi-touch.md
@@ -1,4 +1,4 @@
-# 多点触控详解
+# Android 多点触控详解
Android 多点触控详解,在前面的几篇文章中我们大致了解了 Android 中的事件处理流程和一些简单的处理方案,本次带大家了解 Android 多点触控相关的一些知识。
@@ -48,7 +48,7 @@ Android 多点触控详解,在前面的几篇文章中我们大致了解了 An
在引入多点触控之前,事件的类型很少,基本事件类型只有按下(down)、移动(move) 和 抬起(up),即便加上那些特殊的事件类型也只有几种而已,所以我们可以用几个常量来标记这些事件,在使用的时候使用 `getAction()` 方法来获取具体的事件,之后和这些常量进行对比就行了。
-在 Android 2.0 版本的时候,开始引入多点触控技术,由于技术上并不成熟,硬件和驱动也跟不上,很数设备只能支持追踪两三个点而已,因此在设计 API 上采取了一种简单粗暴的方案,添加了几个常量用语多点触控的事件类型的判断。
+在 Android 2.0 版本的时候,开始引入多点触控技术,由于技术上并不成熟,硬件和驱动也跟不上,多数设备只能支持追踪两三个点而已,因此在设计 API 上采取了一种简单粗暴的方案,添加了几个常量用语多点触控的事件类型的判断。
| 事件 | 简介 |
| ----------------------- | -------------------- |
@@ -243,17 +243,17 @@ switch (event.getActionMasked()) {
| 第4个手指按下 | ACTION_POINTER_DOWN (0x0000**01**05) |
| **第3个手指抬起** | ACTION_POINTER_UP (0x0000**02**06) |
-这个要和上一个对比这看,**重点观察第 3 个手指所触发事件区别**,在上一个示例中,随着第 2 个手指的抬起,第 3 个手指变化为第 2 个,所以抬起时触发的是第 2 根手指的抬起事件(删除线部分)。
+这个要和上一个对比这看,**重点观察第 3 个手指所触发事件区别**,在上一个示例中,随着第 2 个手指的抬起,第 3 个手指变化为第 2(01) 个,所以抬起时触发的是第 2 根手指的抬起事件(删除线部分)。
-但是,如果第 2 个手指抬起后,落在屏幕上另外一个手指会怎样?经过测试,发现另外**落下的手指会替代之前第 2 个手指的位置,系统判定为 2,而不是顺延下去变成 3**,但是如果继续落下其他的手指,数值则会顺延。
+但是,如果第 2 个手指抬起后,落在屏幕上另外一个手指会怎样?经过测试,发现另外**落下的手指会替代之前第 2 个手指的位置,系统判定为 2(01),而不是顺延下去变成 3(02),并且原本第3个手指的index变为原来数值(02)**,但是如果继续落下其他的手指,数值则会顺延。
-**即手指抬起时的 Index 会趋向于和按下时相同,虽然在手指数量不足时,Index 会变小,但是当手指变多时,Index 会保持和按下时一样。**
+**即手指抬起时的 Index 会趋向于和按下时相同,虽然在手指数量不足时,Index 会变小,但是当手指变多时,Index 会趋向于保持和按下时一样。**
> PS:由于程序是从0开始计数的,所以 0 就是 1, 1 就是 2 ...
#### 3.4、对 move 事件无效。
-这个也比较容易理解,我们所取得的 Index 属性实际上是从事件上分离下来的,但是 move 事件始终为 0x0000**00**02,也就是说,在 move 时不论你移动哪个手指,使用 ` getActionIndex()` 获取到的始终是数值 0。
+这个也比较容易理解,我们所取得的 Index 属性实际上是从事件上分离下来的,但是 move 事件始终为 0x0000**00**02,也就是说,在 move 时不论你移动哪个手指,使用 `getActionIndex()` 获取到的始终是数值 0。
既然 move 事件无法用事件索引(Index)区别,那么该如何区分 move 是那个手指发出的呢?这就要用到 pointId 了,**pointId 和 index 最大的区别就是 pointId 是不变的,始终为第一次落下时生成的数值,不会受到其他手指抬起和落下的影响。**
@@ -410,7 +410,7 @@ public class MultiTouchTest extends CustomView {
举一个简单的例子:
-如果我们需要一个**可以用单指拖动的图片**。加入我们不进行多指触控的判断,像下面这样:
+如果我们需要一个**可以用单指拖动的图片**。假如我们不进行多指触控的判断,像下面这样:
**没有针对多指触控处理版本:**
@@ -610,4 +610,4 @@ public class DragView extends CustomView {
-[motionevent]: http://www.gcssloop.com/customview/motionevent
+[motionevent]: http://www.gcssloop.com/customview/motionevent
\ No newline at end of file
From e22012820ffc2dc5f35edb0c91a508bf5b51df6b Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Sun, 22 Jan 2017 22:56:01 +0800
Subject: [PATCH 103/160] Update
---
SourceAnalysis/CircularArray.md | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 SourceAnalysis/CircularArray.md
diff --git a/SourceAnalysis/CircularArray.md b/SourceAnalysis/CircularArray.md
new file mode 100644
index 00000000..e69de29b
From 44b36f050c73a988bdfd915a34e87feed7f6bb5e Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Mon, 23 Jan 2017 03:06:30 +0800
Subject: [PATCH 104/160] Update
---
CustomView/Advance/[18]multi-touch.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CustomView/Advance/[18]multi-touch.md b/CustomView/Advance/[18]multi-touch.md
index 9e6a7f28..2f7c00f9 100644
--- a/CustomView/Advance/[18]multi-touch.md
+++ b/CustomView/Advance/[18]multi-touch.md
@@ -48,7 +48,7 @@ Android 多点触控详解,在前面的几篇文章中我们大致了解了 An
在引入多点触控之前,事件的类型很少,基本事件类型只有按下(down)、移动(move) 和 抬起(up),即便加上那些特殊的事件类型也只有几种而已,所以我们可以用几个常量来标记这些事件,在使用的时候使用 `getAction()` 方法来获取具体的事件,之后和这些常量进行对比就行了。
-在 Android 2.0 版本的时候,开始引入多点触控技术,由于技术上并不成熟,硬件和驱动也跟不上,多数设备只能支持追踪两三个点而已,因此在设计 API 上采取了一种简单粗暴的方案,添加了几个常量用语多点触控的事件类型的判断。
+在 Android 2.0 版本的时候,开始引入多点触控技术,由于技术上并不成熟,硬件和驱动也跟不上,多数设备只能支持追踪两三个点而已,因此在设计 API 上采取了一种简单粗暴的方案,添加了几个常量用于多点触控的事件类型的判断。
| 事件 | 简介 |
| ----------------------- | -------------------- |
From 7fc821bd87e2bc51ec98e4af556473ba773a7744 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Tue, 24 Jan 2017 03:08:50 +0800
Subject: [PATCH 105/160] Update
---
CustomView/Advance/[18]multi-touch.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CustomView/Advance/[18]multi-touch.md b/CustomView/Advance/[18]multi-touch.md
index 2f7c00f9..98efa326 100644
--- a/CustomView/Advance/[18]multi-touch.md
+++ b/CustomView/Advance/[18]multi-touch.md
@@ -95,7 +95,7 @@ switch (event.getAction()) {
注意观察上面编号和id的变化,有两个问题,**1、B手指的编号变化了。2、A手指和C手指id是相同的(A手指抬起后,C手指按下替代了A手指)。**所以这就引出了一个问题:如果存在 ACTION_POINTER_X_MOVE,那么X应该用什么标志呢?编号会变化,id虽然不会变化,但id会被复用,例如A手指抬起后C手指按下,C手指复用了A手指的id。所以不论使用哪一个都不能保证唯一性。
-当然了,解决问题最好的方式就是把问题抛出去,既然从硬件和软件上都不能保证唯一性和不变性,就不做区分了,因此所有的 move 事件都是 `ACTION_MOVE`, 具体是哪个手指产生的 move 用户可以根据其他事件(按下和抬起)来综合判断。
+当然了,解决问题最好的方式就是把问题抛出去,既然从硬件和软件上都不能保证唯一性和不变性,就不做区分了,因此所有的 move 事件都是 `ACTION_MOVE`, 具体是哪个手指产生的 move 用户可以结合其他事件(按下和抬起)来综合判断。
### 2.超过4个手指怎么办?
From 7393790c08773400e51db7ac4d84200e32705187 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Wed, 25 Jan 2017 16:52:36 +0800
Subject: [PATCH 106/160] Update
---
Lecture/README.md | 3 +++
1 file changed, 3 insertions(+)
diff --git a/Lecture/README.md b/Lecture/README.md
index adeb5d56..959ec327 100644
--- a/Lecture/README.md
+++ b/Lecture/README.md
@@ -1 +1,4 @@
# 演讲稿
+
+* [程序员练级指北(郑州GDG-2016DevFest)](https://github.com/GcsSloop/AndroidNote/blob/master/Lecture/gdg-developer-growth-guide.md)
+
From d4d8dfb817bb481f9237c5f378844dadbf6e55e7 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Thu, 26 Jan 2017 16:53:47 +0800
Subject: [PATCH 107/160] Update
---
CustomView/CustomViewRule.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CustomView/CustomViewRule.md b/CustomView/CustomViewRule.md
index e69de29b..4706e9d9 100644
--- a/CustomView/CustomViewRule.md
+++ b/CustomView/CustomViewRule.md
@@ -0,0 +1,2 @@
+# 自定义View基本法
+
From 466efa349b6344ecf3b1b7ee48c905c03a11a944 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Thu, 26 Jan 2017 16:55:23 +0800
Subject: [PATCH 108/160] Update
---
CustomView/README.md | 38 +++++++++++++++++++-------------------
1 file changed, 19 insertions(+), 19 deletions(-)
diff --git a/CustomView/README.md b/CustomView/README.md
index 6fefef4f..cf5a50ac 100644
--- a/CustomView/README.md
+++ b/CustomView/README.md
@@ -5,52 +5,52 @@
## 基础篇
-
-
-
+
+
+
*******
## 进阶篇
-
-
-
+
+
+
*******
-
-
-
+
+
+
*******
-
-
-
+
+
+
*******
-
-
-
+
+
+
*******
-
-
-
+
+
+
### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop)
-
+
From 9cbf2eaf6d35c2a68dc2d809998d8d90b9d28fdb Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Fri, 27 Jan 2017 16:57:41 +0800
Subject: [PATCH 109/160] Update
---
Course/README.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/Course/README.md b/Course/README.md
index b9fd97f3..d2e3d8c9 100644
--- a/Course/README.md
+++ b/Course/README.md
@@ -2,7 +2,7 @@
-
-
-
+
+
+
From 63a64d4c345b4d7596bc5eb34b1a2b8c1953cd79 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Sat, 28 Jan 2017 17:00:22 +0800
Subject: [PATCH 110/160] Update
---
README.md | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/README.md b/README.md
index 1209fb05..a338c9e8 100644
--- a/README.md
+++ b/README.md
@@ -97,6 +97,12 @@
* [LeafLoading - 进度条](https://github.com/GcsSloop/LeafLoading)
* [Rotate3dAnimation - 3D旋转动画(修正版)](https://github.com/GcsSloop/Rotate3dAnimation)
+------
+
+## 源码解析
+
+- [AtomicFile 源码解析](https://github.com/GcsSloop/AndroidNote/blob/master/SourceAnalysis/AtomicFile.md)
+
## 传送门
通往异世界的传送门,请谨慎使用。
From fd9c12257968eae90a8fe76b776a137e2d85f397 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Mon, 30 Jan 2017 06:47:56 +0800
Subject: [PATCH 111/160] Update
---
CustomView/CustomViewRule.md | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/CustomView/CustomViewRule.md b/CustomView/CustomViewRule.md
index 4706e9d9..d6ff9419 100644
--- a/CustomView/CustomViewRule.md
+++ b/CustomView/CustomViewRule.md
@@ -1,2 +1,17 @@
# 自定义View基本法
+我们使用手机,是想要获取某些信息,而 View 是这些信息的直接展示界面,因为信息种类繁多,为了更好的展示这些信息, View 也必须有多种多样,Android 系统本身就给我们提供了不少类型的 View,但有时仍不能满足我们的需要,所以有时可能需要自定义 View 来完成任务。
+
+自定义 View 有许多需要注意的地方,关于这些需要注意的内容,我都会整理在这里,其名为《自定义 View 基本法》。
+
+#### 第一条:尽量避免自定义 View。
+
+由于 View 直接承载了与用户交互的重任,所以必须要考虑到各种情况,例如:
+
+* 当没有设置宽高属性时,View 默认应该多大。
+* 横竖屏转换时 View 可能重新设定大小,此时应如何处理。
+* View 因为特殊情况被销毁后重建,应如何保存和恢复数据。
+
+由于某些情况很特殊,触发条件也特殊,我们简单的实现了一个自定义了一个 View,可能在 99% 的情况下都是正常的,但在某些特殊情况下就会出问题。
+
+但系统提供给我们的 View 都是经过千锤百炼的,基本上考虑到了各种特殊情况的处理,所以通常情况下,系统提供给我们的组件稳定性要好一些。所以我的建议是,能使用系统提供的组件的尽量使用系统的。
\ No newline at end of file
From fa3f292901ab2c4556bdb34737a8de8f492c7715 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Tue, 31 Jan 2017 06:52:22 +0800
Subject: [PATCH 112/160] Update
---
CustomView/CustomViewRule.md | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/CustomView/CustomViewRule.md b/CustomView/CustomViewRule.md
index d6ff9419..ddc55c14 100644
--- a/CustomView/CustomViewRule.md
+++ b/CustomView/CustomViewRule.md
@@ -14,4 +14,9 @@
由于某些情况很特殊,触发条件也特殊,我们简单的实现了一个自定义了一个 View,可能在 99% 的情况下都是正常的,但在某些特殊情况下就会出问题。
-但系统提供给我们的 View 都是经过千锤百炼的,基本上考虑到了各种特殊情况的处理,所以通常情况下,系统提供给我们的组件稳定性要好一些。所以我的建议是,能使用系统提供的组件的尽量使用系统的。
\ No newline at end of file
+但系统提供给我们的 View 都是经过千锤百炼的,基本上考虑到了各种特殊情况的处理,所以通常情况下,系统提供给我们的组件稳定性要好一些。所以我的建议是,能使用系统提供的组件的尽量使用系统的。
+
+#### 第二条:尽量避免从头开始。
+
+如果一定要使用自定义 View,那么尽量去继承系统已有的组件,并重写其中的部分方法,不要自己从头开始写。例如:图像相关的 View 可以考虑继承 ImageView,容器类 View 可以考虑继承 LinerLayout,RelativeLayout 等,原因同上。
+
From 1e4fb2cb504caf05edac0f8bcb15e63a99910e50 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Wed, 1 Feb 2017 06:55:21 +0800
Subject: [PATCH 113/160] Update
---
CustomView/CustomViewRule.md | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/CustomView/CustomViewRule.md b/CustomView/CustomViewRule.md
index ddc55c14..8b0a2873 100644
--- a/CustomView/CustomViewRule.md
+++ b/CustomView/CustomViewRule.md
@@ -14,9 +14,12 @@
由于某些情况很特殊,触发条件也特殊,我们简单的实现了一个自定义了一个 View,可能在 99% 的情况下都是正常的,但在某些特殊情况下就会出问题。
-但系统提供给我们的 View 都是经过千锤百炼的,基本上考虑到了各种特殊情况的处理,所以通常情况下,系统提供给我们的组件稳定性要好一些。所以我的建议是,能使用系统提供的组件的尽量使用系统的。
+系统提供给我们的组件都是经过千锤百炼的,基本上考虑到了各种特殊情况的处理,所以通常情况下,系统提供给我们的组件稳定性要好一些。所以我的建议是,能使用系统提供的组件的尽量使用系统的。
#### 第二条:尽量避免从头开始。
如果一定要使用自定义 View,那么尽量去继承系统已有的组件,并重写其中的部分方法,不要自己从头开始写。例如:图像相关的 View 可以考虑继承 ImageView,容器类 View 可以考虑继承 LinerLayout,RelativeLayout 等,原因同上。
+#### 第三条:处理特殊情况。
+
+针对能想到的一些特殊情况进行处理并且测试,尽量保证自定义 View 能适应各种特殊场景。
\ No newline at end of file
From 6a7afdb70b745f18c07768400ce5bbcbae1b7d7d Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Thu, 2 Feb 2017 07:02:13 +0800
Subject: [PATCH 114/160] Update
---
Course/Markdown/markdown-html.md | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/Course/Markdown/markdown-html.md b/Course/Markdown/markdown-html.md
index e62c3b86..ec82fc0f 100644
--- a/Course/Markdown/markdown-html.md
+++ b/Course/Markdown/markdown-html.md
@@ -1,2 +1,3 @@
-Markdown 网页格式兼容
-
+# Markdown 网页格式兼容
+
+Markdown 作为一种标记型语言,在大多数情况下都是需要转换为 HTML 格式的,所以 Markdown 理论上是兼容 HTML 语法的,在 Markdown 所提供的标记无法满足我们需要的时候,可以尝试使用 HTML 相关语法来实现。
\ No newline at end of file
From b93f533802d3a5b1588bcbec015b7f3672a5818c Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Sat, 4 Feb 2017 09:45:16 +0800
Subject: [PATCH 115/160] Update
---
CustomView/CustomViewRule.md | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
diff --git a/CustomView/CustomViewRule.md b/CustomView/CustomViewRule.md
index 8b0a2873..8ab629e8 100644
--- a/CustomView/CustomViewRule.md
+++ b/CustomView/CustomViewRule.md
@@ -14,7 +14,9 @@
由于某些情况很特殊,触发条件也特殊,我们简单的实现了一个自定义了一个 View,可能在 99% 的情况下都是正常的,但在某些特殊情况下就会出问题。
-系统提供给我们的组件都是经过千锤百炼的,基本上考虑到了各种特殊情况的处理,所以通常情况下,系统提供给我们的组件稳定性要好一些。所以我的建议是,能使用系统提供的组件的尽量使用系统的。
+而系统提供给我们的组件都是经过千锤百炼的,基本上考虑到了各种特殊情况的处理,所以通常情况下,系统提供给我们的组件稳定性要好一些。所以我的建议是,能使用系统提供的组件的尽量使用系统的。
+
+除此之外,使用系统提供的组件也方便于其他人快速读懂项目,便于交流。
#### 第二条:尽量避免从头开始。
@@ -22,4 +24,16 @@
#### 第三条:处理特殊情况。
-针对能想到的一些特殊情况进行处理并且测试,尽量保证自定义 View 能适应各种特殊场景。
\ No newline at end of file
+针对能想到的一些特殊情况进行处理并且测试,尽量保证自定义 View 能适应各种特殊场景。
+
+#### 第四条:留下文档。
+
+**程序员有两大痛苦,1、别人写的项目居然没有文档。2、自己写的项目居然要写文档。**
+
+虽然有时候文档写起来确实挺麻烦,但是个人建议必须要留下文档,至少要为自己写的程序添加**有效的注释**。
+ 于自定义View而言,首先要标明这个自定义View是解决什么问题,有怎样的效果,使用的场景如何。
+ 其次要在关键部位标注实现原理。(例如:显示圆形的ImageView,要标明圆形是如何实现的,使用的是遮罩还是剪裁。)
+ 避免无效注释 (例如:在onDraw上面标注绘图),大家都知道这个是绘图,但绘制逻辑才是重点,要去标注绘制逻辑。
+
+
+
From e6f77b0fa5f2a00d672c2847b938861e98e8422d Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Sun, 5 Feb 2017 10:02:19 +0800
Subject: [PATCH 116/160] Update
---
CustomView/CustomViewRule.md | 17 +++++++++++++----
1 file changed, 13 insertions(+), 4 deletions(-)
diff --git a/CustomView/CustomViewRule.md b/CustomView/CustomViewRule.md
index 8ab629e8..766fcaa0 100644
--- a/CustomView/CustomViewRule.md
+++ b/CustomView/CustomViewRule.md
@@ -30,10 +30,19 @@
**程序员有两大痛苦,1、别人写的项目居然没有文档。2、自己写的项目居然要写文档。**
-虽然有时候文档写起来确实挺麻烦,但是个人建议必须要留下文档,至少要为自己写的程序添加**有效的注释**。
- 于自定义View而言,首先要标明这个自定义View是解决什么问题,有怎样的效果,使用的场景如何。
- 其次要在关键部位标注实现原理。(例如:显示圆形的ImageView,要标明圆形是如何实现的,使用的是遮罩还是剪裁。)
- 避免无效注释 (例如:在onDraw上面标注绘图),大家都知道这个是绘图,但绘制逻辑才是重点,要去标注绘制逻辑。
+虽然有时候文档写起来确实挺麻烦,但是个人建议要留下一份文档,至少要为自己写的程序添加**有效的注释**,这不仅是方便他人,更重要的是方便自己以后修改项目。
+
+* 于自定义View而言,首先要标明这个自定义View是解决什么问题,有怎样的效果,使用的场景如何。
+* 其次要在关键部位标注实现原理。(例如:显示圆形的ImageView,要标明圆形是如何实现的,使用的是遮罩还是剪裁。)
+* 避免无效注释 (例如:在onDraw上面标注绘图),大家都知道这个是绘图,但绘制逻辑才是重点,要去标注绘制逻辑。
+
+#### 第五条:面向结果编程。
+
+我们既然使用自定义View,自然是想要实现一些系统组件无法实现的效果,所以要时刻谨记自己所需要的内容,让其中的所有逻辑都为这个结果服务,我自己实现自定义 View 一般有如下步骤:
+
+1. 原型,用我能想到的逻辑实现原型(demo),不管其代码复杂度,首先要得到结果,通常情况下,第一份代码可得性和整洁性都比较差。
+2. 优化,在原型的基础上对代码进行优化,剔除不必要的内容,例如尝试优化逻辑,对与一些重复性的内容抽取函数进行封装,想办法消灭一些中间变量。同时添加上必要注释,让其逻辑更加清晰易懂。
+3. 测试,对其进行场景测试,尽量保证其正常运行。
From 5b728f9e130192aa6492810d463f0201ebc4d6d2 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Mon, 6 Feb 2017 17:00:14 +0800
Subject: [PATCH 117/160] Update
---
CustomView/Advance/[06]Path_Bezier.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CustomView/Advance/[06]Path_Bezier.md b/CustomView/Advance/[06]Path_Bezier.md
index 4384d9c2..4f22a239 100644
--- a/CustomView/Advance/[06]Path_Bezier.md
+++ b/CustomView/Advance/[06]Path_Bezier.md
@@ -122,7 +122,7 @@
> **PS: 三阶曲线对应的方法是cubicTo**
-#### [贝塞尔曲线速查表](https://github.com/GcsSloop/AndroidNote/blob/master/QuickChart/Bessel.md)
+#### [贝塞尔曲线速查表](https://github.com/GcsSloop/AndroidNote/blob/master/QuickChart/Bezier.md)
#### 强烈推荐[点击这里](http://bezier.method.ac/)练习贝塞尔曲线,可以加深对贝塞尔曲线的理解程度。
From 3673a52b1eb0541b3b358590fc13d94ac82ec81c Mon Sep 17 00:00:00 2001
From: WenChao Kong
Date: Thu, 9 Feb 2017 09:30:02 +1100
Subject: [PATCH 118/160] Update [06]Path_Bezier.md
fix broken link
---
CustomView/Advance/[06]Path_Bezier.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CustomView/Advance/[06]Path_Bezier.md b/CustomView/Advance/[06]Path_Bezier.md
index 4f22a239..e98db733 100644
--- a/CustomView/Advance/[06]Path_Bezier.md
+++ b/CustomView/Advance/[06]Path_Bezier.md
@@ -3,7 +3,7 @@
### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop)
### [【本系列相关文章】](https://github.com/GcsSloop/AndroidNote/tree/master/CustomView/README.md)
-在上一篇文章[Path之基本图形](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B05%5DPath_BasicGraphics.md)中我们了解了Path的基本使用方法,本次了解Path中非常非常非常重要的内容-贝塞尔曲线。
+在上一篇文章[Path之基本图形](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B05%5DPath_Basic.md)中我们了解了Path的基本使用方法,本次了解Path中非常非常非常重要的内容-贝塞尔曲线。
******
From 16019c79ba2f4c7328bfd79bd2deae11bc53c1ca Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=8E=8B=E6=AC=A2?=
Date: Mon, 13 Mar 2017 20:48:43 +0800
Subject: [PATCH 119/160] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E9=87=8D=E5=A4=8D?=
=?UTF-8?q?=E6=96=87=E5=AD=97?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
CustomView/Advance/[02]Canvas_BasicGraphics.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CustomView/Advance/[02]Canvas_BasicGraphics.md b/CustomView/Advance/[02]Canvas_BasicGraphics.md
index 86336f5e..65720057 100644
--- a/CustomView/Advance/[02]Canvas_BasicGraphics.md
+++ b/CustomView/Advance/[02]Canvas_BasicGraphics.md
@@ -111,7 +111,7 @@ Canvas我们可以称之为画布,能够在上面绘制各种东西,是安
******
### 绘制矩形:
-确定确定一个矩形最少需要四个数据,就是**对角线的两个点**的坐标值,这里一般采用**左上角和右下角**的两个点的坐标。
+确定一个矩形最少需要四个数据,就是**对角线的两个点**的坐标值,这里一般采用**左上角和右下角**的两个点的坐标。
关于绘制矩形,Canvas提供了三种重载方法,第一种就是提供**四个数值(矩形左上角和右下角两个点的坐标)来确定一个矩形**进行绘制。
其余两种是先将矩形封装为**Rect或RectF**(实际上仍然是用两个坐标点来确定的矩形),然后传递给Canvas绘制,如下:
From dcec08b7eaac1f2a5cb13e2945029870dd05c225 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=8E=8B=E6=AC=A2?=
Date: Tue, 14 Mar 2017 11:59:44 +0800
Subject: [PATCH 120/160] fix spelling error
---
CustomView/Advance/[04]Canvas_PictureText.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/CustomView/Advance/[04]Canvas_PictureText.md b/CustomView/Advance/[04]Canvas_PictureText.md
index ba7f2c4f..06bffcd5 100644
--- a/CustomView/Advance/[04]Canvas_PictureText.md
+++ b/CustomView/Advance/[04]Canvas_PictureText.md
@@ -415,9 +415,9 @@ PS:图片左上角位置默认为坐标原点。
| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- |
| 下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
-假设我我们指定star为1,end为3,那么最终截取的字符串就是"BC"。
+假设我们指定start为1,end为3,那么最终截取的字符串就是"BC"。
-一般来说,**使用start和end指定的区间是前闭后开的,即包含start指定的下标,而不包含end指定的下标**,故[1,3)最后获取到的下标只有 下标1 和 下标2 的字符,就是"BC".
+一般来说,**使用start和end指定的区间是前闭后开的,即包含start指定的下标,而不包含end指定的下标**,故[1,3)最后获取到的下标只有 下标1 和 下标2 的字符,就是"BC"。
示例:
``` java
@@ -430,7 +430,7 @@ PS:图片左上角位置默认为坐标原点。
另外,对于字符数组char[]我们截取字符串使用起始位置(index)和长度(count)来确定。
-同样,我们指定index为1,count为3,那么最终截取到的字符串是"BCD".
+同样,我们指定index为1,count为3,那么最终截取到的字符串是"BCD"。
其实就是从下标位置为1处向后数3位就是截取到的字符串,示例:
``` java
From d7a6a1d53fd572985b2847e07b3da1b4138ad1f7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=8E=8B=E6=AC=A2?=
Date: Wed, 15 Mar 2017 10:05:16 +0800
Subject: [PATCH 121/160] fix spelling errors
---
CustomView/Advance/[05]Path_Basic.md | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/CustomView/Advance/[05]Path_Basic.md b/CustomView/Advance/[05]Path_Basic.md
index 85f12f53..41fb94e8 100644
--- a/CustomView/Advance/[05]Path_Basic.md
+++ b/CustomView/Advance/[05]Path_Basic.md
@@ -239,7 +239,7 @@ close方法用于连接当前最后一个点和最初的一个点(如果两个
**这一类就是在path中添加一个基本形状,基本形状部分和前面所讲的绘制基本形状并无太大差别,详情参考[Canvas(1)颜色与基本形状](https://github.com/GcsSloop/AndroidNote/blob/master/%E9%97%AE%E9%A2%98/Canvas/Canvas(1).md), 本次只将其中不同的部分摘出来详细讲解一下。**
-**仔细观察一下第一类是方法,无一例外,在最后都有一个_Path.Direction_,这是一个什么神奇的东东?**
+**仔细观察一下第一类的方法,无一例外,在最后都有一个_Path.Direction_,这是一个什么神奇的东东?**
Direction的意思是 方向,趋势。 点进去看一下会发现Direction是一个枚举(Enum)类型,里面只有两个枚举常量,如下:
@@ -369,7 +369,7 @@ Direction的意思是 方向,趋势。 点进去看一下会发现Direction是
-首先我们新建地方两个Path(矩形和圆形)中心都是坐标原点,我们在将包含圆形的path添加到包含矩形的path之前将其进行移动了一段距离,最终绘制出来的效果就如上面所示。
+首先我们新建的两个Path(矩形和圆形)中心都是坐标原点,我们在将包含圆形的path添加到包含矩形的path之前将其进行移动了一段距离,最终绘制出来的效果就如上面所示。
#### 第三类(addArc与arcTo)
方法预览:
@@ -524,12 +524,12 @@ log 输出结果:
**但是第二个方法最后怎么会有一个path作为参数?**
-其实第二个方法中最后的参数das是存储平移后的path的。
+其实第二个方法中最后的参数dst是存储平移后的path的。
| dst状态 | 效果 |
| ----------- | ------------------------------ |
| dst不为空 | 将当前path平移后的状态存入dst中,不会影响当前path |
-| dat为空(null) | 平移将作用于当前path,相当于第一种方法 |
+| dst为空(null) | 平移将作用于当前path,相当于第一种方法 |
示例:
``` java
From d245e0651ec2d71b40a2bf6f098c71cfb414f750 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=8E=8B=E6=AC=A2?=
Date: Thu, 16 Mar 2017 15:27:13 +0800
Subject: [PATCH 122/160] fix spelling and format errors
---
CustomView/Advance/[07]Path_Over.md | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/CustomView/Advance/[07]Path_Over.md b/CustomView/Advance/[07]Path_Over.md
index 19007696..dad580ce 100644
--- a/CustomView/Advance/[07]Path_Over.md
+++ b/CustomView/Advance/[07]Path_Over.md
@@ -113,8 +113,8 @@

>
->P1: 从P1点发出一条射线,沿射线防线移动,并没有与边相交点部分,环绕数为0,故P1在图形外边。
->P2: 从P2点发出一条射线,沿射线方向移动,与图形点左侧边相交,该边从左到右穿过穿过射线,环绕数-1,最终环绕数为-1,故P2在图形内部。
+>P1: 从P1点发出一条射线,沿射线方向移动,并没有与边相交点部分,环绕数为0,故P1在图形外边。
+>P2: 从P2点发出一条射线,沿射线方向移动,与图形点左侧边相交,该边从左到右穿过射线,环绕数-1,最终环绕数为-1,故P2在图形内部。
>P3: 从P3点发出一条射线,沿射线方向移动,在第一个交点处,底边从右到左穿过射线,环绕数+1,在第二个交点处,右侧边从左到右穿过射线,环绕数-1,最终环绕数为0,故P3在图形外部。
通常,这两种方法的判断结果是相同的,但也存在两种方法判断结果不同的情况,如下面这种情况:
@@ -144,7 +144,7 @@ Android中的填充模式有四种,是封装在Path中的一个枚举。
我们可以看到上面有四种模式,分成两对,例如 "奇偶规则" 与 "反奇偶规则" 是一对,它们之间有什么关系呢?
-Inverse 和含义是“相反,对立”,说明反奇偶规则刚好与奇偶规则相反,例如对于一个矩形而言,使用奇偶规则会填充矩形内部,而使用反奇偶规则会填充矩形外部,这个会在后面示例中代码展示两者对区别。
+Inverse 的含义是“相反,对立”,说明反奇偶规则刚好与奇偶规则相反,例如对于一个矩形而言,使用奇偶规则会填充矩形内部,而使用反奇偶规则会填充矩形外部,这个会在后面示例中代码展示两者的区别。
#### Android与填充模式相关的方法
@@ -176,7 +176,7 @@ Inverse 和含义是“相反,对立”,说明反奇偶规则刚好与奇偶
path.addRect(-200,-200,200,200, Path.Direction.CW); // 给Path中添加一个矩形
```
-下面两张图片分别是在奇偶规则于反奇偶规则的情况下绘制的结果,可以看出其填充的区域刚好相反:
+下面两张图片分别是在奇偶规则与反奇偶规则的情况下绘制的结果,可以看出其填充的区域刚好相反:
> PS: 白色为背景色,黑色为填充色。
@@ -214,7 +214,7 @@ Inverse 和含义是“相反,对立”,说明反奇偶规则刚好与奇偶
### 布尔操作(API19)
-布尔操作与我们中学所学的集合操作非常像,只要知道集合操作中等交集,并集,差集等操作,那么理解布尔操作也是很容易的。
+布尔操作与我们中学所学的集合操作非常像,只要知道集合操作中的交集,并集,差集等操作,那么理解布尔操作也是很容易的。
**布尔操作是两个Path之间的运算,主要作用是用一些简单的图形通过一些规则合成一些相对比较复杂,或难以直接得到的图形**。
@@ -257,7 +257,7 @@ Path的布尔运算有五种逻辑,如下:
#### 布尔运算方法
-通过前面到理论知识铺垫,相信大家对布尔运算已经有了基本的认识和理解,下面我们用代码演示一下布尔运算:
+通过前面的理论知识铺垫,相信大家对布尔运算已经有了基本的认识和理解,下面我们用代码演示一下布尔运算:
在Path中的布尔运算有两个方法
@@ -268,7 +268,7 @@ Path的布尔运算有五种逻辑,如下:
两个方法中的返回值用于判断布尔运算是否成功,它们使用方法如下:
-``` `java
+``` java
// 对 path1 和 path2 执行布尔运算,运算方式由第二个参数指定,运算结果存入到path1中。
path1.op(path2, Path.Op.DIFFERENCE);
@@ -334,7 +334,7 @@ Path的布尔运算有五种逻辑,如下:
| 参数 | 作用 |
| ------ | ------------------------------- |
| bounds | 测量结果会放入这个矩形 |
-| exact | 是否精确测量,目前这一个参数作用已经废弃,一般写true即可。 |
+| exact | 是否精确测量,目前这一个参数作用已经废弃,一般写true即可 |
关于exact如有疑问可参见Google官方的提交记录[Path.computeBounds()](https://code.google.com/p/android/issues/detail?id=4070)
@@ -369,7 +369,7 @@ Path的布尔运算有五种逻辑,如下:
### 重置路径
-重置Path有两个方法,分别是reset和rewind,两者区别主要有一下两点:
+重置Path有两个方法,分别是reset和rewind,两者区别主要有以下两点:
| 方法 | 是否保留FillType设置 | 是否保留原有数据结构 |
| ------ | :------------: | :--------: |
@@ -385,7 +385,7 @@ _因为“FillType”影响的是显示效果,而“数据结构”影响的
## 总结
-Path中常用的方法到此已经结束,希望能够帮助大家加深对Path对理解运用,让大家能够用Path愉快的玩耍。( ̄▽ ̄)
+Path中常用的方法到此已经结束,希望能够帮助大家加深对Path的理解运用,让大家能够用Path愉快的玩耍。( ̄▽ ̄)
(,,• ₃ •,,)
#### PS: 由于本人水平有限,某些地方可能存在误解或不准确,如果你对此有疑问可以提交Issues进行反馈。
From 92d8647dacecdf6eb6d3807b35cb187e61e02808 Mon Sep 17 00:00:00 2001
From: sloop
Date: Sat, 25 Mar 2017 17:26:18 +0800
Subject: [PATCH 123/160] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E5=9B=BE=E7=89=87?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
CustomView/Advance/[09]Matrix_Basic.md | 338 ++-----------------------
1 file changed, 18 insertions(+), 320 deletions(-)
diff --git a/CustomView/Advance/[09]Matrix_Basic.md b/CustomView/Advance/[09]Matrix_Basic.md
index 852ee6d9..72b5f3a6 100644
--- a/CustomView/Advance/[09]Matrix_Basic.md
+++ b/CustomView/Advance/[09]Matrix_Basic.md
@@ -12,17 +12,7 @@
它看起来大概是下面这样:
-
+
**Matrix作用就是坐标映射,那么为什么需要Matrix呢? 举一个简单的例子:**
@@ -75,36 +65,11 @@ Matrix 是一个矩阵,最根本的作用就是坐标转换,下面我们就
### 1.缩放(Scale)
-
-
-
+
用矩阵表示:
-
+
> 你可能注意到了,我们坐标多了一个1,这是使用了齐次坐标系的缘故,在数学中我们的点和向量都是这样表示的(x, y),两者看起来一样,计算机无法区分,为此让计算机也可以区分它们,增加了一个标志位,增加之后看起来是这样:
>
@@ -123,36 +88,11 @@ $$)
#### 水平错切
-
-
-
+
用矩阵表示:
-
+
图例:
@@ -160,36 +100,11 @@ $$)
#### 垂直错切
-
-
-
+
用矩阵表示:
-
+
图例:
@@ -199,36 +114,11 @@ $$)
> 水平错切和垂直错切的复合。
-
-
-
+
用矩阵表示:
-
+
图例:
@@ -238,49 +128,11 @@ $$)
假定一个点 A(x0, y0) ,距离原点距离为 r, 与水平轴夹角为 α 度, 绕原点旋转 θ 度, 旋转后为点 B(x, y) 如下:
-
-
-
-
-
-= r \\cdot cos \\alpha \\cdot cos \\theta - r \\cdot sin \\alpha \\cdot sin \\theta
-= x_0 \\cdot cos \\theta - y_0 \\cdot sin \\theta
-$$)
-
-
-= r \\cdot sin \\alpha \\cdot cos \\theta + r \\cdot cos \\alpha \\cdot sin \\theta
-= y_0 \\cdot cos \\theta + x_0 \\cdot sin \\theta
-$$)
+
用矩阵表示:
- & -sin(\\theta) & 0 \\\\
-sin(\\theta) & cos(\\theta) & 0 \\\\
- 0 & 0 & 1
-\\end{1}
-\\right ]
- .
-\\left [
-\\begin{matrix}
-x_0\\\\
-y_0\\\\
-1
-\\end{1}
-\\right ]
-$$)
+
图例:
@@ -290,37 +142,11 @@ $$)
> 此处也是使用齐次坐标的优点体现之一,实际上前面的三个操作使用 2x2 的矩阵也能满足需求,但是使用 2x2 的矩阵,无法将平移操作加入其中,而将坐标扩展为齐次坐标后,将矩阵扩展为 3x3 就可以将算法统一,四种算法均可以使用矩阵乘法完成。
-
-
-
+
用矩阵表示:
-
+
图例:
@@ -473,42 +299,11 @@ Log.e(TAG, "MatrixTest" + matrix.toShortString());
之所以平移距离是 MTRANS\_X = 500,MTRANS\_Y = 800,那是因为执行 Translate 之前 Matrix 已经具有了一个缩放比例。在右乘的时候影响到了具体的数值计算,可以用矩阵乘法计算一下。
-
+
最终结果为:
-
+
当 T*S 的时候,缩放比例则不会影响到 MTRANS\\_X 和 MTRANS\\_Y ,具体可以使用矩阵乘法自己计算一遍。
@@ -620,40 +415,7 @@ m.preScale(sx, sy);
```
用矩阵表示:
-
-
+
#### 2.仅用post:
@@ -667,39 +429,7 @@ m.postTranslate(tx, ty);
用矩阵表示:
-
+
#### 3.混合:
@@ -725,39 +455,7 @@ m.preScale(sx, sy);
用矩阵表示:
-
+
**注意: 由于矩阵乘法不满足交换律,请保证初始矩阵为单位矩阵,如果初始矩阵不为单位矩阵,则导致运算结果不同。**
From bffa074225366c9ea41e23e7f478c70c96ec4fff Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Sat, 25 Mar 2017 17:29:32 +0800
Subject: [PATCH 124/160] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E5=9B=BE=E7=89=87?=
=?UTF-8?q?=E9=93=BE=E6=8E=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
CustomView/Advance/[09]Matrix_Basic.md | 34 +++++++++++++-------------
1 file changed, 17 insertions(+), 17 deletions(-)
diff --git a/CustomView/Advance/[09]Matrix_Basic.md b/CustomView/Advance/[09]Matrix_Basic.md
index 72b5f3a6..e60d8bfe 100644
--- a/CustomView/Advance/[09]Matrix_Basic.md
+++ b/CustomView/Advance/[09]Matrix_Basic.md
@@ -12,7 +12,7 @@
它看起来大概是下面这样:
-
+
**Matrix作用就是坐标映射,那么为什么需要Matrix呢? 举一个简单的例子:**
@@ -65,11 +65,11 @@ Matrix 是一个矩阵,最根本的作用就是坐标转换,下面我们就
### 1.缩放(Scale)
-
+
用矩阵表示:
-
+
> 你可能注意到了,我们坐标多了一个1,这是使用了齐次坐标系的缘故,在数学中我们的点和向量都是这样表示的(x, y),两者看起来一样,计算机无法区分,为此让计算机也可以区分它们,增加了一个标志位,增加之后看起来是这样:
>
@@ -88,11 +88,11 @@ Matrix 是一个矩阵,最根本的作用就是坐标转换,下面我们就
#### 水平错切
-
+
用矩阵表示:
-
+
图例:
@@ -100,11 +100,11 @@ Matrix 是一个矩阵,最根本的作用就是坐标转换,下面我们就
#### 垂直错切
-
+
用矩阵表示:
-
+
图例:
@@ -114,11 +114,11 @@ Matrix 是一个矩阵,最根本的作用就是坐标转换,下面我们就
> 水平错切和垂直错切的复合。
-
+
用矩阵表示:
-
+
图例:
@@ -128,11 +128,11 @@ Matrix 是一个矩阵,最根本的作用就是坐标转换,下面我们就
假定一个点 A(x0, y0) ,距离原点距离为 r, 与水平轴夹角为 α 度, 绕原点旋转 θ 度, 旋转后为点 B(x, y) 如下:
-
+
用矩阵表示:
-
+
图例:
@@ -142,11 +142,11 @@ Matrix 是一个矩阵,最根本的作用就是坐标转换,下面我们就
> 此处也是使用齐次坐标的优点体现之一,实际上前面的三个操作使用 2x2 的矩阵也能满足需求,但是使用 2x2 的矩阵,无法将平移操作加入其中,而将坐标扩展为齐次坐标后,将矩阵扩展为 3x3 就可以将算法统一,四种算法均可以使用矩阵乘法完成。
-
+
用矩阵表示:
-
+
图例:
@@ -299,11 +299,11 @@ Log.e(TAG, "MatrixTest" + matrix.toShortString());
之所以平移距离是 MTRANS\_X = 500,MTRANS\_Y = 800,那是因为执行 Translate 之前 Matrix 已经具有了一个缩放比例。在右乘的时候影响到了具体的数值计算,可以用矩阵乘法计算一下。
-
+
最终结果为:
-
+
当 T*S 的时候,缩放比例则不会影响到 MTRANS\\_X 和 MTRANS\\_Y ,具体可以使用矩阵乘法自己计算一遍。
@@ -415,7 +415,7 @@ m.preScale(sx, sy);
```
用矩阵表示:
-
+
#### 2.仅用post:
@@ -429,7 +429,7 @@ m.postTranslate(tx, ty);
用矩阵表示:
-
+
#### 3.混合:
From d7a98aa2827497ca6d1ba0e6c311d358499cd0a7 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Sat, 15 Apr 2017 22:56:08 +0800
Subject: [PATCH 125/160] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E6=B3=A8=E9=87=8A?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
SourceAnalysis/AtomicFile.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/SourceAnalysis/AtomicFile.md b/SourceAnalysis/AtomicFile.md
index 6f6d8995..78e669c2 100644
--- a/SourceAnalysis/AtomicFile.md
+++ b/SourceAnalysis/AtomicFile.md
@@ -71,7 +71,7 @@ public AtomicFile(File baseName) {
```java
public FileOutputStream startWrite() throws IOException {
- // 如果备份文件不存在,将原文件重命名为备份文件,并删除原文件
+ // 当原文件存在,备份文件不存在的时候,原文件更名为备份文件
if (mBaseName.exists()) {
if (!mBackupName.exists()) {
// 如果原文件存在且备份文件不存在,直接将原文件重命名为备份文件
From dc51d03d505af3cd93ca1e705f707eba7faf6aa5 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Fri, 19 May 2017 11:05:31 +0800
Subject: [PATCH 126/160] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=B0=E6=96=87?=
=?UTF-8?q?=E7=AB=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
CustomView/Advance/[19]gesture-detector.md | 47 ++++++++++++++++++++++
1 file changed, 47 insertions(+)
create mode 100644 CustomView/Advance/[19]gesture-detector.md
diff --git a/CustomView/Advance/[19]gesture-detector.md b/CustomView/Advance/[19]gesture-detector.md
new file mode 100644
index 00000000..814c6f4f
--- /dev/null
+++ b/CustomView/Advance/[19]gesture-detector.md
@@ -0,0 +1,47 @@
+# Android 手势检测(GestureDetector)
+
+因为工作等原因,已经很长时间没有写文章了,趁节假日来水一篇简单的文章,Android 手势检测,如题,本文依旧是和事件相关的,如果你没看过之前的文章,可以到 [自定义 View 系列](http://www.gcssloop.com/customview/CustomViewIndex) 来查看前面的内容。
+
+在开发 Android 手机应用过程中,可能需要对一些手势作出响应,如:单击、双击、长按、滑动、缩放等。这些都是很常用的手势。就拿最简单的双击来说吧,假如我们需要判断一个控件是否被双击(即在较短的时间内快速的点击两次),似乎是一个很容易的任务,但仔细考虑起来,要处理的细节问题也有不少,例如:
+
+1. **记录点击次数**,为了判断是否被点击超过 1 次,所以必须记录点击次数。
+2. **记录点击时间**,由于双击事件是较快速的点击两次,像点击一次后,过来几分钟再点击一次肯定不能算是双击事件,所以在记录点击次数的同时也要记录上一次的点击时间,我们可以设置本次点击距离上一次时间超过一定时间(例如:超过1s)就不识别为双击事件。
+3. **点击状态重置**,在响应双击事件,或者判断不是双击事件的时候要重置计数器和上一次点击时间。重置既可以在点击的时候判断并进行重新设置,也可以使用定时器等超过一定时间后重置状态。
+
+这样看起来,判断一个双击事件就有这么多麻烦事情,更别其他的手势了,虽然这些看起来都很简单,但设计起来需要考虑的细节情况实在是太多了。
+
+那么有没有一种更好的方法来方便的检测手势呢?当然有啦,因为这些手势很常用,系统早就封装了一些方法给我们用,它就是 **GestureDetector** 。我们先看一下关于它的简单介绍:
+
+> GestureDetector 可以使用 MotionEvents 检测各种手势和事件。GestureDetector.OnGestureListener 是一个回调方法,在发生特定的事件时会调用 Listener 中对应的方法回调。这个类只能用于检测触摸事件的 MotionEvent,不能用于轨迹球事件。
+> (话说轨迹球已经消失多长时间了,估计很多人都没见过轨迹球这种东西)。
+>
+> 如何使用:
+>
+> - 创建一个 GestureDetector 实例。
+> - 在onTouchEvent(MotionEvent)方法中,确保调用 GestureDetector 实例的 onTouchEvent(MotionEvent)。回调中定义的方法将在事件发生时执行。
+> - 如果侦听 onContextClick(MotionEvent),则必须在 View 的 onGenericMotionEvent(MotionEvent)中调用 GestureDetector OnGenericMotionEvent(MotionEvent)。
+
+ GestureDetector 本身的方法很少,使用起来也非常简单,下面让我们看一下它的简单使用方法。
+
+```java
+// 创建一个监听回调
+SimpleOnGestureListener listener = new SimpleOnGestureListener() {
+ @Override public boolean onDoubleTap(MotionEvent e) {
+ Toast.makeText(MainActivity.this, "双击666", Toast.LENGTH_SHORT).show();
+ return super.onDoubleTap(e);
+ }
+};
+
+// 创建一个检测器
+final GestureDetector detector = new GestureDetector(this, listener);
+
+// 给监听器设置数据源
+view.setOnTouchListener(new View.OnTouchListener() {
+ @Override public boolean onTouch(View v, MotionEvent event) {
+ return detector.onTouchEvent(event);
+ }
+});
+```
+
+上面是监听一个双击事件,可以看到它使用起来非常简单,接下来我们就看一下有关于它的详细信息。
+
From 11fae36ef87029d8a945e7cf6e52dd73d26104ae Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Sun, 28 May 2017 13:46:52 +0800
Subject: [PATCH 127/160] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E6=A0=BC=E5=BC=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index a338c9e8..e3b614e8 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,7 @@
******
+
## [自定义View](https://github.com/GcsSloop/AndroidNote/tree/master/CustomView/README.md)
* 基础篇
From 7a43d4a2d4fac37e21ce2750ae55274ab70df644 Mon Sep 17 00:00:00 2001
From: GcsSloop
Date: Sun, 9 Jul 2017 23:01:44 +0800
Subject: [PATCH 128/160] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AE=9E=E4=BE=8B?=
=?UTF-8?q?=E4=BB=A3=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
CustomView/Advance/Code/FailingBall.zip | Bin 0 -> 699913 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 CustomView/Advance/Code/FailingBall.zip
diff --git a/CustomView/Advance/Code/FailingBall.zip b/CustomView/Advance/Code/FailingBall.zip
new file mode 100644
index 0000000000000000000000000000000000000000..92f0b8f514c43358347b655d3eb9f6cff571fee4
GIT binary patch
literal 699913
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