diff --git a/AudioVideo/README.md b/AudioVideo/README.md
new file mode 100644
index 00000000..de09b5cb
--- /dev/null
+++ b/AudioVideo/README.md
@@ -0,0 +1,4 @@
+# Audio/Video 杂记
+
+- [通用视频解码播放流程](通用视频解码播放流程.md)
+
diff --git a/AudioVideo/SDL.md b/AudioVideo/SDL.md
new file mode 100644
index 00000000..02b79fb9
--- /dev/null
+++ b/AudioVideo/SDL.md
@@ -0,0 +1,73 @@
+---
+typora-copy-images-to: ./image
+---
+
+## SDL
+
+### 简介
+
+SDL(Simple DirectMedia Layer)库的作用就是封装了复杂的音视频底层交互工作,简化了音视频处理的难度。
+
+**特点:** 开源、跨平台。
+
+### 结构
+
+
+
+它是对底层进行了封装,最终还是调用的平台底层接口与硬件进行交互。
+
+### SDL 流程
+
+
+
+### SDL 主要函数
+
+| 函数 | 简介 |
+| -------------------- | -------------------------- |
+| SDL_Init() | 初始化 SDL 系统。 |
+| SDL_CreateWindow() | 创建窗口 SDL_Window。 |
+| SDL_CreateRenderer() | 创建渲染器 SDL_Renderer。 |
+| SDL_CreateTexture() | 创建纹理 SDL_Texture。 |
+| SDL_UpdateTexture() | 设置纹理数据。 |
+| SDL_RenderCopy() | 将纹理的数据拷贝给渲染器。 |
+| SDL_RenderPresent() | 显示。 |
+| SDL_Delay() | 工具函数,用于延时。 |
+| SDL_Quit() | 退出 SDL 系统。 |
+
+### SDL 数据结构
+
+
+
+**数据结构简介:**
+
+| 结构 | 简介 |
+| ------------ | -------------------- |
+| SDL_Window | 代表一个"窗口"。 |
+| SDL_Renderer | 代表一个"渲染器"。 |
+| SDL_Texture | 代表一个"纹理"。 |
+| SDL_Rect | 一个简单的矩形结构。 |
+
+### SDL 事件和多线程
+
+#### **SDL 多线程**
+
+| 函数 | 简介 |
+| ------------------ | -------------- |
+| SDL_CreateThread() | 创建一个线程。 |
+| SDL_Thread() | 线程的句柄。 |
+
+#### **SDL 事件**
+
+**函数:**
+
+| 函数 | 简介 |
+| --------------- | -------------- |
+| SDL_WaitEvent() | 等待一个事件。 |
+| SDL_PushEvent() | 发送一个事件。 |
+
+**数据结构:**
+
+| 结构 | 简介 |
+| ----------- | -------------- |
+| SDL_Event() | 代表一个事件。 |
+
diff --git "a/AudioVideo/image/SDL346円225円260円346円215円256円347円273円223円346円236円204円.jpg" "b/AudioVideo/image/SDL346円225円260円346円215円256円347円273円223円346円236円204円.jpg"
new file mode 100755
index 00000000..0fa937b7
Binary files /dev/null and "b/AudioVideo/image/SDL346円225円260円346円215円256円347円273円223円346円236円204円.jpg" differ
diff --git "a/AudioVideo/image/SDL346円265円201円347円250円213円.jpg" "b/AudioVideo/image/SDL346円265円201円347円250円213円.jpg"
new file mode 100755
index 00000000..18105033
Binary files /dev/null and "b/AudioVideo/image/SDL346円265円201円347円250円213円.jpg" differ
diff --git "a/AudioVideo/image/SDL347円273円223円346円236円204円.png" "b/AudioVideo/image/SDL347円273円223円346円236円204円.png"
new file mode 100644
index 00000000..9d150d37
Binary files /dev/null and "b/AudioVideo/image/SDL347円273円223円346円236円204円.png" differ
diff --git "a/AudioVideo/image/350円247円206円351円242円221円350円247円243円347円240円201円346円222円255円346円224円276円346円265円201円347円250円213円.gliffy" "b/AudioVideo/image/350円247円206円351円242円221円350円247円243円347円240円201円346円222円255円346円224円276円346円265円201円347円250円213円.gliffy"
new file mode 100644
index 00000000..f4a83d31
--- /dev/null
+++ "b/AudioVideo/image/350円247円206円351円242円221円350円247円243円347円240円201円346円222円255円346円224円276円346円265円201円347円250円213円.gliffy"
@@ -0,0 +1 @@
+{"contentType":"application/gliffy+json","version":"1.1","metadata":{"title":"untitled","revision":0,"exportBorder":false},"embeddedResources":{"index":0,"resources":[]},"stage":{"objects":[{"x":20,"y":580,"rotation":0,"id":3,"uid":"com.gliffy.shape.network.network_v3.home.speakers","width":74,"height":100,"lockAspectRatio":true,"lockShape":false,"order":36,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.speakers_3d.network_v3","strokeWidth":2,"strokeColor":"#000000","fillColor":"#003366","gradient":false,"dropShadow":false,"state":0,"shadowX":0,"shadowY":0,"opacity":1}},"children":[{"x":2,"y":0,"rotation":0,"id":69,"uid":null,"width":75,"height":14,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Text","Text":{"tid":null,"valign":"middle","overflow":"both","vposition":"below","hposition":"none","html":"
音频驱动/设备
","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null}],"linkMap":[]},{"x":264,"y":580,"rotation":0,"id":9,"uid":"com.gliffy.shape.network.network_v3.home.tv_flatscreen","width":76,"height":100,"lockAspectRatio":true,"lockShape":false,"order":35,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.tv_flatscreen_3d.network_v3","strokeWidth":2,"strokeColor":"#000000","fillColor":"#003366","gradient":false,"dropShadow":false,"state":0,"shadowX":0,"shadowY":0,"opacity":1}},"children":[{"x":2,"y":0,"rotation":0,"id":70,"uid":null,"width":75,"height":14,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Text","Text":{"tid":null,"valign":"middle","overflow":"both","vposition":"below","hposition":"none","html":"视频驱动/设备
","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null}],"linkMap":[]},{"x":245,"y":580,"rotation":0,"id":60,"uid":"com.gliffy.shape.basic.basic_v1.default.line","width":100,"height":100,"lockAspectRatio":false,"lockShape":false,"order":34,"graphic":{"type":"Line","Line":{"strokeWidth":2,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","ortho":true,"interpolationType":"linear","cornerRadius":null,"controlPath":[[-65,0],[-65,50],[57,50]],"lockSegments":{}}},"children":null,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":45,"px":0.5,"py":1}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":9,"px":0.5,"py":0.5}}},"linkMap":[]},{"x":114,"y":584,"rotation":0,"id":59,"uid":"com.gliffy.shape.basic.basic_v1.default.line","width":100,"height":100,"lockAspectRatio":false,"lockShape":false,"order":33,"graphic":{"type":"Line","Line":{"strokeWidth":2,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","ortho":true,"interpolationType":"linear","cornerRadius":null,"controlPath":[[66,-4],[66,46],[-57,46]],"lockSegments":{}}},"children":null,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":45,"px":0.5,"py":1}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":3,"px":0.5,"py":0.5}}},"linkMap":[]},{"x":270,"y":502,"rotation":0,"id":58,"uid":"com.gliffy.shape.basic.basic_v1.default.line","width":100,"height":100,"lockAspectRatio":false,"lockShape":false,"order":32,"graphic":{"type":"Line","Line":{"strokeWidth":2,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","ortho":true,"interpolationType":"linear","cornerRadius":null,"controlPath":[[10,-2],[10,18],[-90,18],[-90,38]],"lockSegments":{}}},"children":null,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":40,"px":0.5,"py":1}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":45,"px":0.5,"py":0}}},"linkMap":[]},{"x":92,"y":497,"rotation":0,"id":56,"uid":"com.gliffy.shape.basic.basic_v1.default.line","width":100,"height":100,"lockAspectRatio":false,"lockShape":false,"order":31,"graphic":{"type":"Line","Line":{"strokeWidth":2,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","ortho":true,"interpolationType":"linear","cornerRadius":null,"controlPath":[[-12,3],[-12,23],[88,23],[88,43]],"lockSegments":{}}},"children":null,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":38,"px":0.5,"py":1}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":45,"px":0.5,"py":0}}},"linkMap":[]},{"x":267,"y":433,"rotation":0,"id":54,"uid":"com.gliffy.shape.basic.basic_v1.default.line","width":100,"height":100,"lockAspectRatio":false,"lockShape":false,"order":30,"graphic":{"type":"Line","Line":{"strokeWidth":2,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","ortho":true,"interpolationType":"linear","cornerRadius":10,"controlPath":[[13,-3],[13,7],[13,17],[13,27]],"lockSegments":{}}},"children":null,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":36,"px":0.5,"py":1}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":40,"px":0.5,"py":0}}},"linkMap":[]},{"x":87,"y":431,"rotation":0,"id":53,"uid":"com.gliffy.shape.basic.basic_v1.default.line","width":100,"height":100,"lockAspectRatio":false,"lockShape":false,"order":29,"graphic":{"type":"Line","Line":{"strokeWidth":2,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","ortho":true,"interpolationType":"linear","cornerRadius":10,"controlPath":[[-7,-1],[-7,9],[-7,19],[-7,29]],"lockSegments":{}}},"children":null,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":34,"px":0.5,"py":1}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":38,"px":0.5,"py":0}}},"linkMap":[]},{"x":271,"y":362,"rotation":0,"id":52,"uid":"com.gliffy.shape.basic.basic_v1.default.line","width":100,"height":100,"lockAspectRatio":false,"lockShape":false,"order":28,"graphic":{"type":"Line","Line":{"strokeWidth":2,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","ortho":true,"interpolationType":"linear","cornerRadius":10,"controlPath":[[9,-2],[9,8],[9,18],[9,28]],"lockSegments":{}}},"children":null,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":32,"px":0.5,"py":1}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":36,"px":0.5,"py":0}}},"linkMap":[]},{"x":92,"y":356,"rotation":0,"id":51,"uid":"com.gliffy.shape.basic.basic_v1.default.line","width":100,"height":100,"lockAspectRatio":false,"lockShape":false,"order":27,"graphic":{"type":"Line","Line":{"strokeWidth":2,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","ortho":true,"interpolationType":"linear","cornerRadius":10,"controlPath":[[-12,4],[-12,14],[-12,24],[-12,34]],"lockSegments":{}}},"children":null,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":29,"px":0.5,"py":1}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":34,"px":0.5,"py":0}}},"linkMap":[]},{"x":243,"y":271,"rotation":0,"id":50,"uid":"com.gliffy.shape.basic.basic_v1.default.line","width":100,"height":100,"lockAspectRatio":false,"lockShape":false,"order":26,"graphic":{"type":"Line","Line":{"strokeWidth":2,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","ortho":true,"interpolationType":"linear","cornerRadius":null,"controlPath":[[-63,-1],[-63,24],[37,24],[37,49]],"lockSegments":{}}},"children":null,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":27,"px":0.5,"py":1}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":32,"px":0.5,"py":0}}},"linkMap":[]},{"x":106,"y":268,"rotation":0,"id":49,"uid":"com.gliffy.shape.basic.basic_v1.default.line","width":100,"height":100,"lockAspectRatio":false,"lockShape":false,"order":25,"graphic":{"type":"Line","Line":{"strokeWidth":2,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","ortho":true,"interpolationType":"linear","cornerRadius":null,"controlPath":[[74,2],[74,27],[-26,27],[-26,52]],"lockSegments":{}}},"children":null,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":27,"px":0.5,"py":1}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":29,"px":0.5,"py":0}}},"linkMap":[]},{"x":181,"y":201,"rotation":0,"id":48,"uid":"com.gliffy.shape.basic.basic_v1.default.line","width":100,"height":100,"lockAspectRatio":false,"lockShape":false,"order":24,"graphic":{"type":"Line","Line":{"strokeWidth":2,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","ortho":true,"interpolationType":"linear","cornerRadius":10,"controlPath":[[-1,-1],[-1,9],[-1,19],[-1,29]],"lockSegments":{}}},"children":null,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":25,"px":0.5,"py":1}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":27,"px":0.5,"py":0}}},"linkMap":[]},{"x":179,"y":134,"rotation":0,"id":47,"uid":"com.gliffy.shape.basic.basic_v1.default.line","width":100,"height":100,"lockAspectRatio":false,"lockShape":false,"order":23,"graphic":{"type":"Line","Line":{"strokeWidth":2,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","ortho":true,"interpolationType":"linear","cornerRadius":10,"controlPath":[[1,-4],[1,6],[1,16],[1,26]],"lockSegments":{}}},"children":null,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":23,"px":0.5,"py":1}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":25,"px":0.5,"py":0}}},"linkMap":[]},{"x":120,"y":540,"rotation":0,"id":45,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.process","width":120,"height":40,"lockAspectRatio":false,"lockShape":false,"order":21,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dropShadow":false,"state":0,"shadowX":0,"shadowY":0,"opacity":1}},"children":[{"x":2.4000000000000004,"y":0,"rotation":0,"id":46,"uid":null,"width":115.2,"height":14,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Text","Text":{"tid":null,"valign":"middle","overflow":"none","vposition":"none","hposition":"none","html":"音视频同步
","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null}],"linkMap":[]},{"x":182,"y":62,"rotation":0,"id":42,"uid":"com.gliffy.shape.basic.basic_v1.default.line","width":100,"height":100,"lockAspectRatio":false,"lockShape":false,"order":20,"graphic":{"type":"Line","Line":{"strokeWidth":2,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","ortho":true,"interpolationType":"linear","cornerRadius":10,"controlPath":[[-2,-2],[-2,8],[-2,18],[-2,28]],"lockSegments":{}}},"children":null,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":19,"px":0.5,"py":1}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":23,"px":0.5,"py":0}}},"linkMap":[]},{"x":220,"y":460,"rotation":0,"id":40,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.process","width":120,"height":40,"lockAspectRatio":false,"lockShape":false,"order":18,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2,"strokeColor":"#333333","fillColor":"#d0e0e3","gradient":false,"dropShadow":false,"state":0,"shadowX":0,"shadowY":0,"opacity":1}},"children":[{"x":2.4,"y":0,"rotation":0,"id":41,"uid":null,"width":115.2,"height":14,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Text","Text":{"tid":null,"valign":"middle","overflow":"none","vposition":"none","hposition":"none","html":"视频原始数据
","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null}],"linkMap":[]},{"x":20,"y":460,"rotation":0,"id":38,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.process","width":120,"height":40,"lockAspectRatio":false,"lockShape":false,"order":16,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2,"strokeColor":"#333333","fillColor":"#d0e0e3","gradient":false,"dropShadow":false,"state":0,"shadowX":0,"shadowY":0,"opacity":1}},"children":[{"x":2.4,"y":0,"rotation":0,"id":39,"uid":null,"width":115.2,"height":14,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Text","Text":{"tid":null,"valign":"middle","overflow":"none","vposition":"none","hposition":"none","html":"音频原始数据
","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null}],"linkMap":[]},{"x":220,"y":390,"rotation":0,"id":36,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.process","width":120,"height":40,"lockAspectRatio":false,"lockShape":false,"order":14,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2,"strokeColor":"#333333","fillColor":"#f9cb9c","gradient":false,"dropShadow":false,"state":0,"shadowX":0,"shadowY":0,"opacity":1}},"children":[{"x":2.4,"y":0,"rotation":0,"id":37,"uid":null,"width":115.2,"height":14,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Text","Text":{"tid":null,"valign":"middle","overflow":"none","vposition":"none","hposition":"none","html":"视频解码
","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null}],"linkMap":[]},{"x":20,"y":390,"rotation":0,"id":34,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.process","width":120,"height":40,"lockAspectRatio":false,"lockShape":false,"order":12,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2,"strokeColor":"#333333","fillColor":"#f9cb9c","gradient":false,"dropShadow":false,"state":0,"shadowX":0,"shadowY":0,"opacity":1}},"children":[{"x":2.4,"y":0,"rotation":0,"id":35,"uid":null,"width":115.2,"height":14,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Text","Text":{"tid":null,"valign":"middle","overflow":"none","vposition":"none","hposition":"none","html":"音频解码
","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null}],"linkMap":[]},{"x":220,"y":320,"rotation":0,"id":32,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.process","width":120,"height":40,"lockAspectRatio":false,"lockShape":false,"order":10,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2,"strokeColor":"#333333","fillColor":"#a2c4c9","gradient":false,"dropShadow":false,"state":0,"shadowX":0,"shadowY":0,"opacity":1}},"children":[{"x":2.4,"y":0,"rotation":0,"id":33,"uid":null,"width":115.2,"height":14,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Text","Text":{"tid":null,"valign":"middle","overflow":"none","vposition":"none","hposition":"none","html":"视频压缩数据
","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null}],"linkMap":[]},{"x":20,"y":320,"rotation":0,"id":29,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.process","width":120,"height":40,"lockAspectRatio":false,"lockShape":false,"order":8,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2,"strokeColor":"#333333","fillColor":"#a2c4c9","gradient":false,"dropShadow":false,"state":0,"shadowX":0,"shadowY":0,"opacity":1}},"children":[{"x":2.4,"y":0,"rotation":0,"id":30,"uid":null,"width":115.2,"height":14,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Text","Text":{"tid":null,"valign":"middle","overflow":"none","vposition":"none","hposition":"none","html":"音频压缩数据
","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null}],"linkMap":[]},{"x":120,"y":230,"rotation":0,"id":27,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.process","width":120,"height":40,"lockAspectRatio":false,"lockShape":false,"order":6,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dropShadow":false,"state":0,"shadowX":0,"shadowY":0,"opacity":1}},"children":[{"x":2.3999999999999995,"y":0,"rotation":0,"id":28,"uid":null,"width":115.19999999999999,"height":14,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Text","Text":{"tid":null,"valign":"middle","overflow":"none","vposition":"none","hposition":"none","html":"解封装
","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null}],"linkMap":[]},{"x":120,"y":160,"rotation":0,"id":25,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.process","width":120,"height":40,"lockAspectRatio":false,"lockShape":false,"order":4,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2,"strokeColor":"#333333","fillColor":"#76a5af","gradient":false,"dropShadow":false,"state":0,"shadowX":0,"shadowY":0,"opacity":1}},"children":[{"x":2.3999999999999995,"y":0,"rotation":0,"id":26,"uid":null,"width":115.19999999999999,"height":14,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Text","Text":{"tid":null,"valign":"middle","overflow":"none","vposition":"none","hposition":"none","html":"封装格式数据
","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null}],"linkMap":[]},{"x":120,"y":90,"rotation":0,"id":23,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.process","width":120,"height":40,"lockAspectRatio":false,"lockShape":false,"order":2,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dropShadow":false,"state":0,"shadowX":0,"shadowY":0,"opacity":1}},"children":[{"x":2.4000000000000004,"y":0,"rotation":0,"id":24,"uid":null,"width":115.19999999999999,"height":14,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Text","Text":{"tid":null,"valign":"middle","overflow":"none","vposition":"none","hposition":"none","html":"解协议
","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null}],"linkMap":[]},{"x":120,"y":20,"rotation":0,"id":19,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.process","width":120,"height":40,"lockAspectRatio":false,"lockShape":false,"order":0,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2,"strokeColor":"#333333","fillColor":"#45818e","gradient":false,"dropShadow":false,"state":0,"shadowX":0,"shadowY":0,"opacity":1}},"children":[{"x":2.3999999999999995,"y":0,"rotation":0,"id":21,"uid":null,"width":115.19999999999993,"height":14,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Text","Text":{"tid":null,"valign":"middle","overflow":"none","vposition":"none","hposition":"none","html":"网络数据
","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null}],"linkMap":[]}],"background":"#FFFFFF","width":340.5,"height":698,"maxWidth":5000,"maxHeight":5000,"nodeIndex":72,"autoFit":true,"exportBorder":false,"gridOn":true,"snapToGrid":true,"drawingGuidesOn":true,"pageBreaksOn":false,"printGridOn":false,"printPaper":"LETTER","printShrinkToFit":false,"printPortrait":true,"shapeStyles":{"com.gliffy.shape.network.network_v3.home":{"fill":"#003366"},"com.gliffy.shape.flowchart.flowchart_v1.default":{"fill":"#f9cb9c","stroke":"#333333","strokeWidth":2}},"lineStyles":{"global":{"endArrow":2,"orthoMode":0}},"textStyles":{},"themeData":null}}
\ No newline at end of file
diff --git "a/AudioVideo/image/350円247円206円351円242円221円350円247円243円347円240円201円346円222円255円346円224円276円346円265円201円347円250円213円.jpg" "b/AudioVideo/image/350円247円206円351円242円221円350円247円243円347円240円201円346円222円255円346円224円276円346円265円201円347円250円213円.jpg"
new file mode 100644
index 00000000..25f9b820
Binary files /dev/null and "b/AudioVideo/image/350円247円206円351円242円221円350円247円243円347円240円201円346円222円255円346円224円276円346円265円201円347円250円213円.jpg" differ
diff --git "a/AudioVideo/345円270円270円350円247円201円345円260円201円350円243円205円346円240円274円345円274円217円.md" "b/AudioVideo/345円270円270円350円247円201円345円260円201円350円243円205円346円240円274円345円274円217円.md"
new file mode 100644
index 00000000..5dcf80b8
--- /dev/null
+++ "b/AudioVideo/345円270円270円350円247円201円345円260円201円350円243円205円346円240円274円345円274円217円.md"
@@ -0,0 +1,28 @@
+## 常见封装格式
+
+封装格式的主要作用是把视频码流和音频码流按照一定的格式存储在一个文件中。现如今流行的封装格式如下表所示:
+
+主要封装格式一览
+
+| 名称 | 推出机构 | 流媒体 | 支持的视频编码 | 支持的音频编码 | 目前使用领域 |
+| ---- | ------------------ | ---- | ----------------------------- | ------------------------------------ | --------- |
+| AVI | Microsoft Inc. | 不支持 | 几乎所有格式 | 几乎所有格式 | BT下载影视 |
+| MP4 | MPEG | 支持 | MPEG-2, MPEG-4, H.264, H.263等 | AAC, MPEG-1 Layers I, II, III, AC-3等 | 互联网视频网站 |
+| TS | MPEG | 支持 | MPEG-1, MPEG-2, MPEG-4, H.264 | MPEG-1 Layers I, II, III, AAC, | IPTV,数字电视 |
+| FLV | Adobe Inc. | 支持 | Sorenson, VP6, H.264 | MP3, ADPCM, Linear PCM, AAC等 | 互联网视频网站 |
+| MKV | CoreCodec Inc. | 支持 | 几乎所有格式 | 几乎所有格式 | 互联网视频网站 |
+| RMVB | Real Networks Inc. | 支持 | RealVideo 8, 9, 10 | AAC, Cook Codec, RealAudio Lossless | BT下载影视 |
+
+由表可见,除了AVI之外,其他封装格式都支持流媒体,即可以"边下边播"。有些格式更"万能"一些,支持的视音频编码标准多一些,比如MKV。而有些格式则支持的相对比较少,比如说RMVB。
+
+这些封装格式都有相关的文档,在这里就不一一例举了。
+
+我自己也做过辅助学习的小项目:
+
+[TS封装格式分析器](http://blog.csdn.net/leixiaohua1020/article/details/17973587)
+
+[FLV封装格式分析器](http://blog.csdn.net/leixiaohua1020/article/details/17934487)
+
+### 参考资料:
+
+[视音频编解码技术零基础学习方法](http://blog.csdn.net/leixiaohua1020/article/details/18893769)
\ No newline at end of file
diff --git "a/AudioVideo/345円270円270円350円247円201円345円260円201円350円243円205円346円240円274円345円274円217円346円246円202円350円247円210円.md" "b/AudioVideo/345円270円270円350円247円201円345円260円201円350円243円205円346円240円274円345円274円217円346円246円202円350円247円210円.md"
new file mode 100644
index 00000000..98f1e818
--- /dev/null
+++ "b/AudioVideo/345円270円270円350円247円201円345円260円201円350円243円205円346円240円274円345円274円217円346円246円202円350円247円210円.md"
@@ -0,0 +1,14 @@
+## 常见封装格式概览
+
+| 名称 | 推出机构 | 流媒体 | 支持的视频编码 | 支持的音频编码 | 目前使用领域 |
+| ---- | ------------------ | ---- | ----------------------------- | ------------------------------------ | --------- |
+| AVI | Microsoft Inc. | 不支持 | 几乎所有格式 | 几乎所有格式 | BT下载影视 |
+| MP4 | MPEG | 支持 | MPEG-2, MPEG-4, H.264, H.263等 | AAC, MPEG-1 Layers I, II, III, AC-3等 | 互联网视频网站 |
+| TS | MPEG | 支持 | MPEG-1, MPEG-2, MPEG-4, H.264 | MPEG-1 Layers I, II, III, AAC, | IPTV,数字电视 |
+| FLV | Adobe Inc. | 支持 | Sorenson, VP6, H.264 | MP3, ADPCM, Linear PCM, AAC等 | 互联网视频网站 |
+| MKV | CoreCodec Inc. | 支持 | 几乎所有格式 | 几乎所有格式 | 互联网视频网站 |
+| RMVB | Real Networks Inc. | 支持 | RealVideo 8, 9, 10 | AAC, Cook Codec, RealAudio Lossless | BT下载影视 |
+
+### 参考资料:
+
+[视音频编解码技术零基础学习方法](http://blog.csdn.net/leixiaohua1020/article/details/18893769)
\ No newline at end of file
diff --git "a/AudioVideo/345円270円270円350円247円201円346円265円201円345円252円222円344円275円223円345円215円217円350円256円256円.md" "b/AudioVideo/345円270円270円350円247円201円346円265円201円345円252円222円344円275円223円345円215円217円350円256円256円.md"
new file mode 100644
index 00000000..7cb5b7a2
--- /dev/null
+++ "b/AudioVideo/345円270円270円350円247円201円346円265円201円345円252円222円344円275円223円345円215円217円350円256円256円.md"
@@ -0,0 +1,33 @@
+## 常见流媒体协议
+
+流媒体协议是服务器与客户端之间通信遵循的规定。当前网络上主要的流媒体协议如表所示。
+
+| 名称 | 推出机构 | 传输层协议 | 客户端 | 目前使用领域 |
+| -------- | -------------- | ------- | -------- | -------- |
+| RTSP+RTP | IETF | TCP+UDP | VLC, WMP | IPTV |
+| RTMP | Adobe Inc. | TCP | Flash | 互联网直播 |
+| RTMFP | Adobe Inc. | UDP | Flash | 互联网直播 |
+| MMS | Microsoft Inc. | TCP/UDP | WMP | 互联网直播+点播 |
+| HTTP | WWW+IETF | TCP | Flash | 互联网点播 |
+
+RTSP+RTP经常用于IPTV领域。因为其采用UDP传输视音频,支持组播,效率较高。但其缺点是网络不好的情况下可能会丢包,影响视频观看质量。因而围绕IPTV的视频质量的研究还是挺多的。
+
+RTSP规范可参考:[RTSP协议学习笔记](http://blog.csdn.net/leixiaohua1020/article/details/11955341)
+
+RTSP+RTP系统中衡量服务质量可参考:[网络视频传输的服务质量(QoS)](http://blog.csdn.net/leixiaohua1020/article/details/11883393)
+
+上海IPTV码流分析结果可参考:[IPTV视频码流分析](http://blog.csdn.net/leixiaohua1020/article/details/11846761)
+
+因为互联网网络环境的不稳定性,RTSP+RTP较少用于互联网视音频传输。互联网视频服务通常采用TCP作为其流媒体的传输层协议,因而像RTMP,MMS,HTTP这类的协议广泛用于互联网视音频服务之中。这类协议不会发生丢包,因而保证了视频的质量,但是传输的效率会相对低一些。
+
+此外RTMFP是一种比较新的流媒体协议,特点是支持P2P。
+
+RTMP我做的研究相对多一些:比如[RTMP规范简单分析](http://blog.csdn.net/leixiaohua1020/article/details/11694129),或者[RTMP流媒体播放过程](http://blog.csdn.net/leixiaohua1020/article/details/11704355)
+
+相关工具的源代码分析:[RTMPdump源代码分析 1: main()函数[系列文章\]](http://blog.csdn.net/leixiaohua1020/article/details/12952977)
+
+RTMP协议学习:[RTMP流媒体技术零基础学习方法](http://blog.csdn.net/leixiaohua1020/article/details/15814587)
+
+### 参考资料:
+
+[视音频编解码技术零基础学习方法](http://blog.csdn.net/leixiaohua1020/article/details/18893769)
\ No newline at end of file
diff --git "a/AudioVideo/345円270円270円350円247円201円351円237円263円350円247円206円351円242円221円347円274円226円347円240円201円.md" "b/AudioVideo/345円270円270円350円247円201円351円237円263円350円247円206円351円242円221円347円274円226円347円240円201円.md"
new file mode 100644
index 00000000..524415e3
--- /dev/null
+++ "b/AudioVideo/345円270円270円350円247円201円351円237円263円350円247円206円351円242円221円347円274円226円347円240円201円.md"
@@ -0,0 +1,110 @@
+## 常见音视频编码
+
+### 1. 视频编码
+
+视频编码的主要作用是将视频像素数据(RGB,YUV等)压缩成为视频码流,从而降低视频的数据量。如果视频不经过压缩编码的话,体积通常是非常大的,一部电影可能就要上百G的空间。视频编码是视音频技术中最重要的技术之一。视频码流的数据量占了视音频总数据量的绝大部分。高效率的视频编码在同等的码率下,可以获得更高的视频质量。
+
+视频编码的简单原理可以参考:[视频压缩编码和音频压缩编码的基本原理](http://blog.csdn.net/leixiaohua1020/article/details/28114081)
+
+注:视频编码技术在整个视音频技术中应该是最复杂的技术。如果没有基础的话,可以先买一些书看一下原理,比如说《现代电视原理》《数字电视广播原理与应用》(本科的课本)中的部分章节。
+
+主要视频编码一览
+
+| 名称 | 推出机构 | 推出时间 | 目前使用领域 |
+| ----------- | -------------- | ---- | ------ |
+| HEVC(H.265) | MPEG/ITU-T | 2013 | 研发中 |
+| H.264 | MPEG/ITU-T | 2003 | 各个领域 |
+| MPEG4 | MPEG | 2001 | 不温不火 |
+| MPEG2 | MPEG | 1994 | 数字电视 |
+| VP9 | Google | 2013 | 研发中 |
+| VP8 | Google | 2008 | 不普及 |
+| VC-1 | Microsoft Inc. | 2006 | 微软平台 |
+
+由表可见,有两种视频编码方案是最新推出的:VP9和HEVC。目前这两种方案都处于研发阶段,还没有到达实用的程度。当前使用最多的视频编码方案就是H.264。
+
+#### **1.1 主流编码标准**
+
+H.264仅仅是一个编码标准,而不是一个具体的编码器,H.264只是给编码器的实现提供参照用的。
+
+基于H.264标准的编码器还是很多的,究竟孰优孰劣?可参考:[MSU出品的 H.264编码器比较(2011.5)](http://blog.csdn.net/leixiaohua1020/article/details/12373947)
+
+在学习视频编码的时候,可能会用到各种编码器(实际上就是一个exe文件),他们常用的编码命令可以参考:[各种视频编码器的命令行格式](http://blog.csdn.net/leixiaohua1020/article/details/11705495)
+
+学习H.264最标准的源代码,就是其官方标准JM了。但是要注意,JM速度非常的慢,是无法用于实际的:[H.264参考软件JM12.2RC代码详细流程](http://blog.csdn.net/leixiaohua1020/article/details/11980219)
+
+实际中使用最多的就是x264了,性能强悍(超过了很多商业编码器),而且开源。其基本教程网上极多,不再赘述。编码时候可参考:[x264编码指南——码率控制](http://blog.csdn.net/leixiaohua1020/article/details/12720135)。编码后统计值的含义:[X264输出的统计值的含义(X264 Stats Output)](http://blog.csdn.net/leixiaohua1020/article/details/11884559)
+
+Google推出的VP8属于和H.264同一时代的标准。总体而言,VP8比H.264要稍微差一点。有一篇写的很好的VP8的介绍文章:[深入了解 VP8](http://blog.csdn.net/leixiaohua1020/article/details/12760173)。除了在技术领域,VP8和H.264在专利等方面也是打的不可开交,可参考文章:[WebM(VP8) vs H.264](http://blog.csdn.net/leixiaohua1020/article/details/12720237)
+
+此外,我国还推出了自己的国产标准AVS,性能也不错,但目前比H.264还是要稍微逊色一点。不过感觉我国在视频编解码领域还算比较先进的,可参考:[视频编码国家标准AVS与H.264的比较(节选)](http://blog.csdn.net/leixiaohua1020/article/details/12851745)
+
+近期又推出了AVS新一代的版本AVS+,具体的性能测试还没看过。不过据说AVS+得到了国家政策上非常强力的支持。
+
+#### **1.2 下一代编码标准**
+
+下一代的编解码标准就要数HEVC和VP9了。VP9是Google继VP8之后推出的新一代标准。VP9和HEVC相比,要稍微逊色一些。它们的对比可参考:(1)[HEVC与VP9编码效率对比](http://blog.csdn.net/leixiaohua1020/article/details/11713041) (2)[HEVC,VP9,x264性能对比](http://blog.csdn.net/leixiaohua1020/article/details/19014955)
+
+HEVC在未来拥有很多大的优势,可参考:[HEVC将会取代H.264的原因](http://blog.csdn.net/leixiaohua1020/article/details/11844949)
+
+学习HEVC最标准的源代码,就是其官方标准HM了。其速度比H.264的官方标准代码又慢了一大截,使用可参考:[HEVC学习—— HM的使用](http://blog.csdn.net/leixiaohua1020/article/details/12759297)
+
+未来实际使用的HEVC开源编码器很有可能是x265,目前该项目还处于发展阶段,可参考:[x265(HEVC编码器,基于x264)](http://blog.csdn.net/leixiaohua1020/article/details/13991351)[介绍](http://blog.csdn.net/leixiaohua1020/article/details/13991351)。x265的使用可以参考:[HEVC(H.265)标准的编码器(x265,DivX265)试用](http://blog.csdn.net/leixiaohua1020/article/details/18861635)
+
+主流以及下一代编码标准之间的比较可以参考文章:[视频编码方案之间的比较(HEVC,H.264,MPEG2等)](http://blog.csdn.net/leixiaohua1020/article/details/12237177)
+
+此外,在码率一定的情况下,几种编码标准的比较可参考:[限制码率的视频编码标准比较(包括MPEG-2,H.263,MPEG-4,以及 H.264)](http://blog.csdn.net/leixiaohua1020/article/details/12851975)
+
+结果大致是这样的:
+
+HEVC> VP9> H.264> VP8> MPEG4> H.263> MPEG2。
+
+截了一些图,可以比较直观的了解各种编码标准:
+
+HEVC码流简析:[HEVC码流简单分析](http://blog.csdn.net/leixiaohua1020/article/details/11845069)
+
+H.264码流简析:[H.264简单码流分析](http://blog.csdn.net/leixiaohua1020/article/details/11845625)
+
+MPEG2码流简析:[MPEG2简单码流分析](http://blog.csdn.net/leixiaohua1020/article/details/11846185)
+
+以上简析使用的工具:[视频码流分析工具](http://blog.csdn.net/leixiaohua1020/article/details/11845435)
+
+我自己做的小工具: [H.264码流分析器](http://blog.csdn.net/leixiaohua1020/article/details/17933821)
+
+### 2. 音频编码
+
+音频编码的主要作用是将音频采样数据(PCM等)压缩成为音频码流,从而降低音频的数据量。音频编码也是互联网视音频技术中一个重要的技术。但是一般情况下音频的数据量要远小于视频的数据量,因而即使使用稍微落后的音频编码标准,而导致音频数据量有所增加,也不会对视音频的总数据量产生太大的影响。高效率的音频编码在同等的码率下,可以获得更高的音质。
+
+音频编码的简单原理可以参考:[视频压缩编码和音频压缩编码的基本原理](http://blog.csdn.net/leixiaohua1020/article/details/28114081)
+
+主要音频编码一览
+
+| 名称 | 推出机构 | 推出时间 | 目前使用领域 |
+| ---- | -------------- | ---- | ------- |
+| AAC | MPEG | 1997 | 各个领域(新) |
+| AC-3 | Dolby Inc. | 1992 | 电影 |
+| MP3 | MPEG | 1993 | 各个领域(旧) |
+| WMA | Microsoft Inc. | 1999 | 微软平台 |
+
+由表可见,近年来并未推出全新的音频编码方案,可见音频编码技术已经基本可以满足人们的需要。音频编码技术近期绝大部分的改动都是在MP3的继任者——AAC的基础上完成的。
+
+这些编码标准之间的比较可以参考文章:[音频编码方案之间音质比较(AAC,MP3,WMA等)](http://blog.csdn.net/leixiaohua1020/article/details/11730661)
+
+结果大致是这样的:
+
+AAC+> MP3PRO> AAC> RealAudio> WMA> MP3
+
+AAC格式的介绍:[AAC格式简介](http://blog.csdn.net/leixiaohua1020/article/details/11822537)
+
+AAC几种不同版本之间的对比:[AAC规格(LC,HE,HEv2)及性能对比](http://blog.csdn.net/leixiaohua1020/article/details/11971419)
+
+AAC专利方面的介绍:[AAC专利介绍](http://blog.csdn.net/leixiaohua1020/article/details/11854587)
+
+此外杜比数字的编码标准也比较流行,但是貌似比最新的AAC稍为逊色:[AC-3技术综述](http://blog.csdn.net/leixiaohua1020/article/details/11822737)
+
+我自己做的小工具:[ AAC格式分析器](http://blog.csdn.net/leixiaohua1020/article/details/18155549)
+
+
+
+### 参考资料:
+
+[视音频编解码技术零基础学习方法](http://blog.csdn.net/leixiaohua1020/article/details/18893769)
\ No newline at end of file
diff --git "a/AudioVideo/351円200円232円347円224円250円350円247円206円351円242円221円350円247円243円347円240円201円346円222円255円346円224円276円346円265円201円347円250円213円.md" "b/AudioVideo/351円200円232円347円224円250円350円247円206円351円242円221円350円247円243円347円240円201円346円222円255円346円224円276円346円265円201円347円250円213円.md"
new file mode 100644
index 00000000..6f35f5d3
--- /dev/null
+++ "b/AudioVideo/351円200円232円347円224円250円350円247円206円351円242円221円350円247円243円347円240円201円346円222円255円346円224円276円346円265円201円347円250円213円.md"
@@ -0,0 +1,18 @@
+---
+typora-copy-images-to: ./image
+---
+
+## 通用视频解码播放流程
+
+**通用的网络视频播放流程:**
+
+1. 从网络数据流中获得视频数据流。
+2. 将视频数据流解析成压缩音频数据和压缩视频数据。
+3. 分别对音频和视频解码获取原始(采样)数据。
+4. 经过同步策略后,有序的将原始(采样)数据输出到指定设备播放。
+
+
+
+### 参考资料:
+
+[视音频编解码技术零基础学习方法](http://blog.csdn.net/leixiaohua1020/article/details/18893769)
\ No newline at end of file
diff --git a/Course/Markdown/README.md b/Course/Markdown/README.md
new file mode 100644
index 00000000..a35826e3
--- /dev/null
+++ b/Course/Markdown/README.md
@@ -0,0 +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)
diff --git a/Course/Markdown/markdown-html.md b/Course/Markdown/markdown-html.md
new file mode 100644
index 00000000..ec82fc0f
--- /dev/null
+++ b/Course/Markdown/markdown-html.md
@@ -0,0 +1,3 @@
+# Markdown 网页格式兼容
+
+Markdown 作为一种标记型语言,在大多数情况下都是需要转换为 HTML 格式的,所以 Markdown 理论上是兼容 HTML 语法的,在 Markdown 所提供的标记无法满足我们需要的时候,可以尝试使用 HTML 相关语法来实现。
\ No newline at end of file
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 @@
-
-
-
+
+
+
diff --git a/CustomView/Advance/[01]CustomViewProcess.md b/CustomView/Advance/[01]CustomViewProcess.md
index 5166f06f..70e4a5be 100644
--- a/CustomView/Advance/[01]CustomViewProcess.md
+++ b/CustomView/Advance/[01]CustomViewProcess.md
@@ -29,7 +29,7 @@
> 例如:制作一个支持自动加载网络图片的ImageView,制作图表等。
-**PS: 自定义View在大多数情况下都有替代方案,利用图片或者组合动画来实现,但是使用后者可能会面临内存耗费过大,制作麻烦更诸多问题。**
+**PS: 自定义View在大多数情况下都有替代方案,利用图片或者组合动画来实现,但是使用后者可能会面临内存耗费过大,制作麻烦等诸多问题。**
*******
diff --git a/CustomView/Advance/[02]Canvas_BasicGraphics.md b/CustomView/Advance/[02]Canvas_BasicGraphics.md
index 86336f5e..60143dc4 100644
--- a/CustomView/Advance/[02]Canvas_BasicGraphics.md
+++ b/CustomView/Advance/[02]Canvas_BasicGraphics.md
@@ -111,22 +111,22 @@ Canvas我们可以称之为画布,能够在上面绘制各种东西,是安
******
### 绘制矩形:
-确定确定一个矩形最少需要四个数据,就是**对角线的两个点**的坐标值,这里一般采用**左上角和右下角**的两个点的坐标。
+我们都知道,确定一个矩形最少需要四个数据,就是**对角线的两个点**的坐标值,这里一般采用**左上角和右下角**的两个点的坐标。
关于绘制矩形,Canvas提供了三种重载方法,第一种就是提供**四个数值(矩形左上角和右下角两个点的坐标)来确定一个矩形**进行绘制。
其余两种是先将矩形封装为**Rect或RectF**(实际上仍然是用两个坐标点来确定的矩形),然后传递给Canvas绘制,如下:
``` java
- // 第一种
- canvas.drawRect(100,100,800,400,mPaint);
+// 第一种
+canvas.drawRect(100,100,800,400,mPaint);
- // 第二种
- Rect rect = new Rect(100,100,800,400);
- canvas.drawRect(rect,mPaint);
+// 第二种
+Rect rect = new Rect(100,100,800,400);
+canvas.drawRect(rect,mPaint);
- // 第三种
- RectF rectF = new RectF(100,100,800,400);
- canvas.drawRect(rectF,mPaint);
+// 第三种
+RectF rectF = new RectF(100,100,800,400);
+canvas.drawRect(rectF,mPaint);
```
以上三种方法所绘制出来的结果是完全一样的。
@@ -192,7 +192,7 @@ Canvas我们可以称之为画布,能够在上面绘制各种东西,是安
******
### 绘制椭圆:
-相对于绘制圆角矩形,绘制椭圆就简单的多了,因为他只需要一个矩形矩形作为参数:
+相对于绘制圆角矩形,绘制椭圆就简单的多了,因为他只需要一个矩形作为参数:
``` java
// 第一种
diff --git a/CustomView/Advance/[03]Canvas_Convert.md b/CustomView/Advance/[03]Canvas_Convert.md
index 2e1c8206..4ad9f63f 100644
--- a/CustomView/Advance/[03]Canvas_Convert.md
+++ b/CustomView/Advance/[03]Canvas_Convert.md
@@ -77,15 +77,15 @@
缩放比例(sx,sy)取值范围详解:
-| 取值范围(n) | 说明 |
-| -------- | -------------------------- |
-| [-∞, -1) | 先根据缩放中心放大n倍,再根据中心轴进行翻转 |
-| -1 | 根据缩放中心轴进行翻转 |
-| (-1, 0) | 先根据缩放中心缩小到n,再根据中心轴进行翻转 |
-| 0 | 不会显示,若sx为0,则宽度为0,不会显示,sy同理 |
-| (0, 1) | 根据缩放中心缩小到n |
-| 1 | 没有变化 |
-| (1, +∞) | 根据缩放中心放大n倍 |
+| 取值范围(n) | 说明 |
+| ----------- | ---------------------------------------------- |
+| (-∞, -1) | 先根据缩放中心放大n倍,再根据中心轴进行翻转 |
+| -1 | 根据缩放中心轴进行翻转 |
+| (-1, 0) | 先根据缩放中心缩小到n,再根据中心轴进行翻转 |
+| 0 | 不会显示,若sx为0,则宽度为0,不会显示,sy同理 |
+| (0, 1) | 根据缩放中心缩小到n |
+| 1 | 没有变化 |
+| (1, +∞) | 根据缩放中心放大n倍 |
如果在缩放时稍微注意一下就会发现缩放的中心默认为坐标原点,而缩放中心轴就是坐标轴,如下:
@@ -181,6 +181,9 @@
调用两次缩放则 x轴实际缩放为0.5x0.5=0.25 y轴实际缩放为0.5x0.1=0.05
下面我们利用这一特性制作一个有趣的图形。
+
+> 注意设置画笔模式为描边(STROKE)
+
``` java
// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2, mHeight / 2);
@@ -324,7 +327,7 @@ Y = sy * x + y
#### (5)快照(save)和回滚(restore)
-Q: 为什存在快照与回滚
+Q: 为什么存在快照与回滚
A:画布的操作是不可逆的,而且很多画布操作会影响后续的步骤,例如第一个例子,两个圆形都是在坐标原点绘制的,而因为坐标系的移动绘制出来的实际位置不同。所以会对画布的一些状态进行保存和回滚。
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
diff --git a/CustomView/Advance/[05]Path_Basic.md b/CustomView/Advance/[05]Path_Basic.md
index 85f12f53..58e9c2d2 100644
--- a/CustomView/Advance/[05]Path_Basic.md
+++ b/CustomView/Advance/[05]Path_Basic.md
@@ -36,7 +36,7 @@
**请关闭硬件加速,以免引起不必要的问题!
请关闭硬件加速,以免引起不必要的问题!
请关闭硬件加速,以免引起不必要的问题!**
-**在AndroidMenifest文件中application节点下添上 android:hardwareAccelerated="false"以关闭整个应用的硬件加速。
更多请参考这里:[Android的硬件加速及可能导致的问题](https://github.com/GcsSloop/AndroidNote/issues/7)**
+**在AndroidMainfest文件中application节点下添上 android:hardwareAccelerated="false"以关闭整个应用的硬件加速。
更多请参考这里:[Android的硬件加速及可能导致的问题](https://github.com/GcsSloop/AndroidNote/issues/7)**
## Path作用
本次特地开了一篇详细讲解Path,为什么要单独摘出来呢,这是因为Path在2D绘图中是一个很重要的东西。
@@ -60,10 +60,10 @@ _The Path class encapsulates compound (multiple contour) geometric paths consist
另外路径有开放和封闭的区别。
-| 图像 | 名称 | 备注 |
-| ---------------------------------------- | ---- | ------------- |
+| 图像 | 名称 | 备注 |
+| ------------------------------------------------------------ | -------- | -------------------------- |
|  | 封闭路径 | 首尾相接形成了一个封闭区域 |
-|  | 开放路径 | 没有首位相接形成封闭区域 |
+|  | 开放路径 | 没有首尾相接形成封闭区域 |
> 这个是我随便画的,仅为展示一下区别,请无视我灵魂画师一般的绘图水准。
@@ -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
diff --git a/CustomView/Advance/[06]Path_Bezier.md b/CustomView/Advance/[06]Path_Bezier.md
index 4384d9c2..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中非常非常非常重要的内容-贝塞尔曲线。
******
@@ -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/)练习贝塞尔曲线,可以加深对贝塞尔曲线的理解程度。
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愉快的玩耍。( ̄▽ ̄)
(,,• 3 •,,)
#### PS: 由于本人水平有限,某些地方可能存在误解或不准确,如果你对此有疑问可以提交Issues进行反馈。
diff --git a/CustomView/Advance/[08]Path_Play.md b/CustomView/Advance/[08]Path_Play.md
index 3c6e342c..35387d5f 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,11 +232,9 @@ getSegment 用于获取Path的一个片段,方法如下: - - ### 4.nextContour -我们知道 Path 可以由多条曲线构成,但不论是 getLength , getgetSegment 或者是其它方法,都只会在其中第一条线段上运行,而这个 `nextContour` 就是用于跳转到下一条曲线到方法,_如果跳转成功,则返回 true, 如果跳转失败,则返回 false。_ +我们知道 Path 可以由多条曲线构成,但不论是 getLength , getSegment 或者是其它方法,都只会在其中第一条线段上运行,而这个 `nextContour` 就是用于跳转到下一条曲线到方法,_如果跳转成功,则返回 true, 如果跳转失败,则返回 false。_ 如下,我们创建了一个 Path 并使其中包含了两个闭合的曲线,内部的边长是200,外面的边长是400,现在我们使用 PathMeasure 分别测量两条曲线的总长度。 @@ -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)
-[]()
-
-
-
-
-
diff --git a/CustomView/Advance/[09]Matrix_Basic.md b/CustomView/Advance/[09]Matrix_Basic.md
index b4f4b346..e3985de3 100644
--- a/CustomView/Advance/[09]Matrix_Basic.md
+++ b/CustomView/Advance/[09]Matrix_Basic.md
@@ -1,52 +1,18 @@
-# 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呢? 举一个简单的例子:**
@@ -54,8 +20,7 @@ $$)
以下图为例,我们的内容区和屏幕坐标系还相差一个通知栏加一个标题栏的距离,所以两者是不重合的,我们在内容区的坐标系中的内容最终绘制的时候肯定要转换为实际的物理坐标系来绘制,Matrix在此处的作用就是转换这些数值。
->
-假设通知栏高度为20像素,导航栏高度为40像素,那么我们在内容区的(0,0)位置绘制一个点,最终就要转化为在实际坐标系中的(0,60)位置绘制一个点。
+> 假设通知栏高度为20像素,导航栏高度为40像素,那么我们在内容区的(0,0)位置绘制一个点,最终就要转化为在实际坐标系中的(0,60)位置绘制一个点。

@@ -65,13 +30,11 @@ $$)
### Matrix特点
-* 作用范围更广,Matrix在View,图片,动画效果等各个方面均有运用,相比与之前讲解等画布操作应用范围更广。
-* 更加灵活,画布操作是对Matrix的封装,Matrix作为更接近底层的东西,必然要比画布操作更加灵活。
-* 封装很好,Matrix本身对各个方法就做了很好的封装,让开发者可以很方便的操作Matrix。
-*
-* 难以深入理解,很难理解中各个数值的意义,以及操作规律,如果不了解矩阵,也很难理解前乘,后乘。
+- 作用范围更广,Matrix在View,图片,动画效果等各个方面均有运用,相比与之前讲解等画布操作应用范围更广。
+- 更加灵活,画布操作是对Matrix的封装,Matrix作为更接近底层的东西,必然要比画布操作更加灵活。
+- 封装很好,Matrix本身对各个方法就做了很好的封装,让开发者可以很方便的操作Matrix。
+- 难以深入理解,很难理解中各个数值的意义,以及操作规律,如果不了解矩阵,也很难理解前乘,后乘。
-
### 常见误解
**1.认为Matrix最下面的一行的三个参数(MPERSP_0、MPERSP_1、MPERSP_2)没有什么太大的作用,在这里只是为了凑数。**
@@ -82,9 +45,6 @@ $$)
的确,更改MPERSP_2的值能够达到类似缩放的效果,但这是因为齐次坐标的缘故,并非这个参数的实际功能。
-******
-
-
## Matrix基本原理
Matrix 是一个矩阵,最根本的作用就是坐标转换,下面我们就看看几种常见变换的原理:
@@ -105,46 +65,18 @@ Matrix 是一个矩阵,最根本的作用就是坐标转换,下面我们就
### 1.缩放(Scale)
-
-
-
-
+
用矩阵表示:
-
+
+> 你可能注意到了,我们坐标多了一个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这一误解就源于此**)。
图例:
@@ -156,37 +88,11 @@ $$)
#### 水平错切
-
-
-
+
用矩阵表示:
-
+
图例:
@@ -194,37 +100,11 @@ $$)
#### 垂直错切
-
-
-
+
用矩阵表示:
-
+
图例:
@@ -234,37 +114,11 @@ $$)
> 水平错切和垂直错切的复合。
-
-
-
+
用矩阵表示:
-
+
图例:
@@ -274,102 +128,30 @@ $$)
假定一个点 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 ]
-$$)
+
图例:

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

-
-
## Matrix复合原理
其实Matrix的多种复合操作都是使用矩阵乘法实现的,从原理上理解很简单,但是,使用矩阵乘法也有其弱点,后面的操作可能会影响到前面到操作,所以在构造Matrix时顺序很重要。
@@ -379,14 +161,16 @@ $$)
### 前乘(pre)
前乘相当于矩阵的右乘:
-
+
+
> 这表示一个矩阵与一个特殊矩阵前乘后构造出结果矩阵。
### 后乘(post)
-前乘相当于矩阵的左乘:
-
+后乘相当于矩阵的左乘:
+
+
> 这表示一个矩阵与一个特殊矩阵后乘后构造出结果矩阵。
@@ -396,163 +180,261 @@ $$)
## 组合
-我们使用Matrix最终目的就是让视图显示为我们想要的状态,为此我们可能需要多种操作结合使用。
+**关于 Matrix 的文章终有一个问题,就是 pre 和 post 这一部分的理论非常别扭,国内大多数文章都是这样的,看起来貌似是对的但很难理解,部分内容违背直觉。**
-我发现很多讲解Matrix的文章喜欢用绕某一个点缩放(旋转)的示例来讲解,如下:
+**我由于也受到了这些文章的影响,自然而然的继承了这一理论,直到在评论区有一位小伙伴提出了一个问题,才让我重新审视了这一部分的内容,并进行了一定反思。**
+经过良久的思考之后,我决定抛弃国内大部分文章的那套理论和结论,只用严谨的数学逻辑和程序逻辑来阐述这一部分的理论,也许仍有疏漏,如有发现请指正。
+**首先澄清两个错误结论,记住,是错误结论,错误结论,错误结论。**
+
+### ~~错误结论一: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
+
+```
+由于上面例子中原始矩阵(M)是一个单位矩阵(I),所以可得:
+
+// 第一段 pre
+M' = (M*T)*R = I*T*R = T*R
+
+// 第二段 post
+M' = T*(R*M) = T*R*I = T*R
+```
+
+由于两者最终的化简公式是相同的,所以两者是等价的,但是,这结论不具备普适性。
+
+**即原始矩阵不为单位矩阵的时候,两者无法化简为相同的公式,结果自然也会不同。另外,执行顺序就是程序书写顺序,不存在所谓的正序逆序。**
+
+### ~~错误结论二:pre 是先执行,而 post 是后执行。~~
-首先,**这个思路是没有任何问题的,也是实现绕某一点操作的核心原理**,但这可能会对一部分小白造成误解,认为只能这样实现,然而查看一下Matrix的方法表就能知道四大操作都可以指定中心点,所以,上面的三行代码用一行就能完成:
+这一条结论比上一条更离谱。
+
+之所以产生这个错误完全是因为写文章的人懂英语。
+
+```
+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时,个人建议尽量全部使用后乘或者全部使用前乘,这样操作顺序容易确定,出现问题也比较容易排查。
当然,由于矩阵乘法不满足交换律,前乘和后乘的结果是不同的,使用时应结合具体情景分析使用。**
+同样化简公式:
-### Pre与Post的区别
+```
+// 矩阵乘法满足结合律
+M‘ = T*(M*R) = T*M*R = (T*M)*R
+```
-主要区别其实就是矩阵的乘法顺序不同,pre相当于矩阵的右乘,而post相当于矩阵的左乘。
+**从实际上来说,由于矩阵乘法满足结合律,所以不论你说是靠右先执行还是靠左先执行,从结果上来说都没有错。**
-以下观点存在歧义,故做删除标注:
+**之前基于这条错误的结论我进行了一次错误的证明:**
-(削除)
-在图像处理中,越靠近右边的矩阵越先执行,所以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影响计算顺序? 实则不然,下面我们用一个例子说明:
+上面的论证是完全错误的,因为可以轻松举出反例:
->
```java
Matrix matrix = new Matrix();
-matrix.postScale(0.5f, 0.8f);
+matrix.preScale(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操作会后执行这一说法**。
+反例中,虽然将 `postScale` 改为了 `preScale` ,但两者结果是完全相同的,所以先后论根本就是错误的。
+
+他们结果相同是因为最终化简公式是相同的,都是 S*T
+
+之所以平移距离是 MTRANS\_X = 500,MTRANS\_Y = 800,那是因为执行 Translate 之前 Matrix 已经具有了一个缩放比例。在右乘的时候影响到了具体的数值计算,可以用矩阵乘法计算一下。
+
-### 下面我们用不同对方式来构造一个矩阵:
+最终结果为:
-**假设我们需要先缩放再平移。**
+
+
+当 T*S 的时候,缩放比例则不会影响到 MTRANS\\_X 和 MTRANS\\_Y ,具体可以使用矩阵乘法自己计算一遍。
+
+## 如何理解和使用 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后的矩阵,是一个单位矩阵。
+- 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();
-m.preTranslate(tx, ty); //使用pre,越靠后越先执行。
+m.preTranslate(tx, ty);
m.preScale(sx, sy);
```
用矩阵表示:
-
-
+
#### 2.仅用post:
-``` java
+```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
+```java
+// 混合 M‘ = T*M*S = T*S
Matrix m = new Matrix();
m.reset();
m.preScale(sx, sy);
@@ -561,7 +443,8 @@ m.postTranslate(tx, ty);
或:
-``` java
+```java
+// 混合 M‘ = T*M*S = T*S
Matrix m = new Matrix();
m.reset();
m.postTranslate(tx, ty);
@@ -572,46 +455,12 @@ m.preScale(sx, sy);
用矩阵表示:
-
-
-
-**注意: 由于矩阵乘法不满足交换律,请保证初始矩阵为空,如果初始矩阵不为空,则导致运算结果不同。**
-
-
-
+
+
+**注意: 由于矩阵乘法不满足交换律,请保证初始矩阵为单位矩阵,如果初始矩阵不为单位矩阵,则导致运算结果不同。**
+
+上面虽然用了很多不同的写法,但最终的化简公式是一样的,这些不同的写法,都是根据同一个公式反向推算出来的。
+
## Matrix方法表
这个方法表,暂时放到这里让大家看看,方法的使用讲解放在下一篇文章中。
@@ -627,22 +476,21 @@ $$)
| 特殊方法 | setPolyToPoly setRectToRect rectStaysRect setSinCos | 一些特殊操作 |
| 矩阵相关 | invert isAffine isIdentity | 求逆矩阵、 是否为仿射矩阵、 是否为单位矩阵 ... |
-
## 总结
对于Matrix重在理解,理解了其中的原理之后用起来将会更加得心应手。
-**学完了本篇之后,推荐配合鸿洋大大的视频课程 [
-打造个性的图片预览与多点触控](http://www.imooc.com/learn/239) 食用,定然能够让你对Matrix对理解更上一层楼。**
+学完了本篇之后,推荐配合鸿洋大大的视频课程 [
+打造个性的图片预览与多点触控](http://www.imooc.com/learn/239) 食用,定然能够让你对Matrix对理解更上一层楼。
+
+由于个人水平有限,文章中可能会出现错误,如果你觉得哪一部分有错误,或者发现了错别字等内容,欢迎在评论区告诉我,另外,据说关注 [作者微博](http://weibo.com/GcsSloop) 不仅能第一时间收到新文章消息,还能变帅哦。
-
-## About Me
+## About
-### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop)
+[本系列相关文章](http://www.gcssloop.com/customview/CustomViewIndex/)
-
+作者微博: [GcsSloop](http://weibo.com/GcsSloop)
-
## 参考资料
[Matrix](https://developer.android.com/reference/android/graphics/Matrix.html)
@@ -653,4 +501,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/)
-[]()
diff --git a/CustomView/Advance/[10]Matrix_Method.md b/CustomView/Advance/[10]Matrix_Method.md
index 6d7b0a0c..3254f4bb 100644
--- a/CustomView/Advance/[10]Matrix_Method.md
+++ b/CustomView/Advance/[10]Matrix_Method.md
@@ -40,16 +40,7 @@ Matrix matrix = new Matrix();
通过这种方式创建出来的并不是一个数值全部为空的矩阵,而是一个单位矩阵,如下:
-
-
+
#### 有参构造
diff --git a/CustomView/Advance/[11]Matrix_3D_Camera.md b/CustomView/Advance/[11]Matrix_3D_Camera.md
index 0e4c83b6..e59848c0 100644
--- a/CustomView/Advance/[11]Matrix_3D_Camera.md
+++ b/CustomView/Advance/[11]Matrix_3D_Camera.md
@@ -16,7 +16,7 @@
| 基本方法 | save、restore | 保存、 回滚 |
| 常用方法 | getMatrix、applyToCanvas | 获取Matrix、应用到画布 |
| 平移 | translate | 位移 |
-| 旋转 | rotat (API 12)、rotateX、rotateY、rotateZ | 各种旋转 |
+| 旋转 | rotate (API 12)、rotateX、rotateY、rotateZ | 各种旋转 |
| 相机位置 | setLocation (API 12)、getLocationX (API 16)、getLocationY (API 16)、getLocationZ (API 16) | 设置与获取相机位置 |
> Camera的方法并不是特别多,很多内容与之前的讲解的Canvas和Matrix类似,不过又稍有不同,之前的画布操作和Matrix主要是作用于2D空间,而Camera则主要作用于3D空间。
@@ -71,7 +71,9 @@

-> 摄像机的位置默认是 (0, 0, -576)。其中 -576= -8 x 72,虽然官方文档说距离屏幕的距离是 -8, 但经过测试实际距离是 -576 像素,当距离为 -10 的时候,实际距离为 -720 像素。不过这个数值72我也不明白是什么东西,我使用了3款手机测试,屏幕大小和像素密度均不同,但结果都是一样的,知道的小伙伴可以告诉我一声。
+> 摄像机的位置默认是 (0, 0, -576)。其中 -576= -8 x 72,虽然官方文档说距离屏幕的距离是 -8, 但经过测试实际距离是 -576 像素,当距离为 -10 的时候,实际距离为 -720 像素。我使用了3款手机测试,屏幕大小和像素密度均不同,但结果都是一样的。
+>
+> 这个魔数可以在 Android 底层的图像引擎 Skia 中找到。在 Skia 中,Camera 的位置单位是英寸,英寸和像素的换算单位在 Skia 中被固定为 72 像素,而 Android 中把这个换算单位照搬了过来。
diff --git a/CustomView/Advance/[18]multi-touch.md b/CustomView/Advance/[18]multi-touch.md
new file mode 100644
index 00000000..98efa326
--- /dev/null
+++ b/CustomView/Advance/[18]multi-touch.md
@@ -0,0 +1,613 @@
+# Android 多点触控详解
+
+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**05**
+
+**取得事件索引(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(01) 个,所以抬起时触发的是第 2 根手指的抬起事件(删除线部分)。
+
+但是,如果第 2 个手指抬起后,落在屏幕上另外一个手指会怎样?经过测试,发现另外**落下的手指会替代之前第 2 个手指的位置,系统判定为 2(01),而不是顺延下去变成 3(02),并且原本第3个手指的index变为原来数值(02)**,但是如果继续落下其他的手指,数值则会顺延。
+
+**即手指抬起时的 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
diff --git a/CustomView/Advance/[19]gesture-detector.md b/CustomView/Advance/[19]gesture-detector.md
new file mode 100644
index 00000000..94e60e18
--- /dev/null
+++ b/CustomView/Advance/[19]gesture-detector.md
@@ -0,0 +1,386 @@
+# Android 手势检测(GestureDetector)
+
+Android 手势检测,主要是 GestureDetector 相关内容的用法和注意事项,本文依旧属于事件处理这一体系,部分内容会涉及到之前文章提及过的知识点,如果你没看过之前的文章,可以到 [自定义 View 系列](http://www.gcssloop.com/customview/CustomViewIndex) 来查看这些内容。
+
+在开发 Android 手机应用过程中,可能需要对一些手势作出响应,如:单击、双击、长按、滑动、缩放等。这些都是很常用的手势。就拿最简单的双击来说吧,假如我们需要判断一个控件是否被双击(即在较短的时间内快速的点击两次),似乎是一个很容易的任务,但仔细考虑起来,要处理的细节问题也有不少,例如:
+
+1. **记录点击次数**,为了判断是否被点击超过 1 次,所以必须记录点击次数。
+2. **记录点击时间**,由于双击事件是较快速的点击两次,像点击一次后,过来几分钟再点击一次肯定不能算是双击事件,所以在记录点击次数的同时也要记录上一次的点击时间,我们可以设置本次点击距离上一次时间超过一定时间(例如:超过100ms)就不识别为双击事件。
+3. **点击状态重置**,在响应双击事件,或者判断不是双击事件的时候要重置计数器和上一次点击时间。重置既可以在点击的时候判断并进行重新设置,也可以使用定时器等超过一定时间后重置状态。
+
+这样看起来,判断一个双击事件就有这么多麻烦事情,更别其他的手势了,虽然这些看起来都很简单,但设计起来需要考虑的细节情况实在是太多了。
+
+那么有没有一种更好的方法来方便的检测手势呢?当然有啦,因为这些手势很常用,系统早就封装了一些方法给我们用,接下来我们就看看它们是如何使用的。
+
+## GestureDetector
+
+> GestureDetector 可以使用 MotionEvents 检测各种手势和事件。GestureDetector.OnGestureListener 是一个回调方法,在发生特定的事件时会调用 Listener 中对应的方法回调。这个类只能用于检测触摸事件的 MotionEvent,不能用于轨迹球事件。
+> (话说轨迹球已经消失多长时间了,估计很多人都没见过轨迹球这种东西)。
+>
+> 如何使用:
+>
+> - 创建一个 GestureDetector 实例。
+> - 在onTouchEvent(MotionEvent)方法中,确保调用 GestureDetector 实例的 onTouchEvent(MotionEvent)。回调中定义的方法将在事件发生时执行。
+> - 如果侦听 onContextClick(MotionEvent),则必须在 View 的 onGenericMotionEvent(MotionEvent)中调用 GestureDetector OnGenericMotionEvent(MotionEvent)。
+
+ GestureDetector 本身的方法比较少,使用起来也非常简单,下面让我们先看一下它的简单使用示例,分解开来大概需要三个步骤。
+
+```java
+// 1.创建一个监听回调
+SimpleOnGestureListener listener = new SimpleOnGestureListener() {
+ @Override public boolean onDoubleTap(MotionEvent e) {
+ Toast.makeText(MainActivity.this, "双击666", Toast.LENGTH_SHORT).show();
+ return super.onDoubleTap(e);
+ }
+};
+
+// 2.创建一个检测器
+final GestureDetector detector = new GestureDetector(this, listener);
+
+// 3.给监听器设置数据源
+view.setOnTouchListener(new View.OnTouchListener() {
+ @Override public boolean onTouch(View v, MotionEvent event) {
+ return detector.onTouchEvent(event);
+ }
+});
+```
+
+接下来我们先了解一下 GestureDetector 里面都有哪些内容。
+
+### 1. 构造函数
+
+GestureDetector 一共有 5 种构造函数,但有 2 种被废弃了,1 种是重复的,所以我们只需要关注其中的 2 种构造函数即可,如下:
+
+| 构造函数 |
+| ---------------------------------------- |
+| GestureDetector(Context context, GestureDetector.OnGestureListener listener) |
+| GestureDetector(Context context, GestureDetector.OnGestureListener listener, Handler handler) |
+
+第 1 种构造函数里面需要传递两个参数,上下文(Context) 和 手势监听器(OnGestureListener),这个很容易理解,就不再过多叙述,上面的例子中使用的就是这一种。
+
+第 2 种构造函数则需要多传递一个 Handler 作为参数,这个有什么作用呢?其实作用也非常简单,这个 Handler 主要是为了给 GestureDetector 提供一个 Looper。
+
+在通常情况下是不需这个 Handler 的,因为它会在内部自动创建一个 Handler 用于处理数据,如果你在主线程中创建 GestureDetector,那么它内部创建的 Handler 会自动获得主线程的 Looper,然而如果你在一个没有创建 Looper 的子线程中创建 GestureDetector 则需要传递一个带有 Looper 的 Handler 给它,否则就会因为无法获取到 Looper 导致创建失败。
+
+第 2 种构造函数使用方式如下(下面是两种在子线程中创建 GestureDetector 的方法):
+
+```java
+// 方式一、在主线程创建 Handler
+final Handler handler = new Handler();
+new Thread(new Runnable() {
+ @Override public void run() {
+ final GestureDetector detector = new GestureDetector(MainActivity.this, new
+ GestureDetector.SimpleOnGestureListener() , handler);
+ // ... 省略其它代码 ...
+ }
+}).start();
+
+// 方式二、在子线程创建 Handler,并且指定 Looper
+new Thread(new Runnable() {
+ @Override public void run() {
+ final Handler handler = new Handler(Looper.getMainLooper());
+ final GestureDetector detector = new GestureDetector(MainActivity.this, new
+ GestureDetector.SimpleOnGestureListener() , handler);
+ // ... 省略其它代码 ...
+ }
+}).start();
+```
+
+当然了,使用其它创建 Handler 的方式也是可以的,重点传递的 Handler 一定要有 Looper,敲黑板,重点是 Handler 中的 Looper。假如子线程准备了 Looper 那么可以直接使用第 1 种构造函数进行创建,如下:
+
+```java
+new Thread(new Runnable() {
+ @Override public void run() {
+ Looper.prepare(); // <- 重点在这里 + final GestureDetector detector = new GestureDetector(MainActivity.this, new + GestureDetector.SimpleOnGestureListener()); + // ... 省略其它代码 ... + } +}).start(); +``` + +### 2.手势监听器 + +既然是手势检测,自然要在对应的手势出现的时候通知调用者,最合适的自然是事件监听器模式。目前 GestureDetecotr 有四种监听器。 + +| 监听器 | 简介 | +| ---------------------------------------- | ---------------------------------------- | +| [OnContextClickListener](https://developer.android.com/reference/android/view/GestureDetector.OnContextClickListener.html) | 这个很容易让人联想到ContextMenu,然而它和ContextMenu并没有什么关系,它是在Android6.0(API 23)才添加的一个选项,是用于检测外部设备上的按钮是否按下的,例如蓝牙触控笔上的按钮,一般情况下,忽略即可。 | +| [OnDoubleTapListener](https://developer.android.com/reference/android/view/GestureDetector.OnDoubleTapListener.html) | 双击事件,有三个回调类型:双击(DoubleTap)、单击确认(SingleTapConfirmed) 和 双击事件回调(DoubleTapEvent) | +| [OnGestureListener](https://developer.android.com/reference/android/view/GestureDetector.OnGestureListener.html) | 手势检测,主要有以下类型事件:按下(Down)、 一扔(Fling)、长按(LongPress)、滚动(Scroll)、触摸反馈(ShowPress) 和 单击抬起(SingleTapUp) | +| [SimpleOnGestureListener](https://developer.android.com/reference/android/view/GestureDetector.SimpleOnGestureListener.html) | 这个是上述三个接口的空实现,一般情况下使用这个比较多,也比较方便。 | + +#### 2.1 OnContextClickListener + +由于 OnContextClickListener 主要是用于检测外部设备按钮的,关于它需要注意一点,如果侦听 onContextClick(MotionEvent),则必须在 View 的 onGenericMotionEvent(MotionEvent)中调用 GestureDetector 的 OnGenericMotionEvent(MotionEvent)。 + +由于目前我们用到这个监听器的场景并不多,所以也就不展开介绍了,重点关注后面几个监听器。 + +#### 2.2 OnDoubleTapListener + +这个很明显就是用于检测双击事件的,它有三个回调接口,分别是 onDoubleTap、onDoubleTapEvent 和 onSingleTapConfirmed。 + +##### **2.2.1 onDoubleTap 与 onSingleTapConfirmed** + +**如果你只想监听双击事件,那么只用关注 onDoubleTap 就行了,如果你同时要监听单击事件则需要关注 onSingleTapConfirmed 这个回调函数**。 + +有人可能会有疑问,监听单击事件为什么要使用 onSingleTapConfirmed,使用 OnClickListener 不行吗?从理论上是可行的,但是我并不推荐这样使用,主要有两个原因: +1.它们两个是存在一定冲突的,如果你看过 [事件分发机制详解](http://www.gcssloop.com/customview/dispatch-touchevent-source) 就会知道,如果想要两者同时被触发,则 setOnTouchListener 不能消费事件,如果 onTouchListener 消费了事件,就可能导致 OnClick 无法正常触发。 +2.需要同时监听单击和双击,则说明单击和双击后响应逻辑不同,然而使用 OnClickListener 会在双击事件发生时触发两次,这显然不是我们想要的结果。而使用 onSingleTapConfirmed 就不用考虑那么多了,你完全可以把它当成单击事件来看待,而且在双击事件发生时,onSingleTapConfirmed 不会被调用,这样就不会引发冲突。 + +如果你需要同时监听两种点击事件可以这样写: + +```java +GestureDetector detector = new GestureDetector(this, new GestureDetector + .SimpleOnGestureListener() { + @Override public boolean onSingleTapConfirmed(MotionEvent e) { + Toast.makeText(MainActivity.this, "单击", Toast.LENGTH_SHORT).show(); + return false; + } + @Override public boolean onDoubleTap(MotionEvent e) { + Toast.makeText(MainActivity.this, "双击", Toast.LENGTH_SHORT).show(); + return false; + } +}); +``` + +关于 onSingleTapConfirmed 原理也非常简单,这一个回调函数在单击事件发生后300ms后触发(注意,不是立即触发的),只有在确定不会有后续的事件后,既当前事件肯定是单击事件才触发 onSingleTapConfirmed,所以在进行点击操作时,onDoubleTap 和 onSingleTapConfirmed 只会有一个被触发,也就不存在冲突了。 + +当然,如果你对事件分发机制非常了解的话,随便怎么用都行,条条大路通罗马,我这里只是推荐一种最简单而且不容易出错的实现方案。 + +##### **2.2.2 onDoubleTapEvent** + +**有些细心的小伙伴可能注意到还有一个 onDoubleTapEvent 回调函数,它是干什么的呢?它在双击事件确定发生时会对第二次按下产生的 MotionEvent 信息进行回调。** + +至于为什么要存在这样的回调,就要涉及到另一个比较细致的问题了,那就是 onDoubleTap 的触发时间,如果你在这些函数被调用时打印一条日志,那么你会看到这样的信息: + +``` +GCS-LOG: onDoubleTap +GCS-LOG: onDoubleTapEvent - down +GCS-LOG: onDoubleTapEvent - move +GCS-LOG: onDoubleTapEvent - move +GCS-LOG: onDoubleTapEvent - up +``` + +通过观察这些信息你会发现它们的调用顺序非常有趣,首先是 onDoubleTap 被触发,之后依次触发 onDoubleTapEvent 的 down、move、up 等信息,为什么说它们有趣呢?是因为这样的调用顺序会引发两种猜想,第一种猜想是 onDoubleTap 是在第二次手指抬起(up)后触发的,而 onDoubleTapEvent 是一种延时回调。第二种猜想则是 onDoubleTap 在第二次手指按下(dowm)时触发,onDoubleTapEvent 是一种实时回调。 + +通过测试和观察源码发现第二种猜想是正确的,因为第二次按下手指时,即便不抬起也会触发 onDoubleTap 和 onDoubleTapEvent 的 down,而且源码中逻辑也表明 onDoubleTapEvent 是一种实时回调。 + +这就引发了另一个问题,双击的触发时间,虽然这是一个细微到很难让人注意到的问题,假如说我们想要在第二次按下抬起后才判定这是一个双击操作,触发后续的内容,则不能使用 onDoubleTap 了,需要使用 onDoubleTapEvent 来进行更细微的控制,如下: + +```java +final GestureDetector detector = new GestureDetector(MainActivity.this, new GestureDetector.SimpleOnGestureListener() { + @Override public boolean onDoubleTap(MotionEvent e) { + Logger.e("第二次按下时触发"); + return super.onDoubleTap(e); + } + + @Override public boolean onDoubleTapEvent(MotionEvent e) { + switch (e.getActionMasked()) { + case MotionEvent.ACTION_UP: + Logger.e("第二次抬起时触发"); + break; + } + return super.onDoubleTapEvent(e); + } +}); +``` + +如果你不需要控制这么细微的话,忽略即可(Logger 是我自己封装的日志库,忽略即可)。 + +#### 2.3 OnGestureListener + +这个是手势检测中较为核心的一个部分了,主要检测以下类型事件:按下(Down)、 一扔(Fling)、长按(LongPress)、滚动(Scroll)、触摸反馈(ShowPress) 和 单击抬起(SingleTapUp)。 + +##### 2.3.1 onDown + +```java +@Override public boolean onDown(MotionEvent e) { + return true; +} +``` + +看过前面的文章应该知道,down 在事件分发体系中是一个较为特殊的事件,为了保证事件被唯一的 View 消费,哪个 View 消费了 down 事件,后续的内容就会传递给该 View。如果我们想让一个 View 能够接收到事件,有两种做法: + +1、让该 View 可以点击,因为可点击状态会默认消费 down 事件。 + +2、手动消费掉 down 事件。 + +由于图片、文本等一些控件默认是不可点击的,所以我们要么声明它们的 clickable 为 true,要么在发生 down 事件是返回 true。所以 onDown 在这里的作用就很明显了,就是为了保证让该控件能拥有消费事件的能力,以接受后续的事件。 + +##### 2.3.2 onFling + +Failing 中文直接翻译过来就是一扔、抛、甩,最常见的场景就是在 ListView 或者 RecyclerView 上快速滑动时手指抬起后它还会滚动一段时间才会停止。onFling 就是检测这种手势的。 + +```java +@Override +public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float + velocityY) { + return super.onFling(e1, e2, velocityX, velocityY); +} +``` + +在 onFling 的回调中共有四个参数,分别是: + +| 参数 | 简介 | +| --------- | ------------------ | +| e1 | 手指按下时的 Event。 | +| e2 | 手指抬起时的 Event。 | +| velocityX | 在 X 轴上的运动速度(像素/秒)。 | +| velocityY | 在 Y 轴上的运动速度(像素/秒)。 | + +我们可以通过 e1 和 e2 获取到手指按下和抬起时的坐标、时间等相关信息,通过 velocityX 和 velocityY 获取到在这段时间内的运动速度,单位是像素/秒(即 1 秒内滑动的像素距离)。 + +这个我们自己用到的地方比较少,但是也可以帮助我们简单的做出一些有趣的效果,例如下面的这种弹球效果,会根据滑动的力度和方向产生不同的弹跳效果。 + + + +其实这种原理非常简单,简化之后如下: + +1. 记录 velocityX 和 velocityY 作为初始速度,之后不断让速度衰减,直至为零。 +2. 根据速度和当前小球的位置计算一段时间后的位置,并在该位置重新绘制小球。 +3. 判断小球边缘是否碰触控件边界,如果碰触了边界则让速度反向。 + +根据这三条基本的逻辑就可以做出比较像的弹球效果,[具体的Demo可以看这里](https://raw.githubusercontent.com/GcsSloop/AndroidNote/master/CustomView/Demo/FailingBall.zip)。 + +##### 2.3.3 onLongPress + +这个是检测长按事件的,即手指按下后不抬起,在一段时间后会触发该事件。 + +```java +@Override +public void onLongPress(MotionEvent e) { +} +``` + +##### 2.3.4 onScroll + +onScroll 就是监听滚动事件的,它看起来和 onFaling 比较像,不同的是,onSrcoll 后两个参数不是速度,而是滚动的距离。 + +```java +@Override +public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float + distanceY) { + return super.onScroll(e1, e2, distanceX, distanceY); +} +``` + +| 参数 | | +| --------- | ----------- | +| e1 | 手指按下时的Event | +| e2 | 手指抬起时的Event | +| distanceX | 在 X 轴上划过的距离 | +| distanceY | 在 Y 轴上划过的距离 | + +##### 2.3.5 onShowPress + +它是用户按下时的一种回调,主要作用是给用户提供一种视觉反馈,可以在监听到这种事件时可以让控件换一种颜色,或者产生一些变化,告诉用户他的动作已经被识别。 + +不过这个消息和 onSingleTapConfirmed 类似,也是一种延时回调,延迟时间是 180 ms,假如用户手指按下后立即抬起或者事件立即被拦截,时间没有超过 180 ms的话,这条消息会被 remove 掉,也就不会触发这个回调。 + +```java +@Override +public void onShowPress(MotionEvent e) { +} +``` + +##### 2.3.6 onSingleTapUp + +```java +@Override +public boolean onSingleTapUp(MotionEvent e) { + return super.onSingleTapUp(e); +} +``` + +这个也很容易理解,就是用户单击抬起时的回调,但是它和上面的 `onSingleTapConfirmed` 之间有何不同呢?和 `onClick` 又有何不同呢? + +单击事件触发: + +```java +GCS: onSingleTapUp +GCS: onClick +GCS: onSingleTapConfirmed +``` + +| 类型 | 触发次数 | 摘要 | +| -------------------- | ---- | ---- | +| onSingleTapUp | 1 | 单击抬起 | +| onSingleTapConfirmed | 1 | 单击确认 | +| onClick | 1 | 单击事件 | + +双击事件触发: + +```java +GCS: onSingleTapUp +GCS: onClick +GCS: onDoubleTap // <- 双击 +GCS: onClick +``` + +| 类型 | 触发次数 | 摘要 | +| -------------------- | ---- | ------------ | +| onSingleTapUp | 1 | 在双击的第一次抬起时触发 | +| onSingleTapConfirmed | 0 | 双击发生时不会触发。 | +| onClick | 2 | 在双击事件时触发两次。 | + +可以看出来这三个事件还是有所不同的,根据自己实际需要进行使用即可 + +#### 2.4 SimpleOnGestureListener + +这个里面并没有什么内容,只是对上面三种 Listener 的空实现,在上面的例子中使用的基本都是这监听器。因为它用起来更方便一点。 + +这主要是 GestureDetector 构造函数的设计问题,以只监听 OnDoubleTapListener 为例,如果想要使用 OnDoubleTapListener 接口则需要这样进行设置: + +```java +GestureDetector detector = new GestureDetector(this, new GestureDetector + .SimpleOnGestureListener()); +detector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() { + @Override public boolean onSingleTapConfirmed(MotionEvent e) { + Toast.makeText(MainActivity.this, "单击确认", Toast.LENGTH_SHORT).show(); + return false; + } + + @Override public boolean onDoubleTap(MotionEvent e) { + Toast.makeText(MainActivity.this, "双击", Toast.LENGTH_SHORT).show(); + return false; + } + + @Override public boolean onDoubleTapEvent(MotionEvent e) { + // Toast.makeText(MainActivity.this,"",Toast.LENGTH_SHORT).show(); + return false; + } +}); +``` + +既然都已经创建 SimpleOnGestureListener 了,再创建一个 OnDoubleTapListener 显然十分浪费,如果构造函数不使用 SimpleOnGestureListener,而是使用 OnGestureListener 的话,会多出几个无用的空实现,显然很浪费,所以在一般情况下,老老实实的使用 SimpleOnGestureListener 就好了。 + +### 3. 相关方法 + +除了各类监听器之外,与 GestureDetector 相关的方法其实并不多,只有几个,下面来简单介绍一下。 + +| 方法 | 摘要 | +| ----------------------- | ---------------------------------------- | +| setIsLongpressEnabled | 通过布尔值设置是否允许触发长按事件,true 表示允许,false 表示不允许。 | +| isLongpressEnabled | 判断当前是否允许触发长按事件,true 表示允许,false 表示不允许。 | +| onTouchEvent | 这个是其中一个重要的方法,在最开始已经演示过使用方式了。 | +| onGenericMotionEvent | 这个是在 API 23 之后才添加的内容,主要是为 OnContextClickListener 服务的,暂时不用关注。 | +| setContextClickListener | 设置 ContextClickListener 。 | +| setOnDoubleTapListener | 设置 OnDoubleTapListener 。 | + +### 结语 + +关于手势检测部分的 GestureDetector 相关内容基本就这么多了,其实手势检测还有一个 ScaleGestureDetector 也是为手势检测服务的,限于篇幅,本次就讲这么多吧。 + +其实手势检测辅助类 GestureDetector 本身并不是很复杂,带上注释等内容才不到1000行,感兴趣的可以自己研究一下实现方式。 + +## About Me + +### 作者微博: @GcsSloop
+
+
+
+## 参考资料
+
+[文档 · GestureDetector ](https://developer.android.com/reference/android/view/GestureDetector.html)
+[源码 · GestureDetector](https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/view/GestureDetector.java)
\ No newline at end of file
diff --git a/CustomView/Base/[03]Color.md b/CustomView/Base/[03]Color.md
index 86d3983c..300b9cc5 100644
--- a/CustomView/Base/[03]Color.md
+++ b/CustomView/Base/[03]Color.md
@@ -32,7 +32,7 @@ B(Blue) | 蓝色 | 无色 | 蓝色
*其中 A R G B 的取值范围均为0~255(即16进制的0x00~0xff)*
-A 从ox00到oxff表示从透明到不透明。
+A 从0x00到0xff表示从透明到不透明。
RGB 从0x00到0xff表示颜色从浅到深。
@@ -131,7 +131,7 @@ PicPick具备了截取全屏、活动窗口、指定区域、固定区域、手
**注意:**
-1.这里我们一般把每个通道的取值从0(ox00)到255(0xff)映射到0到1的浮点数表示。
+1.这里我们一般把每个通道的取值从0(0x00)到255(0xff)映射到0到1的浮点数表示。
2.这里等式右边的"绘制的颜色"、"Canvas上的原有颜色"都是经过预乘了自己的Alpha通道的值。如绘制颜色:0x88ffffff,那么参与运算时的每个颜色通道的值不是1.0,而是(1.0 * 0.5333 = 0.5333)。 (其中0.5333 = 0x88/0xff)
diff --git a/CustomView/CustomViewRule.md b/CustomView/CustomViewRule.md
new file mode 100644
index 00000000..766fcaa0
--- /dev/null
+++ b/CustomView/CustomViewRule.md
@@ -0,0 +1,48 @@
+# 自定义View基本法
+
+我们使用手机,是想要获取某些信息,而 View 是这些信息的直接展示界面,因为信息种类繁多,为了更好的展示这些信息, View 也必须有多种多样,Android 系统本身就给我们提供了不少类型的 View,但有时仍不能满足我们的需要,所以有时可能需要自定义 View 来完成任务。
+
+自定义 View 有许多需要注意的地方,关于这些需要注意的内容,我都会整理在这里,其名为《自定义 View 基本法》。
+
+#### 第一条:尽量避免自定义 View。
+
+由于 View 直接承载了与用户交互的重任,所以必须要考虑到各种情况,例如:
+
+* 当没有设置宽高属性时,View 默认应该多大。
+* 横竖屏转换时 View 可能重新设定大小,此时应如何处理。
+* View 因为特殊情况被销毁后重建,应如何保存和恢复数据。
+
+由于某些情况很特殊,触发条件也特殊,我们简单的实现了一个自定义了一个 View,可能在 99% 的情况下都是正常的,但在某些特殊情况下就会出问题。
+
+而系统提供给我们的组件都是经过千锤百炼的,基本上考虑到了各种特殊情况的处理,所以通常情况下,系统提供给我们的组件稳定性要好一些。所以我的建议是,能使用系统提供的组件的尽量使用系统的。
+
+除此之外,使用系统提供的组件也方便于其他人快速读懂项目,便于交流。
+
+#### 第二条:尽量避免从头开始。
+
+如果一定要使用自定义 View,那么尽量去继承系统已有的组件,并重写其中的部分方法,不要自己从头开始写。例如:图像相关的 View 可以考虑继承 ImageView,容器类 View 可以考虑继承 LinerLayout,RelativeLayout 等,原因同上。
+
+#### 第三条:处理特殊情况。
+
+针对能想到的一些特殊情况进行处理并且测试,尽量保证自定义 View 能适应各种特殊场景。
+
+#### 第四条:留下文档。
+
+**程序员有两大痛苦,1、别人写的项目居然没有文档。2、自己写的项目居然要写文档。**
+
+虽然有时候文档写起来确实挺麻烦,但是个人建议要留下一份文档,至少要为自己写的程序添加**有效的注释**,这不仅是方便他人,更重要的是方便自己以后修改项目。
+
+* 于自定义View而言,首先要标明这个自定义View是解决什么问题,有怎样的效果,使用的场景如何。
+* 其次要在关键部位标注实现原理。(例如:显示圆形的ImageView,要标明圆形是如何实现的,使用的是遮罩还是剪裁。)
+* 避免无效注释 (例如:在onDraw上面标注绘图),大家都知道这个是绘图,但绘制逻辑才是重点,要去标注绘制逻辑。
+
+#### 第五条:面向结果编程。
+
+我们既然使用自定义View,自然是想要实现一些系统组件无法实现的效果,所以要时刻谨记自己所需要的内容,让其中的所有逻辑都为这个结果服务,我自己实现自定义 View 一般有如下步骤:
+
+1. 原型,用我能想到的逻辑实现原型(demo),不管其代码复杂度,首先要得到结果,通常情况下,第一份代码可得性和整洁性都比较差。
+2. 优化,在原型的基础上对代码进行优化,剔除不必要的内容,例如尝试优化逻辑,对与一些重复性的内容抽取函数进行封装,想办法消灭一些中间变量。同时添加上必要注释,让其逻辑更加清晰易懂。
+3. 测试,对其进行场景测试,尽量保证其正常运行。
+
+
+
diff --git a/CustomView/Demo/FailingBall.zip b/CustomView/Demo/FailingBall.zip
new file mode 100644
index 00000000..92f0b8f5
Binary files /dev/null and b/CustomView/Demo/FailingBall.zip differ
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)
-
+
diff --git a/Lecture/README.md b/Lecture/README.md
new file mode 100644
index 00000000..959ec327
--- /dev/null
+++ b/Lecture/README.md
@@ -0,0 +1,4 @@
+# 演讲稿
+
+* [程序员练级指北(郑州GDG-2016DevFest)](https://github.com/GcsSloop/AndroidNote/blob/master/Lecture/gdg-developer-growth-guide.md)
+
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日 之间,全球大部分谷歌开发者社区都会举办该活动,一年一次。
diff --git a/OpenGL/README.md b/OpenGL/README.md
new file mode 100644
index 00000000..3f834828
--- /dev/null
+++ b/OpenGL/README.md
@@ -0,0 +1,3 @@
+# OpenGL
+
+OpenGL 全称 Open Graphics Library,是用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口(API)。OpenGL 常用于CAD、虚拟实境、科学可视化程序和电子游戏开发。
diff --git a/README.md b/README.md
index 73d11e95..1e8b8821 100644
--- a/README.md
+++ b/README.md
@@ -8,13 +8,14 @@
## [博客](http://www.gcssloop.com/#blog "GcsSloop的博客")
-新开的博客,在博客中可以获得更好的阅读体验,同时在博客的评论区可以更及时的向我反馈文章中的问题。
+我的个人博客,在博客中可以获得更好的阅读体验,同时在博客的评论区可以更及时的向我反馈文章中的问题。
******
+
## [自定义View](https://github.com/GcsSloop/AndroidNote/tree/master/CustomView/README.md)
* 基础篇
@@ -36,8 +37,29 @@
* [安卓自定义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)
+ * [安卓自定义View进阶 - 手势检测(GestureDetector)](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B19%5Dgesture-detector.md)
+ * [安卓自定义View进阶 - 缩放手势检测(ScaleGestureDetector)](http://www.gcssloop.com/customview/scalegesturedetector)
+ * [安卓自定义View进阶 - 画笔基础(Paint)](http://www.gcssloop.com/customview/paint-base)
+
+
+* [ViewSupport - 自定义View工具包](https://github.com/GcsSloop/ViewSupport)
+******
+
+## 雕虫晓技
+
+* [雕虫晓技(一) 组件化](http://www.gcssloop.com/gebug/componentr)
+* [雕虫晓技(二) 编码](http://www.gcssloop.com/gebug/coding)
+* [雕虫晓技(三) 通用圆角布局全解析](http://www.gcssloop.com/gebug/rclayout)
+* [雕虫晓技(四) 搭建私有Maven仓库(带容灾备份)](http://www.gcssloop.com/gebug/maven-private)
+* [雕虫晓技(五) 网格分页布局源码解析(上) (付费)](https://xiaozhuanlan.com/topic/5841730926)
+* [雕虫晓技(六) 网格分页布局源码解析(下) (付费)](https://xiaozhuanlan.com/topic/1456397082)
+* [雕虫晓技(七) 用旧Android手机做远程摄像头](http://www.gcssloop.com/gebug/internet-ip-webcam)
+* [雕虫晓技(八) Android与数据流的斗争](http://www.gcssloop.com/gebug/android-stream)
+* [雕虫晓技(九) Netty与私有协议框架](http://www.gcssloop.com/gebug/netty-private-protocol)
+* [雕虫晓技(十) Android超简单气泡效果](http://www.gcssloop.com/gebug/bubble-sample)
******
@@ -50,7 +72,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)
@@ -89,12 +111,20 @@
## 开源库
+* [arc-seekbar - 弧形SeekBar](https://github.com/GcsSloop/arc-seekbar)
+* [encrypt - 加密工具包](https://github.com/GcsSloop/encrypt)
+* [rclayout - 通用圆角布局](https://github.com/GcsSloop/rclayout)
* [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)
+------
+
+## 源码解析
+
+- [AtomicFile 源码解析](https://github.com/GcsSloop/AndroidNote/blob/master/SourceAnalysis/AtomicFile.md)
+
## 传送门
通往异世界的传送门,请谨慎使用。
@@ -130,6 +160,20 @@
* 商业用途请点击最下面图片联系本人。
* 微信公众号转载一律不授权 `原创` 标志。
+## 捐赠
+
+#### 如果你觉得我的文章有帮助的话,捐赠一些晶石,鼓励我继续研究! 🐾
+
+| | |
+| ---------------------------------------- | ---------------------------------------- |
+|  |  |
+
+## 交流群
+
+QQ群:612310796
+微信群:加我个人微信 GcsSloop,备注加群。
+
+
### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop)
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()) {
// 如果原文件存在且备份文件不存在,直接将原文件重命名为备份文件
diff --git a/SourceAnalysis/CircularArray.md b/SourceAnalysis/CircularArray.md
new file mode 100644
index 00000000..e69de29b
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是一个轻量级标记型书写语言,由于其学习门槛低和简洁性而被很多人喜爱。
-