├── AudioVideo ├── README.md ├── SDL.md ├── image │ ├── SDL数据结构.jpg │ ├── SDL流程.jpg │ ├── SDL结构.png │ ├── 视频解码播放流程.gliffy │ └── 视频解码播放流程.jpg ├── 常见封装格式.md ├── 常见封装格式概览.md ├── 常见流媒体协议.md ├── 常见音视频编码.md └── 通用视频解码播放流程.md ├── ChaosCrystal ├── ADB常用命令.md ├── AndroidStudio常用快捷键(Mac).md ├── Android中dip、dp、sp、pt和px.md ├── HowToViewAPISourceOnline.md ├── README.md └── 录屏与GIF制作.md ├── Course ├── HowToUsePlantUMLInAS.md ├── HowToUsePlantUMLInAS[Mac].md ├── Markdown │ ├── README.md │ ├── markdown-editor.md │ ├── markdown-grammar.md │ ├── markdown-html.md │ ├── markdown-link.md │ └── markdown-start.md ├── README.md ├── ReleaseLibraryByJitPack.md └── jitpack-javadoc.md ├── CustomView ├── Advance │ ├── Code │ │ ├── CheckView.java │ │ ├── CheckView.md │ │ ├── SearchView.java │ │ ├── SearchView.md │ │ ├── SetPolyToPoly.java │ │ └── SetPolyToPoly.md │ ├── Res │ │ ├── Checkmark.png │ │ └── poly_test.jpg │ ├── [01]CustomViewProcess.md │ ├── [02]Canvas_BasicGraphics.md │ ├── [03]Canvas_Convert.md │ ├── [04]Canvas_PictureText.md │ ├── [05]Path_Basic.md │ ├── [06]Path_Bezier.md │ ├── [07]Path_Over.md │ ├── [08]Path_Play.md │ ├── [09]Matrix_Basic.md │ ├── [10]Matrix_Method.md │ ├── [11]Matrix_3D_Camera.md │ ├── [12]Dispatch-TouchEvent-Theory.md │ ├── [15]Dispatch-TouchEvent-Source.md │ ├── [16]MotionEvent.md │ ├── [17]touch-matrix-region.md │ ├── [18]multi-touch.md │ ├── [19]gesture-detector.md │ └── [99]DrawText.md ├── Base │ ├── [01]CoordinateSystem.md │ ├── [02]AngleAndRadian.md │ └── [03]Color.md ├── CustomViewRule.md ├── Demo │ ├── FailingBall.zip │ ├── PieView.zip │ └── dispatchTouchEventDemo.zip └── README.md ├── Lecture ├── README.md └── gdg-developer-growth-guide.md ├── OpenGL └── README.md ├── QuickChart ├── Bezier.md ├── Canvas.md ├── Matrix.md ├── Path.md └── README.md ├── README.md └── SourceAnalysis ├── AtomicFile.md ├── CircularArray.md └── README.md /AudioVideo/README.md: -------------------------------------------------------------------------------- 1 | # Audio/Video 杂记 2 | 3 | - [通用视频解码播放流程](通用视频解码播放流程.md) 4 | 5 | -------------------------------------------------------------------------------- /AudioVideo/SDL.md: -------------------------------------------------------------------------------- 1 | --- 2 | typora-copy-images-to: ./image 3 | --- 4 | 5 | ## SDL 6 | 7 | ### 简介 8 | 9 | SDL(Simple DirectMedia Layer)库的作用就是封装了复杂的音视频底层交互工作,简化了音视频处理的难度。 10 | 11 | **特点:** 开源、跨平台。 12 | 13 | ### 结构 14 | 15 | ![SDL结构](image/SDL结构.png) 16 | 17 | 它是对底层进行了封装,最终还是调用的平台底层接口与硬件进行交互。 18 | 19 | ### SDL 流程 20 | 21 | ![SDL流程](image/SDL流程.jpg) 22 | 23 | ### SDL 主要函数 24 | 25 | | 函数 | 简介 | 26 | | -------------------- | -------------------------- | 27 | | SDL_Init() | 初始化 SDL 系统。 | 28 | | SDL_CreateWindow() | 创建窗口 SDL_Window。 | 29 | | SDL_CreateRenderer() | 创建渲染器 SDL_Renderer。 | 30 | | SDL_CreateTexture() | 创建纹理 SDL_Texture。 | 31 | | SDL_UpdateTexture() | 设置纹理数据。 | 32 | | SDL_RenderCopy() | 将纹理的数据拷贝给渲染器。 | 33 | | SDL_RenderPresent() | 显示。 | 34 | | SDL_Delay() | 工具函数,用于延时。 | 35 | | SDL_Quit() | 退出 SDL 系统。 | 36 | 37 | ### SDL 数据结构 38 | 39 | ![SDL数据结构](image/SDL数据结构.jpg) 40 | 41 | **数据结构简介:** 42 | 43 | | 结构 | 简介 | 44 | | ------------ | -------------------- | 45 | | SDL_Window | 代表一个“窗口”。 | 46 | | SDL_Renderer | 代表一个“渲染器”。 | 47 | | SDL_Texture | 代表一个“纹理”。 | 48 | | SDL_Rect | 一个简单的矩形结构。 | 49 | 50 | ### SDL 事件和多线程 51 | 52 | #### **SDL 多线程** 53 | 54 | | 函数 | 简介 | 55 | | ------------------ | -------------- | 56 | | SDL_CreateThread() | 创建一个线程。 | 57 | | SDL_Thread() | 线程的句柄。 | 58 | 59 | #### **SDL 事件** 60 | 61 | **函数:** 62 | 63 | | 函数 | 简介 | 64 | | --------------- | -------------- | 65 | | SDL_WaitEvent() | 等待一个事件。 | 66 | | SDL_PushEvent() | 发送一个事件。 | 67 | 68 | **数据结构:** 69 | 70 | | 结构 | 简介 | 71 | | ----------- | -------------- | 72 | | SDL_Event() | 代表一个事件。 | 73 | 74 | -------------------------------------------------------------------------------- /AudioVideo/image/SDL数据结构.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GcsSloop/AndroidNote/21ba33e3d40c7f462d3c7c90322f4318060fe3ff/AudioVideo/image/SDL数据结构.jpg -------------------------------------------------------------------------------- /AudioVideo/image/SDL流程.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GcsSloop/AndroidNote/21ba33e3d40c7f462d3c7c90322f4318060fe3ff/AudioVideo/image/SDL流程.jpg -------------------------------------------------------------------------------- /AudioVideo/image/SDL结构.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GcsSloop/AndroidNote/21ba33e3d40c7f462d3c7c90322f4318060fe3ff/AudioVideo/image/SDL结构.png -------------------------------------------------------------------------------- /AudioVideo/image/视频解码播放流程.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GcsSloop/AndroidNote/21ba33e3d40c7f462d3c7c90322f4318060fe3ff/AudioVideo/image/视频解码播放流程.jpg -------------------------------------------------------------------------------- /AudioVideo/常见封装格式.md: -------------------------------------------------------------------------------- 1 | ## 常见封装格式 2 | 3 | 封装格式的主要作用是把视频码流和音频码流按照一定的格式存储在一个文件中。现如今流行的封装格式如下表所示: 4 | 5 | 主要封装格式一览 6 | 7 | | 名称 | 推出机构 | 流媒体 | 支持的视频编码 | 支持的音频编码 | 目前使用领域 | 8 | | ---- | ------------------ | ---- | ----------------------------- | ------------------------------------ | --------- | 9 | | AVI | Microsoft Inc. | 不支持 | 几乎所有格式 | 几乎所有格式 | BT下载影视 | 10 | | MP4 | MPEG | 支持 | MPEG-2, MPEG-4, H.264, H.263等 | AAC, MPEG-1 Layers I, II, III, AC-3等 | 互联网视频网站 | 11 | | TS | MPEG | 支持 | MPEG-1, MPEG-2, MPEG-4, H.264 | MPEG-1 Layers I, II, III, AAC, | IPTV,数字电视 | 12 | | FLV | Adobe Inc. | 支持 | Sorenson, VP6, H.264 | MP3, ADPCM, Linear PCM, AAC等 | 互联网视频网站 | 13 | | MKV | CoreCodec Inc. | 支持 | 几乎所有格式 | 几乎所有格式 | 互联网视频网站 | 14 | | RMVB | Real Networks Inc. | 支持 | RealVideo 8, 9, 10 | AAC, Cook Codec, RealAudio Lossless | BT下载影视 | 15 | 16 | 由表可见,除了AVI之外,其他封装格式都支持流媒体,即可以“边下边播”。有些格式更“万能”一些,支持的视音频编码标准多一些,比如MKV。而有些格式则支持的相对比较少,比如说RMVB。 17 | 18 | 这些封装格式都有相关的文档,在这里就不一一例举了。 19 | 20 | 我自己也做过辅助学习的小项目: 21 | 22 | [TS封装格式分析器](http://blog.csdn.net/leixiaohua1020/article/details/17973587) 23 | 24 | [FLV封装格式分析器](http://blog.csdn.net/leixiaohua1020/article/details/17934487) 25 | 26 | ### 参考资料: 27 | 28 | [视音频编解码技术零基础学习方法](http://blog.csdn.net/leixiaohua1020/article/details/18893769) -------------------------------------------------------------------------------- /AudioVideo/常见封装格式概览.md: -------------------------------------------------------------------------------- 1 | ## 常见封装格式概览 2 | 3 | | 名称 | 推出机构 | 流媒体 | 支持的视频编码 | 支持的音频编码 | 目前使用领域 | 4 | | ---- | ------------------ | ---- | ----------------------------- | ------------------------------------ | --------- | 5 | | AVI | Microsoft Inc. | 不支持 | 几乎所有格式 | 几乎所有格式 | BT下载影视 | 6 | | MP4 | MPEG | 支持 | MPEG-2, MPEG-4, H.264, H.263等 | AAC, MPEG-1 Layers I, II, III, AC-3等 | 互联网视频网站 | 7 | | TS | MPEG | 支持 | MPEG-1, MPEG-2, MPEG-4, H.264 | MPEG-1 Layers I, II, III, AAC, | IPTV,数字电视 | 8 | | FLV | Adobe Inc. | 支持 | Sorenson, VP6, H.264 | MP3, ADPCM, Linear PCM, AAC等 | 互联网视频网站 | 9 | | MKV | CoreCodec Inc. | 支持 | 几乎所有格式 | 几乎所有格式 | 互联网视频网站 | 10 | | RMVB | Real Networks Inc. | 支持 | RealVideo 8, 9, 10 | AAC, Cook Codec, RealAudio Lossless | BT下载影视 | 11 | 12 | ### 参考资料: 13 | 14 | [视音频编解码技术零基础学习方法](http://blog.csdn.net/leixiaohua1020/article/details/18893769) -------------------------------------------------------------------------------- /AudioVideo/常见流媒体协议.md: -------------------------------------------------------------------------------- 1 | ## 常见流媒体协议 2 | 3 | 流媒体协议是服务器与客户端之间通信遵循的规定。当前网络上主要的流媒体协议如表所示。 4 | 5 | | 名称 | 推出机构 | 传输层协议 | 客户端 | 目前使用领域 | 6 | | -------- | -------------- | ------- | -------- | -------- | 7 | | RTSP+RTP | IETF | TCP+UDP | VLC, WMP | IPTV | 8 | | RTMP | Adobe Inc. | TCP | Flash | 互联网直播 | 9 | | RTMFP | Adobe Inc. | UDP | Flash | 互联网直播 | 10 | | MMS | Microsoft Inc. | TCP/UDP | WMP | 互联网直播+点播 | 11 | | HTTP | WWW+IETF | TCP | Flash | 互联网点播 | 12 | 13 | RTSP+RTP经常用于IPTV领域。因为其采用UDP传输视音频,支持组播,效率较高。但其缺点是网络不好的情况下可能会丢包,影响视频观看质量。因而围绕IPTV的视频质量的研究还是挺多的。 14 | 15 | RTSP规范可参考:[RTSP协议学习笔记](http://blog.csdn.net/leixiaohua1020/article/details/11955341) 16 | 17 | RTSP+RTP系统中衡量服务质量可参考:[网络视频传输的服务质量(QoS)](http://blog.csdn.net/leixiaohua1020/article/details/11883393) 18 | 19 | 上海IPTV码流分析结果可参考:[IPTV视频码流分析](http://blog.csdn.net/leixiaohua1020/article/details/11846761) 20 | 21 | 因为互联网网络环境的不稳定性,RTSP+RTP较少用于互联网视音频传输。互联网视频服务通常采用TCP作为其流媒体的传输层协议,因而像RTMP,MMS,HTTP这类的协议广泛用于互联网视音频服务之中。这类协议不会发生丢包,因而保证了视频的质量,但是传输的效率会相对低一些。 22 | 23 | 此外RTMFP是一种比较新的流媒体协议,特点是支持P2P。 24 | 25 | RTMP我做的研究相对多一些:比如[RTMP规范简单分析](http://blog.csdn.net/leixiaohua1020/article/details/11694129),或者[RTMP流媒体播放过程](http://blog.csdn.net/leixiaohua1020/article/details/11704355) 26 | 27 | 相关工具的源代码分析:[RTMPdump源代码分析 1: main()函数[系列文章\]](http://blog.csdn.net/leixiaohua1020/article/details/12952977) 28 | 29 | RTMP协议学习:[RTMP流媒体技术零基础学习方法](http://blog.csdn.net/leixiaohua1020/article/details/15814587) 30 | 31 | ### 参考资料: 32 | 33 | [视音频编解码技术零基础学习方法](http://blog.csdn.net/leixiaohua1020/article/details/18893769) -------------------------------------------------------------------------------- /AudioVideo/常见音视频编码.md: -------------------------------------------------------------------------------- 1 | ## 常见音视频编码 2 | 3 | ### 1. 视频编码 4 | 5 | 视频编码的主要作用是将视频像素数据(RGB,YUV等)压缩成为视频码流,从而降低视频的数据量。如果视频不经过压缩编码的话,体积通常是非常大的,一部电影可能就要上百G的空间。视频编码是视音频技术中最重要的技术之一。视频码流的数据量占了视音频总数据量的绝大部分。高效率的视频编码在同等的码率下,可以获得更高的视频质量。 6 | 7 | 视频编码的简单原理可以参考:[视频压缩编码和音频压缩编码的基本原理](http://blog.csdn.net/leixiaohua1020/article/details/28114081) 8 | 9 | 注:视频编码技术在整个视音频技术中应该是最复杂的技术。如果没有基础的话,可以先买一些书看一下原理,比如说《现代电视原理》《数字电视广播原理与应用》(本科的课本)中的部分章节。 10 | 11 | 主要视频编码一览 12 | 13 | | 名称 | 推出机构 | 推出时间 | 目前使用领域 | 14 | | ----------- | -------------- | ---- | ------ | 15 | | HEVC(H.265) | MPEG/ITU-T | 2013 | 研发中 | 16 | | H.264 | MPEG/ITU-T | 2003 | 各个领域 | 17 | | MPEG4 | MPEG | 2001 | 不温不火 | 18 | | MPEG2 | MPEG | 1994 | 数字电视 | 19 | | VP9 | Google | 2013 | 研发中 | 20 | | VP8 | Google | 2008 | 不普及 | 21 | | VC-1 | Microsoft Inc. | 2006 | 微软平台 | 22 | 23 | 由表可见,有两种视频编码方案是最新推出的:VP9和HEVC。目前这两种方案都处于研发阶段,还没有到达实用的程度。当前使用最多的视频编码方案就是H.264。 24 | 25 | #### **1.1 主流编码标准** 26 | 27 | H.264仅仅是一个编码标准,而不是一个具体的编码器,H.264只是给编码器的实现提供参照用的。 28 | 29 | 基于H.264标准的编码器还是很多的,究竟孰优孰劣?可参考:[MSU出品的 H.264编码器比较(2011.5)](http://blog.csdn.net/leixiaohua1020/article/details/12373947) 30 | 31 | 在学习视频编码的时候,可能会用到各种编码器(实际上就是一个exe文件),他们常用的编码命令可以参考:[各种视频编码器的命令行格式](http://blog.csdn.net/leixiaohua1020/article/details/11705495) 32 | 33 | 学习H.264最标准的源代码,就是其官方标准JM了。但是要注意,JM速度非常的慢,是无法用于实际的:[H.264参考软件JM12.2RC代码详细流程](http://blog.csdn.net/leixiaohua1020/article/details/11980219) 34 | 35 | 实际中使用最多的就是x264了,性能强悍(超过了很多商业编码器),而且开源。其基本教程网上极多,不再赘述。编码时候可参考:[x264编码指南——码率控制](http://blog.csdn.net/leixiaohua1020/article/details/12720135)。编码后统计值的含义:[X264输出的统计值的含义(X264 Stats Output)](http://blog.csdn.net/leixiaohua1020/article/details/11884559) 36 | 37 | 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) 38 | 39 | 此外,我国还推出了自己的国产标准AVS,性能也不错,但目前比H.264还是要稍微逊色一点。不过感觉我国在视频编解码领域还算比较先进的,可参考:[视频编码国家标准AVS与H.264的比较(节选)](http://blog.csdn.net/leixiaohua1020/article/details/12851745) 40 | 41 | 近期又推出了AVS新一代的版本AVS+,具体的性能测试还没看过。不过据说AVS+得到了国家政策上非常强力的支持。 42 | 43 | #### **1.2 下一代编码标准** 44 | 45 | 下一代的编解码标准就要数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) 46 | 47 | HEVC在未来拥有很多大的优势,可参考:[HEVC将会取代H.264的原因](http://blog.csdn.net/leixiaohua1020/article/details/11844949) 48 | 49 | 学习HEVC最标准的源代码,就是其官方标准HM了。其速度比H.264的官方标准代码又慢了一大截,使用可参考:[HEVC学习—— HM的使用](http://blog.csdn.net/leixiaohua1020/article/details/12759297) 50 | 51 | 未来实际使用的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) 52 | 53 | 主流以及下一代编码标准之间的比较可以参考文章:[视频编码方案之间的比较(HEVC,H.264,MPEG2等)](http://blog.csdn.net/leixiaohua1020/article/details/12237177) 54 | 55 | 此外,在码率一定的情况下,几种编码标准的比较可参考:[限制码率的视频编码标准比较(包括MPEG-2,H.263,MPEG-4,以及 H.264)](http://blog.csdn.net/leixiaohua1020/article/details/12851975) 56 | 57 | 结果大致是这样的: 58 | 59 | HEVC > VP9 > H.264> VP8 > MPEG4 > H.263 > MPEG2。 60 | 61 | 截了一些图,可以比较直观的了解各种编码标准: 62 | 63 | HEVC码流简析:[HEVC码流简单分析](http://blog.csdn.net/leixiaohua1020/article/details/11845069) 64 | 65 | H.264码流简析:[H.264简单码流分析](http://blog.csdn.net/leixiaohua1020/article/details/11845625) 66 | 67 | MPEG2码流简析:[MPEG2简单码流分析](http://blog.csdn.net/leixiaohua1020/article/details/11846185) 68 | 69 | 以上简析使用的工具:[视频码流分析工具](http://blog.csdn.net/leixiaohua1020/article/details/11845435) 70 | 71 | 我自己做的小工具: [H.264码流分析器](http://blog.csdn.net/leixiaohua1020/article/details/17933821) 72 | 73 | ### 2. 音频编码 74 | 75 | 音频编码的主要作用是将音频采样数据(PCM等)压缩成为音频码流,从而降低音频的数据量。音频编码也是互联网视音频技术中一个重要的技术。但是一般情况下音频的数据量要远小于视频的数据量,因而即使使用稍微落后的音频编码标准,而导致音频数据量有所增加,也不会对视音频的总数据量产生太大的影响。高效率的音频编码在同等的码率下,可以获得更高的音质。 76 | 77 | 音频编码的简单原理可以参考:[视频压缩编码和音频压缩编码的基本原理](http://blog.csdn.net/leixiaohua1020/article/details/28114081) 78 | 79 | 主要音频编码一览 80 | 81 | | 名称 | 推出机构 | 推出时间 | 目前使用领域 | 82 | | ---- | -------------- | ---- | ------- | 83 | | AAC | MPEG | 1997 | 各个领域(新) | 84 | | AC-3 | Dolby Inc. | 1992 | 电影 | 85 | | MP3 | MPEG | 1993 | 各个领域(旧) | 86 | | WMA | Microsoft Inc. | 1999 | 微软平台 | 87 | 88 | 由表可见,近年来并未推出全新的音频编码方案,可见音频编码技术已经基本可以满足人们的需要。音频编码技术近期绝大部分的改动都是在MP3的继任者——AAC的基础上完成的。 89 | 90 | 这些编码标准之间的比较可以参考文章:[音频编码方案之间音质比较(AAC,MP3,WMA等)](http://blog.csdn.net/leixiaohua1020/article/details/11730661) 91 | 92 | 结果大致是这样的: 93 | 94 | AAC+ > MP3PRO > AAC> RealAudio > WMA > MP3 95 | 96 | AAC格式的介绍:[AAC格式简介](http://blog.csdn.net/leixiaohua1020/article/details/11822537) 97 | 98 | AAC几种不同版本之间的对比:[AAC规格(LC,HE,HEv2)及性能对比](http://blog.csdn.net/leixiaohua1020/article/details/11971419) 99 | 100 | AAC专利方面的介绍:[AAC专利介绍](http://blog.csdn.net/leixiaohua1020/article/details/11854587) 101 | 102 | 此外杜比数字的编码标准也比较流行,但是貌似比最新的AAC稍为逊色:[AC-3技术综述](http://blog.csdn.net/leixiaohua1020/article/details/11822737) 103 | 104 | 我自己做的小工具:[ AAC格式分析器](http://blog.csdn.net/leixiaohua1020/article/details/18155549) 105 | 106 | 107 | 108 | ### 参考资料: 109 | 110 | [视音频编解码技术零基础学习方法](http://blog.csdn.net/leixiaohua1020/article/details/18893769) -------------------------------------------------------------------------------- /AudioVideo/通用视频解码播放流程.md: -------------------------------------------------------------------------------- 1 | --- 2 | typora-copy-images-to: ./image 3 | --- 4 | 5 | ## 通用视频解码播放流程 6 | 7 | **通用的网络视频播放流程:** 8 | 9 | 1. 从网络数据流中获得视频数据流。 10 | 2. 将视频数据流解析成压缩音频数据和压缩视频数据。 11 | 3. 分别对音频和视频解码获取原始(采样)数据。 12 | 4. 经过同步策略后,有序的将原始(采样)数据输出到指定设备播放。 13 | 14 | ![视频解码播放流程](image/视频解码播放流程.jpg) 15 | 16 | ### 参考资料: 17 | 18 | [视音频编解码技术零基础学习方法](http://blog.csdn.net/leixiaohua1020/article/details/18893769) -------------------------------------------------------------------------------- /ChaosCrystal/ADB常用命令.md: -------------------------------------------------------------------------------- 1 | # ADB常用命令 2 | 3 | 命令 | 说明 4 | --------------------------------|----------------------------------------------------- 5 | adb start-server | 启动服务 6 | adb kill-server | 关闭服务 7 | adb devices | 显示当前连接的所有设备(如果服务没有开启会自动开启) 8 | adb install xxx.apk | 将应用安装进设备中 9 | adb uninstall <包名> | 卸载应用 10 | adb -s <设备名> <命令> | 如果有多个设备,指定某一个设备进行操作 11 | adb pull <手机文件> <电脑文件> | 将手机内文件导入到电脑上(文件名均为全称) 12 | adb push <电脑文件> <手机文件> | 将电脑中文件推送到手机上(文件名均为全称) 13 | adb shell | 进入手机命令行终端 14 | ┗ ls | 查看目录列表 15 | ┗ ls -l | 显示详细信息(权限 用户名 组名 时间 包名) 16 | ┗ ps | 当前正在运行的进程 17 | ┗ ping | 手机的网络连通性 18 | 19 | -------------------------------------------------------------------------------- /ChaosCrystal/AndroidStudio常用快捷键(Mac).md: -------------------------------------------------------------------------------- 1 | # AndroidStudio常用快捷键(Mac) 2 | 3 | 这里的快捷键是基于OSX个人定制版本的,具体请到 setting -> keymap 设置 4 | 5 | 快捷键 | 作用 6 | ----------------------------|----------------------------------------------- 7 | Option + Enter | 自动修正 8 | Command + N | 自动生成代码(Getter Setter) 9 | Command + Alt + L | 格式化代码 10 | Contral + Shift + F | 格式化代码(定制) 11 | Command + Alt + T | 把选中的代码放在 try{} 、if{} 、 else{} 里 12 | Command + / | 注释 // 13 | Command + Shift + / | 注释 /* */ 14 | Command + Shift + Up/Down | 语句上下移动 15 | Option + Shift + Up/Down | 内容上下移动 16 | Option + Command + M | 将选中代码块封装成一个方法 17 | Command + D | 复制当前一行(或选择区域),并粘贴到下面 18 | Command + Z | 后退 19 | Command + Shift + Z | 前进 20 | Control + Alt + O | 优化导入的包 21 | Ctrl(Command)+ - / + | 折叠/展开代码 22 | Ctrl(Command)+Shift+ - / + | 折叠/展开全部代码 23 | Ctrl(Command)+Shift+. | 折叠/展开当前花括号中的代码 24 | Command + Y | 快速查看代码实现 25 | Contral + H | 查看继承关系 26 | Contral + Alt + H | 查看调用关系 27 | Command + [ | 返回上一次查看的位置 28 | Command + ] | 前进到返回之前查看的位置 29 | Command + J | 自动生成代码 30 | Command + E | 查看最近打开的文件 31 | 32 | -------------------------------------------------------------------------------- /ChaosCrystal/Android中dip、dp、sp、pt和px.md: -------------------------------------------------------------------------------- 1 | # Android中dip、dp、sp、pt和px 2 | 3 | 概念区别: 4 | 5 | 单位 | 含义 6 | --- | --- 7 | dip | device independent pixels(设备独立像素). 不同设备有不同的显示效果,这个和设备硬件有关,一般我们为了支持WVGA、HVGA和QVGA **推荐使用这个,不依赖像素**。 8 | dp | 同上,和dip一样。 9 | px | pixels(像素). 不同设备显示效果相同,一般我们HVGA代表320x480像素。 10 | sp | scaled pixels(放大像素). 主要用于字体显示best for textsize。 11 | pt | point,是一个标准的长度单位,1pt=1/72英寸,用于印刷业,非常简单易用。 12 | in | (英寸):长度单位。 13 | mm | (毫米):长度单位。 14 | 15 | ## 工具包 16 | 17 | 在 [ViewSupport](https://github.com/GcsSloop/ViewSupport) 支持包中可以找到该工具。 18 | 19 | ## 单位转换代码: 20 | ``` java 21 | /** 22 | * dp、sp 转换为 px 的工具类 23 | * 24 | * @author fxsky 2012.11.12 25 | * 26 | */ 27 | public class DisplayUtil { 28 | /** 29 | * 将px值转换为dip或dp值,保证尺寸大小不变 30 | * 31 | * @param pxValue 32 | * @param scale 33 | * (DisplayMetrics类中属性density) 34 | * @return 35 | */ 36 | public static int px2dip(Context context, float pxValue) { 37 | final float scale = context.getResources().getDisplayMetrics().density; 38 | return (int) (pxValue / scale + 0.5f); 39 | } 40 | 41 | /** 42 | * 将dip或dp值转换为px值,保证尺寸大小不变 43 | * 44 | * @param dipValue 45 | * @param scale 46 | * (DisplayMetrics类中属性density) 47 | * @return 48 | */ 49 | public static int dip2px(Context context, float dipValue) { 50 | final float scale = context.getResources().getDisplayMetrics().density; 51 | return (int) (dipValue * scale + 0.5f); 52 | } 53 | 54 | /** 55 | * 将px值转换为sp值,保证文字大小不变 56 | * 57 | * @param pxValue 58 | * @param fontScale 59 | * (DisplayMetrics类中属性scaledDensity) 60 | * @return 61 | */ 62 | public static int px2sp(Context context, float pxValue) { 63 | final float fontScale = context.getResources().getDisplayMetrics().scaledDensity; 64 | return (int) (pxValue / fontScale + 0.5f); 65 | } 66 | 67 | /** 68 | * 将sp值转换为px值,保证文字大小不变 69 | * 70 | * @param spValue 71 | * @param fontScale 72 | * (DisplayMetrics类中属性scaledDensity) 73 | * @return 74 | */ 75 | public static int sp2px(Context context, float spValue) { 76 | final float fontScale = context.getResources().getDisplayMetrics().scaledDensity; 77 | return (int) (spValue * fontScale + 0.5f); 78 | } 79 | } 80 | ``` 81 | 82 | -------------------------------------------------------------------------------- /ChaosCrystal/HowToViewAPISourceOnline.md: -------------------------------------------------------------------------------- 1 | # 在线查看Android API源码的方法 2 | 3 | 4 | 方法 | 推荐程度| 链接 | 备注 5 | ---|---|---|--- 6 | 官方GitHub仓库 | ★☆☆☆☆ | [GitHub-Android](https://github.com/android) | 搜索不方便 7 | 通过GrepCode | ★★★☆☆ | [GrepCode-Android](http://grepcode.com/project/repository.grepcode.com/java/ext/com.google.android/android/) | 有搜索功能,可以在线查看到各个版本的源码,也可以下载 8 | 通过Chrome插件 | ★★★★★ | [插件:SDKSearch](https://chrome.google.com/webstore/detail/android-sdk-search/hgcbffeicehlpmgmnhnkjbjoldkfhoin)
[Android API官网](http://developer.android.com/reference/packages.html) | 在官网查看API时会在下面多出一个View Source按钮(如下图) 9 | 10 | ![](http://ww3.sinaimg.cn/large/005Xtdi2jw1f3tvw0e7exj30du07tq32.jpg) 11 | 12 | 13 | -------------------------------------------------------------------------------- /ChaosCrystal/README.md: -------------------------------------------------------------------------------- 1 | ![](http://ww1.sinaimg.cn/large/005Xtdi2jw1f5xd2k7s8hj30x30d8tab.jpg) 2 | 3 | # 混沌水晶 4 | 5 | 混沌水晶本身并没有太大功效,但与其他物品合成之后可能产生质的变化。 6 | 7 | * [Android中dip、dp、sp、pt和px](https://github.com/GcsSloop/AndroidNote/blob/master/ChaosCrystal/Android%E4%B8%ADdip%E3%80%81dp%E3%80%81sp%E3%80%81pt%E5%92%8Cpx.md) 8 | * [AndroidStudio常用快捷键(Mac)](https://github.com/GcsSloop/AndroidNote/blob/master/ChaosCrystal/AndroidStudio%E5%B8%B8%E7%94%A8%E5%BF%AB%E6%8D%B7%E9%94%AE(Mac).md) 9 | * [在线查看Android API源码](https://github.com/GcsSloop/AndroidNote/blob/master/ChaosCrystal/HowToViewAPISourceOnline.md) 10 | * [录屏与GIF制作](https://github.com/GcsSloop/AndroidNote/blob/master/ChaosCrystal/%E5%BD%95%E5%B1%8F%E4%B8%8EGIF%E5%88%B6%E4%BD%9C.md) 11 | * [ADB常用命令](https://github.com/GcsSloop/AndroidNote/blob/master/ChaosCrystal/ADB%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4.md) 12 | 13 | 14 | -------------------------------------------------------------------------------- /ChaosCrystal/录屏与GIF制作.md: -------------------------------------------------------------------------------- 1 | # 录屏与GIF制作 2 | 3 | ## 说明 4 | 在实际工作或学习过程中,很多地方都需要展示一些过程,或者展示动态的效果,使用视频显得比较大了,而且也不利于分享,故使用GIF就是一种很好的方法,体积小,易于分享展示。 5 | 6 | ## 制作GIF的方法 7 | > 此处大部分参考自 廖祜秋(我们的秋百万大哥) 整理的内容: [Make GIF Snapshot for Android APP](http://www.liaohuqiu.net/posts/make-gif-for-android-app/) 8 | 9 | 制作GIF一般分为以下两种情况: 10 | 11 | 类别 | 说明 | 备注 12 | --- | --- | --- 13 | 第一种 | 在电脑上可以看到效果 | 在模拟器中运行的app,视频等 14 | 第二种 | 在手机上可以看到效果 | 在真机中运行的app 15 | 16 | ### 第一种的制作方法 17 | 第一种方法比较容易直接录制屏幕然后转换为GIF就行了,有很多可视化的制作软件,这里推荐几种: 18 | 19 | 名称 | 说明 | 地址 20 | --- | --- | --- 21 | 都叫兽GIF | 可视化GIF制作工具,支持录制后再次编辑,不同程度压缩,添加水印等功能,但默认右下角有一个都叫兽的水印,不适合强迫症患者 | [都叫兽GIF](http://www.reneelab.com.cn)
[Windows直接下载](http://www.reneelab.com.cn/download-center/renee-gifer) 22 | LICEcap | 可视化GIF制作工具,支持编辑录制区域大小,可改变录制帧率,功能没有上面多,但用起来很简单 | [LICEcap](http://www.cockos.com/licecap/)
[Windows直接下载](http://www.cockos.com/licecap/licecap123-install.exe) 23 | ezgif | 在线制作GIF的工具,这个的功能也很强大,支持将多张图片合成为GIF,将视频转换为GIF,剪切旋转等多种操作 | [ezgif](http://ezgif.com/) 24 | 25 | ## 第二种的制作方法 26 | 虽然手机上也有一些录屏GIF制作软件,但是大部分都很渣,很难达到我们想要的效果。 27 | 28 | 我个人一般是将操作过程录制下来,然后发送到电脑上,用电脑软件截取GIF,也就是转为第一种方法。 29 | 30 | ### 录制手机操作的方法 31 | 32 | #### 通过AndroidStudio录屏工具: 33 | 34 | 这个用起来很方便,录制后能直接保存在电脑上。 35 | 36 | ![](http://ww3.sinaimg.cn/large/005Xtdi2jw1f13ro2pqw2j30bd0ahwfi.jpg) 37 | 38 | 1.选到该选项卡 39 | 40 | 2.上面一个按钮是截屏,下面一个按钮是录屏。 41 | 42 | #### 通过ADB命令: 43 | 44 | 作为一个程序猿,虽然可视化操作很爽,但不会两条命令怎么能装逼呢。下面就教大家如何用命令录屏。 45 | 46 | ``` shell 47 | $ adb shell // 进入shell 48 | shell@ $ screenrecord --verbose /sdcard/demo.mp4 // 开始录制(保存到内存卡上) 49 | (press Ctrl-C to stop) // 按Ctrl+C结束录制 50 | shell@ $ exit // 结束录制 51 | $ adb pull /sdcard/demo.mp4 // 将内存卡中的文件传到电脑上 52 | ``` 53 | **[更多ADB命令看这里](http://developer.android.com/tools/help/shell.html#screenrecord)** 54 | 55 | #### 通过Python命令录制 56 | 57 | 虽说是Python命令,但是底层调用的依旧是adb,内部实现依赖了ffmpeg。 58 | 59 | **详情请参考RoboGif的文档->[RoboGif](https://github.com/GcsSloop/RoboGif)** 60 | 61 | 如果你对Python还不了解,可以到这里学习一下Python的基础知识:**[PythonNote](https://github.com/GcsSloop/PythonNote)** 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /Course/HowToUsePlantUMLInAS.md: -------------------------------------------------------------------------------- 1 | # 在AndroidStudio中使用PlantUML 2 | 3 | ### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop) 4 | 5 | ## 前言 6 | 7 | ### 这是Windows平台设置教程,Mac平台戳这里:在AndroidStudio中使用PlantUML(Mac) 8 | 9 | **Unified Modeling Language (UML)又称统一建模语言或标准建模语言,用来描述 类(对象的)、对象、关联、职责、行为、接口、用例、包、顺序、协作,以及状态。是用来帮助自己理清众多类之间复杂关系的不二利器,也能帮助别人快速理解你的设计思路。** 10 | 11 | 那么,我们怎么在AndroidStudio中创建自己的UML类图呢?接下来我就教大家如何用正确的姿势创建UML类图。 12 | 13 | ## 一.用正确的姿势安装panltUML插件 14 | ### 1.File->Settings->Plugins->Browse repositories 15 | ![这里写图片描述](http://img.blog.csdn.net/20151130192101011) 16 | ### 2.在搜索框输入plantUML 17 | ![这里写图片描述](http://img.blog.csdn.net/20151130192547549) 18 | ### 3.导入插件 19 | #### (ps:由于我已经安装过了,所以没有Install plugin 按钮,未安装的都有这样一个按钮,如下,点击安装即可。) 20 | ![这里写图片描述](http://img.blog.csdn.net/20151130192907006) 21 | 22 | #### 如果以上步骤正确的完成,重启AndroidStudio 右键->new 的时候你会发现多了这么一堆东西,如果出现了这些说明plantUML已经正确的安装了。 23 | ![这里写图片描述](http://img.blog.csdn.net/20151130193249965) 24 | 25 | #### 当然了,所有事情都不会是一帆风顺的,当你迫不及待的想创建一个文件试试的时候你会发现下面的情况。 26 | ![这里写图片描述](http://img.blog.csdn.net/20151130193752721) 27 | #### 想必此时你的内心一定和我当时一样,一万头草泥马奔腾而过,这都是什么东西!!! 28 | #### 一切事情都是有原因的,而这个因为你还缺少一个必要的东西,就是大名鼎鼎的贝尔实验室开发的一个工具包:Graphviz。 29 | ## 二,用正确的姿势安装Graphviz 30 | ### 1.下载Graphviz 31 | ###[【下载地址戳这里】](http://www.graphviz.org/Download_windows.php) 32 | ![这里写图片描述](http://img.blog.csdn.net/20151130194703804) 33 | ### 2.安装 34 | #### 安装过程我就不详细讲解了,点击next后要记住安装的目录。之后下一步,直到完成就行。 35 | ![这里写图片描述](http://img.blog.csdn.net/20151130195454083) 36 | 37 | ## 三.用正确的姿势设置plantUML 38 | ### 1.点击右上角的设置按钮或进入File->Settings->Other Settings ->PlantUML 39 | ### 2.将文件路径填写为刚刚Graphviz的目录下bin目录中dot.exe文件。 40 | #### (我的为:D:/Program/Graphviz/bin/dot.exe) 41 | ![这里写图片描述](http://img.blog.csdn.net/20151130200308586) 42 | ### 3.点击OK 刷新一下界面就能看到这个了。 43 | ![这里写图片描述](http://img.blog.csdn.net/20151130200452927) 44 | #### 讲到这里,就已经安装完成了,可以愉快的用代码来书写UML图了。 45 | #### 什么?你说你还不会书写的语法?没关系,其实我也不会,不过我有一个好的教程推荐给你,相信你看完就明白啦。 46 | ## 四.用正确的姿势学习使用UML 47 | ### 1.[【PlantUML快速指南戳这里】](http://archive.3zso.com/archives/plantuml-quickstart.html) 48 | ### 2.注意,这个教程中的语法和AndroidStudio中基本一致,区别就是开始和结束标志不同。 49 | 50 | ####好了,到这里该教程正式结束,祝各位小伙伴能愉快的使用plantUML玩耍。 51 | 52 | ## About Me 53 | ### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop) 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /Course/HowToUsePlantUMLInAS[Mac].md: -------------------------------------------------------------------------------- 1 | # 在AndroidStudio中使用PlantUML(Mac) 2 | 3 | ### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop) 4 | 5 | ## 前言 6 | 7 | ### 这是Mac平台设置教程,Windows平台戳这里:[在AndroidStudio中使用PlantUML(Windows)](https://github.com/GcsSloop/AndroidNote/blob/master/Course/HowToUsePlantUMLInAS.md) 8 | 9 | **Unified Modeling Language (UML)又称统一建模语言或标准建模语言,用来描述 类(对象的)、对象、关联、职责、行为、接口、用例、包、顺序、协作,以及状态。是用来帮助自己理清众多类之间复杂关系的不二利器,也能帮助别人快速理解你的设计思路。** 10 | 11 | 那么,我们怎么在AndroidStudio中创建自己的UML类图呢?接下来我就教大家如何用正确的姿势创建UML类图。 12 | 13 | ## 一.用正确的姿势安装panltUML插件 14 | ### 1.File->Settings->Plugins->Browse repositories 15 | ![这里写图片描述](http://img.blog.csdn.net/20151130192101011) 16 | ### 2.在搜索框输入plantUML 17 | ![这里写图片描述](http://img.blog.csdn.net/20151130192547549) 18 | ### 3.导入插件 19 | #### (ps:由于我已经安装过了,所以没有Install plugin 按钮,未安装的都有这样一个按钮,如下,点击安装即可。) 20 | ![这里写图片描述](http://img.blog.csdn.net/20151130192907006) 21 | 22 | #### 如果以上步骤正确的完成,重启AndroidStudio 右键->new 的时候你会发现多了这么一堆东西,如果出现了这些说明plantUML已经正确的安装了。 23 | ![这里写图片描述](http://img.blog.csdn.net/20151130193249965) 24 | 25 | #### 当然了,所有事情都不会是一帆风顺的,当你迫不及待的想创建一个文件试试的时候你会发现下面的情况。 26 | ![这里写图片描述](http://img.blog.csdn.net/20151130193752721) 27 | #### 想必此时你的内心一定和我当时一样,一万头草泥马奔腾而过,这都是什么东西!!! 28 | #### 一切事情都是有原因的,而这个因为你还缺少一个必要的东西,就是大名鼎鼎的贝尔实验室开发的一个工具包:Graphviz。 29 | ## 二,用正确的姿势安装Graphviz 30 | 31 | 此处建议使用Homebrew自动下载安装,如果你还没有用过Homebrew来帮助你管理软件,参考这里: [Homebrew&HomebrewCask](https://github.com/GcsSloop/MacDeveloper/blob/master/Tools/Homebrew.md) 32 | 33 | 安装好homebrew后直接输入如下命令,按下回车后等待片刻即可安装成功: 34 | 35 | ``` 36 | brew install graphviz 37 | ``` 38 | 39 | 安装完成后,Homebrew会告诉你安装位置,请记好这个位置,你也可以用info命名查看安装位置: 40 | 41 | ``` 42 | brew info graphviz 43 | ``` 44 | 45 | 我的安装位置在 `/usr/local/Cellar/graphviz/2.38.0` 46 | 47 | ![](http://ww2.sinaimg.cn/large/005Xtdi2gw1f6h4le3ao7j30fu0a6gol.jpg) 48 | 49 | 50 | ## 三.用正确的姿势设置plantUML 51 | 52 | ### 1.点击右上角的设置按钮或进入File->Settings->Other Settings ->PlantUML或者在预览页面点击右上角的设置图标,如下: 53 | 54 | ### 2.将文件路径填写为刚刚Graphviz的目录下bin目录中dot文件。 55 | 56 | #### (我的为:/usr/local/Cellar/graphviz/2.38.0/bin/dot) 57 | 58 | 59 | ![](http://ww3.sinaimg.cn/large/005Xtdi2gw1f6h4mbnnhsj30te0g676i.jpg) 60 | 61 | ### 3.点击OK 刷新一下界面就能看到结果了。 62 | 63 | #### 讲到这里,就已经安装完成了,可以愉快的用代码来书写UML图了。 64 | 65 | #### 什么?你说你还不会书写的语法?没关系,其实我也不会,不过我有一个好的教程推荐给你,相信你看完就明白啦。 66 | 67 | ## 四.用正确的姿势学习使用UML 68 | ### 1.[【PlantUML快速指南戳这里】](http://archive.3zso.com/archives/plantuml-quickstart.html) 69 | ### 2.注意,这个教程中的语法和AndroidStudio中基本一致,区别就是开始和结束标志不同。 70 | 71 | ####好了,到这里该教程正式结束,祝各位小伙伴能愉快的使用plantUML玩耍。 72 | 73 | ## About Me 74 | ### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop) 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Course/Markdown/README.md: -------------------------------------------------------------------------------- 1 | # Markdown 实用技巧 2 | 3 | * [Markdown 快速入门](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-start.md) 4 | * [Markdown 基础语法](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-grammar.md) 5 | * [Markdown 链接图片](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-link.md) 6 | * [Markdown 编辑器](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-editor.md) 7 | -------------------------------------------------------------------------------- /Course/Markdown/markdown-editor.md: -------------------------------------------------------------------------------- 1 | # Markdown实用技巧-编辑器(Typora) 2 | 3 | 本次的安利对象是一个 Markdown 编辑器,是会长见过的最简单,最优雅的编辑器,先来看一下它的界面吧: 4 | 5 | ![QQ20161207-0](http://ww3.sinaimg.cn/large/006y8lVajw1fahmo8d9udj30ph0brgo0.jpg) 6 | 7 | 8 | 9 | 它的界面非常简单,有多种主题可选,更重要的是**它的预览界面和编辑界面是一体的,而不像其他编辑器那样是左右分开的。** 10 | 11 | ![](http://ww2.sinaimg.cn/large/005Xtdi2jw1fahnsrelggg30fp0dwakd.gif) 12 | 13 | 从上面的示例中可以看出,其输入模式支持多种,不论是手动输入语法还是使用快捷键,都非常的流畅,实时看到效果变化,除此之外 Typora 还有很多优点。 14 | 15 | ## Typora 的优点 16 | 17 | * 预览和编辑界面一体。 18 | * 强大的快捷键。 19 | * 兼容常见扩展语法。 20 | * 兼容 HTML (新版进行了完善)。 21 | * 支持 YAML 格式。 22 | * 支持公式编辑(LaTeX公式)。 23 | * 表格和代码块编辑起来非常方便。 24 | * 支持本地图片相对链接。 25 | * 支持将本地图片拖进编辑页面。 26 | * 支持多种导出格式(PDF,HTML,Word,LaTeX 等)。 27 | * 有多种主题可选。 28 | 29 | 虽然上面的内容在其他编辑器上也可以见到,但做到这么完善的并不多,想要尝试的小伙伴赶紧来试试吧,相信你也会爱上它的。 30 | 31 | #### [点击这里进入 Typora 官网](http://www.typora.io/) 32 | 33 | 既然 Typora 这么好,为什么不在一开始就推荐呢,这是因为在 Markdown 系列文章第一篇发布的时候,Typora 还有一些小瑕疵(HTML语法兼容部分)让会长不满意,所以只是放在了推荐首位,在最近更新了新版本之后,这一部分已经修复完善了,基本算是完美了,所以会长我才特地写一篇文章来推荐。 34 | 35 | **Typora 支持在 WIndows,Linux,和 Mac 上使用,如果你正在使用的是 Mac 的话,那么恭喜你,除了上述的优点,你可以用到一些 Mac 系统才有的福利。** 36 | 37 | ## 版本回溯功能 38 | 39 | 这是 Mac 系统提供的一个小功能,使用 Mac 的时候你可能会注意到一个小细节,**使用系统文本编辑器进行编辑文本的时候,从来不会提示你保存文本,即便编辑后不点保存立即退出里面的内容也不会丢失。**你可能会觉得这不就是一个自动保存功能么,有啥稀奇的? 40 | 41 | 那么你是否遇到过这一种情况,编辑了半天的内容觉得不太好想放弃保存,继续使用之前的内容,这在Windows上很容易实现,只要编辑的时候没有手动保存,直接点击关闭不保存,再打开的时候就是编辑之前的内容。 42 | 43 | 然而在 Mac 上自动保存了,如果关闭后再打开,点 `Command + Z` 也没用了,这不就尴尬了,难道说遇到这种情况只能关闭前狂点 `Command + Z` 回退? 44 | 45 | 作为一个有情怀的操作系统,自然不会意识不到这个问题,它提供了更强大的方法,那就是版本回溯,相当于一个自动的 Git 系统,这就厉害了。 46 | 47 | Trpora 也用到了这一特性,点击 `File > Revert To > Browse All Versions` 就能看到了。以我之前发布的一篇文章为例: 48 | 49 | ![](http://ww2.sinaimg.cn/large/006y8lVajw1facti4okblj31400p0gs8.jpg) 50 | 51 | **上图中左边为当前版本,右边为选中版本,最右侧是时间线,使用这一功能可以将文章回溯到任意时间点,妈妈再也不怕我的文章内容丢失啦,也成功避免了以下这种情况 ▼** 52 | 53 | ![](http://ww3.sinaimg.cn/large/005Xtdi2jw1factpcyrfqj30rs0ja40l.jpg) 54 | 55 | 由于这一功能是 Mac 系统提供的,所以不仅对纯文本有效,对 iWork 也是有效的,iWork 上面也有版本回溯功能,像 key,numbers,pages 都支持版本回溯,中文版点 `文件 > 复原 > 浏览所有版本` 即可看到过去保存的数据。 56 | 57 | 当然了,这只是福利之一。 58 | 59 | ## 更便捷的图片插入和上传方式 60 | 61 | 众所周知,Markdown 插入图片是一个大问题,尤其是在本地编辑的时候,但是这些在遇到 Typora 后就变的越来越简单了。 62 | 63 | **对于本地图片,我们可以直接拖进来,再也不用记复杂的语法和查图片地址了,没错,只要拖进来就行,Typora 会自动识别图片并进行插入。** 64 | 65 | 不过还有一个问题,我们的很多文章都是要发布的,直接拖进来这种方式在本地看起来没问题,但上传到网站上是读取不到本地图片的,通常解决方案就是先上传到图床上,之后再将链接插入到 Markdown 中,这样发布就没问题了。 66 | 67 | 但是,当一篇文章需要插入很多图片的时候,一次次的上传,一次次的复制链接,一次次的粘贴,也是很麻烦的,虽然有一些脚本可以通过快捷键进行上传,但终归还是多了一些步骤,很不方便,而且对新手很不友好,更何况很多新手根本不知道图床所谓何物。 68 | 69 | 这时候就要祭出 Mac 上另一个神器了: iPic [(点击此处进行下载)](https://itunes.apple.com/cn/app/ipic-tu-chuang-shen-qi-zhong/id1101244278?mt=12),它是一个专业图床管理工具,貌似出来还没多久,目前免费版只能上传到微博图床,专业版支持大部分图床,专业版一年 30RMB,如果经常使用的话,倒也不算贵。 70 | 71 | **话说不还是要用图床么?** 72 | 73 | **但 Typora 的方便之处就在于可以和 iPic 无缝结合,纯傻瓜式一键操作,能一次性的将文章中所有本地图片上传到图床上。** 74 | 75 | **首先安装 iPic,运行 iPic,之后点击 `Edit > Image Tools > Upload Local Images via iPic` 就像下面这样就行了。** 76 | 77 | ![![QQ20161207-0](../Downloads/QQ20161207-0.png)QQ20161202-8](http://ww1.sinaimg.cn/large/006y8lVajw1facuf1yjjkj30fo0dmq4v.jpg) 78 | 79 | **如果你觉得这样还是很麻烦的话,还有另外的选项,可以选择插入本地图片的时候自动上传到服务器,当然了,为了保护个人隐私,这一个选项默认是关闭的,首先你要找到首选项,点击 `Typora > Preferences` 或者快捷键 `Command + ,` 都行,之后打开以下两个选项,其中一个选项是后面用到的。** 80 | 81 | ![QQ20161202-9](http://ww3.sinaimg.cn/large/006y8lVajw1fahlaeo8pej30by0ddgn8.jpg) 82 | 83 | **打开之后继续选择 `Edit > Image Tools > When Insert Local Images > Upload Image via iPic` 它的意思是当插入本地图片时自动通过 iPic 上传到服务器上。** 84 | 85 | ![QQ20161203-0](http://ww2.sinaimg.cn/large/006y8lVajw1fahlajkq1gj30qe0hwtas.jpg) 86 | 87 | 这种方案虽然方便,但是会长并不推荐大家这么做,主要还是不安全,误操作的话可能会将一些包含个人隐私的照片上传上去。 88 | 89 | 会长推荐另一种方案,就是 `Copy Image File to Folder`,操作步骤和上面类似,它的意思是插入本地图片时自动归档到某一个文件夹,在文章编辑完成后在点击上传按钮统一上传,这样有以下几点好处: 90 | 91 | * 安全,不会因为误操作将涉及隐私照片传到服务器上。 92 | * 相当于自动建立了一个本地图片档案,方便备份保存。 93 | * 如果图床上的图片丢失,可以从快速从本地备份中找到并恢复。 94 | 95 | 96 | 关于 Typora 的推荐内容暂时就到这里结束啦,更多关于 Typora 的多详情请参阅 [Support](http://support.typora.io/)。关于 Markdown 的更多内容请参见之前的文章: 97 | 98 | [Markdown实用技巧-快速入门](https://www.gcssloop.com/markdown/markdown-start) 99 | [Markdown实用技巧-基础语法](https://www.gcssloop.com/markdown/markdown-grammar) 100 | [Markdown实用技巧-链接和图片](https://www.gcssloop.com/markdown/markdown-links) 101 | 102 | ## 结语 103 | 104 | 最后稍微夹带一点个人建议,个人觉得 Typora 除了界面简洁之外,最大的优点就是快捷键方便了,然而,快捷键那么多,要不要专一去记呢? 105 | 106 | 个人的建议是不要专一去记忆这些快捷键,用到的时候打开菜单看一眼快捷键是什么,之后用快捷键按出来,这样用过几次之后自然就会记住常用的快捷键,而且会记得很牢固,不然每个应用都有快捷键,一个一个的记多麻烦。 107 | 108 | 本文中涉及到的两个软件下载地址: 109 | 110 | [**Typora**](http://www.typora.io/) 111 | [**iPic**](https://itunes.apple.com/cn/app/id1101244278?ls=1&mt=12) 112 | 113 | **最后留一个课后作业,关注会长的 [新浪微博](http://weibo.com/GcsSloop) 。** 114 | 115 | ## About Me 116 | 117 | ### 作者微博: @GcsSloop 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /Course/Markdown/markdown-grammar.md: -------------------------------------------------------------------------------- 1 | # Markdown 基础语法 2 | 3 | 为保证语法兼容性,本文只介绍基础语法,扩展语法等其它内容,会在后续的文章中单独介绍。 4 | **注意:所有的标记符号均使用英文,中文无效。** 5 | 6 | **** 7 | 8 | ## 标题 9 | 10 | Markdown 支持多种标题格式。 11 | 12 | 利用 `=` (等号)和 `-`(减号)可以定义一级标题和二级标题,(任何数量的 `=` 和 `-` 都有效果) : 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
Markdown预览
一级标题
====

一级标题

二级标题
----

二级标题

28 | 29 | 通过在行首插入 1 到 6 个 # ,来定义对应的 1 到 6 阶 标题,个人推荐使用这种,例如: 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 |
Markdown预览
# 一级标题

一级标题

## 二级标题

二级标题

### 三级标题

三级标题

#### 四级标题

四级标题

##### 五级标题
五级标题
###### 六级标题
六级标题
61 | 62 | **** 63 | 64 | ## 段落和换行 65 | 66 | 在 Markdown 中段落由一行或者多行文本组成,相邻的两行文字会被视为同一段落,如果存在空行则被视为不同段落( Markdown 对空行的定义是看起来是空行就是空行,即使空行中存在 `空格` `TAB` `回车` 等不可见字符,同样会被视为空行)。 67 | 68 | Markdown支持段内换行,如果你想进行段落内换行可以在上一行结尾插入两个以上的空格后再回车。 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 80 | 85 | 86 | 87 | 91 | 97 | 98 | 99 | 104 | 112 | 113 |
Markdown预览
77 | 第一行
78 | 相邻被视为同一段落。 79 |
81 |

82 | 第一行 相邻被视为同一段落。 83 |

84 |
88 | 第一行[空格][空格]
89 | 上一行结尾存在两个空格,段内换行 90 |
92 |

93 | 第一行
94 | 上一行结尾存在两个空格,段内换行。 95 |

96 |
100 | 第一行
101 |
102 | 两行之间存在空行,视为不同段落。 103 |
105 |

106 | 第一行 107 |

108 |

109 | 两行之间存在空行,视为不同段落。 110 |

111 |
114 | 115 | ****** 116 | 117 | ## 强调 118 | 119 | 删除线的 `~` 符号一般位于键盘左上角位置。 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 144 | 147 | 148 |
Markdown预览
*倾斜* 129 | 倾斜 130 |
**粗体**粗体
~~删除线~~删除线
142 | > 引用 143 | 145 |
引用
146 |
149 | 150 | ****** 151 | 152 | ## 链接和图片 153 | 154 | 为了规避某些平台的防盗链机制,图片推荐使用图床,否则在不同平台上发布需要重新上传很麻烦的,图床最好选大平台的图床,一时半会不会倒闭的那种,个人目前主要用的是微博图床 [Chrome插件-微博图床](https://chrome.google.com/webstore/detail/%E6%96%B0%E6%B5%AA%E5%BE%AE%E5%8D%9A%E5%9B%BE%E5%BA%8A/fdfdnfpdplfbbnemmmoklbfjbhecpnhf?hl=zh-CN) 以及 [Alfred脚本-微博图床](https://imciel.com/2016/07/17/weibo-picture-upload-alfred-workflow/) 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 |
Markdown预览
[GcsSloop](http://www.gcssloop.com)GcsSloop
![GcsSloop Blog](http://www.gcssloop.com/assets/siteinfo/friends/gcssloop.jpg)GcsSloop Blog
170 | 171 | ****** 172 | 173 | ## 列表 174 | 175 | 无序列表前面可以用 `*` `+` `-` 等,结果是相同的。 176 | 有序列表的数字即便不按照顺序排列,结果仍是有序的。 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 194 | 207 | 208 | 209 | 210 | 218 | 231 | 232 |
Markdown预览
187 | * 项目
188 | * 项目
189 | * 项目
190 |   * 子项目
191 |   * 子项目
192 | * 项目 193 |
195 |
    196 |
  • 项目
  • 197 |
  • 项目
  • 198 |
  • 项目 199 |
      200 |
    • 子项目
    • 201 |
    • 子项目
    • 202 |
    203 |
  • 204 |
  • 项目
  • 205 |
206 |
211 | 1. 项目
212 | 2. 项目
213 | 3. 项目
214 |   1. 子项目
215 |   2. 子项目
216 | 4. 项目 217 |
219 |
    220 |
  1. 项目
  2. 221 |
  3. 项目
  4. 222 |
  5. 项目 223 |
      224 |
    1. 子项目
    2. 225 |
    3. 子项目
    4. 226 |
    227 |
  6. 228 |
  7. 项目
  8. 229 |
230 |
233 | 234 | **** 235 | 236 | ## 下划线和特殊符号 237 | 238 | 由于 Markdown 使用一些特殊符号进行标记,当我们想要在文档中使用这些特殊符号并防止被 Markdown 转换的时候,可以使用 `\` (转义符) 将这些特殊符号进行转义。 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 250 | 253 | 254 | 255 | 258 | 259 | 260 |
Markdown预览
247 | 在一行中用三个以上的星号、减号、下划线来建立一个分隔线
248 | --- 249 |
251 |
252 |
可以利用反斜杠(转义字符)来插入一些在语法中有特殊意义的符号
256 | \*Hi\* 257 |
*Hi*
261 | 262 | **** 263 | 264 | ## 代码 265 | 266 | ### 1.行内代码 267 | 268 | 行内代码可以使用反引号来标记(反引号一般位于键盘左上角,要用英文): 269 | 270 | ``` 271 | 一句话 `行内代码` 一句话。 272 | ``` 273 | 274 | **预览:** 275 | 276 | 一句话 `行内代码` 一句话。 277 | 278 | ### 2.多行代码 279 | 280 | 多行代码使用 3 个反引号来标记(反引号一般位于键盘左上角,要用英文) ,在第一个 ````` 后面可以跟语言类型,没有语言类型可以省略不写: 281 | 282 | ``` 283 | ​``` java 284 | // 我是注释 285 | int a = 5; 286 | ​``` 287 | ``` 288 | 289 | **预览:** 290 | 291 | ``` java 292 | // 我是注释 293 | int a = 5; 294 | ``` 295 | 296 | 297 | 298 | **** 299 | 300 | ## 表格 301 | 302 | **扩展的 Markdown 支持手写表格**,格式也非常简单,第二行分割线部分可以使用 `:` 来控制内容状态。 303 | **注意,Markdown 标准(原生)语法中没有表格支持,但现在多数平台已经支持了该语法,如 GitHub,CSDN,简书 等均支持,所以写在这里:** 304 | 305 | **Markdown:** 306 | 307 | ``` 308 | | 默认 | 靠右 | 居中 | 靠左 | 309 | | ---- | ---: | :--: | :--- | 310 | | 内容 | 内容 | 内容 | 内容 | 311 | | 内容 | 内容 | 条目 | 内容 | 312 | ``` 313 | 314 | **预览:** 315 | 316 | | 默认 | 靠右 | 居中 | 靠左 | 317 | | ---- | ---: | :--: | :--- | 318 | | 内容 | 内容 | 内容 | 内容 | 319 | | 内容 | 内容 | 条目 | 内容 | 320 | 321 | 322 | ## 参考资料 323 | 324 | [Markdown-语法说明](http://www.markdown.cn/) 325 | 326 | 327 | ## About Me 328 | 329 | ### 作者微博: @GcsSloop 330 | 331 | 332 | -------------------------------------------------------------------------------- /Course/Markdown/markdown-html.md: -------------------------------------------------------------------------------- 1 | # Markdown 网页格式兼容 2 | 3 | Markdown 作为一种标记型语言,在大多数情况下都是需要转换为 HTML 格式的,所以 Markdown 理论上是兼容 HTML 语法的,在 Markdown 所提供的标记无法满足我们需要的时候,可以尝试使用 HTML 相关语法来实现。 -------------------------------------------------------------------------------- /Course/Markdown/markdown-link.md: -------------------------------------------------------------------------------- 1 | # Markdown实用技巧-链接和图片 2 | 3 | 博客地址: http://www.gcssloop.com/markdown/markdown-links 4 | 5 | Sloop 喝过半杯咖啡,涨红的脸色渐渐复了原,旁人便又问道,“ Sloop,你当真会写文章么?” Sloop 看着问他的人,显出不屑置辩的神气。他们便接着说道,“你怎的连半个赞也捞不到呢?” Sloop 立刻显出颓唐不安模样,脸上笼上了一层灰色,嘴里说些话;这回可是全是技术名词之类,一些不懂了。在这时候,众人也都哄笑起来:办公室内外充满了快活的空气。 6 | 7 | 在这些时候,我可以附和着笑,老板是决不责备的。而且老板见了 Sloop,也每每这样问他,引人发笑。Sloop 自己知道不能和他们谈天,便只好向实习生说话。有一回对我说道,“你会用 Markdown 么?” 我略略点一点头。他说,“会用 Markdown,……我便考你一考。Markdown 的链接,你是怎样写的?” 我想,码农一样的人,也配考我么?便回过脸去,不再理会。Sloop 等了许久,很恳切的说道,“不能写罢?……我教给你,记着!这些写法应该记着。将来做程序员的时候,写文档要用。”我暗想我和程序员的等级还很远呢,而且我们这里的程序员也从不写文档;又好笑,又不耐烦,懒懒的答他道,“谁要你教,不是一对方括号后面跟一对圆括号么?” Sloop 显出极高兴的样子,将两个指头的长指甲敲着电脑,点头说,“对呀对呀!……链接有4样基本写法,你知道么?” 我愈不耐烦了,努着嘴走远。Sloop 刚想打开编辑器,给我演示,见我毫不热心,便又叹一口气,显出极惋惜的样子。 8 | 9 | ***** 10 | 11 | ## 前言 12 | 13 | **本文是适用于对 Markdown 有一定了解的魔法师,以帮助他们挖掘更多关于 Markdown 的可能性,例如:链接的不同类型以及使用方式,如何在新标签页打开链接,如何控制图片大小等,对 Markdown 还不了解的魔法师请参考 [Markdown快速入门][markdown-start] 和 [Markdown基础语法][markdown-grammar] 。** 14 | 15 | **注意:以下的部分语法不属于标准语法,存在不兼容的问题,不能保证所有平台都能够使用。对于非标准语法(拓展语法)我会进行标注说明。** 16 | 17 | ***** 18 | 19 | ## 行内式链接: 20 | 21 | ```markdown 22 | 博客地址: [GcsSloop](http://www.gcssloop.com) 23 | 博客地址: [GcsSloop](http://www.gcssloop.com "GcsSloop的博客") 24 | ``` 25 | 26 | 博客地址: [GcsSloop](http://www.gcssloop.com) 27 | 博客地址: [GcsSloop](http://www.gcssloop.com "GcsSloop的博客") 28 | 29 | ***** 30 | 31 | ## 参考式链接: 32 | 33 | ```markdown 34 | [GcsSloop的博客][gcssloop] 35 | 36 | [gcssloop]: http://www.gcssloop.com 37 | // 或者 38 | [gcssloop]: http://www.gcssloop.com "点击访问GcsSloop的博客" 39 | ``` 40 | 41 | [GcsSloop的博客][gcssloop] 42 | 43 | **为什么要使用参考式呢?** 44 | 在写文章的时候很可能会在文章不同的地方引用同一篇文章,使用参考式可以少写一点字符。 45 | 更重要的是,参考文章的链接可能会改变,如果将参考链接统一写在文末的话,改起来会更容易。 46 | 47 | ***** 48 | 49 | ## 自动链接: 50 | 51 | ```markdown 52 | 53 | ``` 54 | 55 | 56 | 57 | ***** 58 | 59 | ## 相对链接: 60 | 61 | **如果你的内容是发布在 GitHub 或者自己的个人网站上,那么相对链接是一个很好用的东西**,例如引用本站的一张图片可以这样写: 62 | 63 | ```markdown 64 | ![头像](/assets/siteinfo/avatar.jpg) 65 | ``` 66 | 67 | ![头像](/assets/siteinfo/avatar.jpg) 68 | 69 | **相对链接优点:** 70 | 71 | * 线下预览和线上效果相同,不受网络影响。 72 | * 避免了分别上传图片导致的麻烦。 73 | * GitHub复制仓库更方便,改仓库名字不会导致资源链接失效。 74 | 75 | **相对链接缺点:** 76 | 77 | * 不适用于需要在多平台发布的文章。 78 | * 某些本地编辑器不支持读取相对链接。 79 | 80 | ***** 81 | 82 | ## 图片链接: 83 | 84 | 图片链接其实就是把图片和链接嵌套在一起: 85 | 86 | > 顺便为 DiyCode 打一个小广告,欢迎更多小伙伴的加入。 87 | 88 | ```markdown 89 | [![](http://ww1.sinaimg.cn/large/005Xtdi2jw1f9e6ii0bsgj30xc04gaau.jpg)](http://www.diycode.cc/wiki/encouragement) 90 | ``` 91 | 92 | [![](http://ww1.sinaimg.cn/large/005Xtdi2jw1f9e6ii0bsgj30xc04gaau.jpg)](http://www.diycode.cc/wiki/encouragement){:target="_blank"} 93 | 94 | ***** 95 | 96 | IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII 97 | 98 | **注意:从这里开始往下的部分,属于拓展语法,可能存在某些平台(编辑器)无法识别的问题,请亲自测试后再使用。** 99 | 100 | IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII 101 | 102 | ## 在新标签页打开: 103 | 104 | Markdown 的默认链接方式都是在当前标签页打开的,这就会导致打开链接之后原始页面被覆盖掉,如果想要让链接在新标签页打开可以在后面添加上 `{:target="_blank"}` 或者使用 HTML 语法。 105 | 106 | ``` 107 | [GcsSloop](http://www.gcssloop.com){:target="_blank"} 108 | 109 | [GcsSloop的博客][gcssloop]{:target="_blank"} 110 | 111 | {:target="_blank"} 112 | 113 | 关于GcsSloop 114 | ``` 115 | 116 | [GcsSloop](http://www.gcssloop.com){:target="_blank"}
117 | [GcsSloop的博客][gcssloop]{:target="_blank"}
118 | {:target="_blank"}
119 | 关于GcsSloop 120 | 121 | 注意: 部分平台可能不识别 `{:target="_blank"}` 标签,例如你正在看的这个 GitHub 就不识别。。 122 | 123 | ***** 124 | 125 | ## 注释(脚注): 126 | 127 | 注释一般用于解释一些专业名词或者难以理解的内容,由于注释的解释部分一般放在文末,所以又称为脚注: 128 | 129 | ``` 130 | GcsSloop[^1]是一个超级魔法师[^2] 。 131 | 132 | [^1]: GcsSloop:非著名程序员。 133 | [^2]: 魔法师:会魔法的人类 134 | ``` 135 | 136 | GcsSloop[^1]是一个超级魔法师[^2] 。 137 | 138 | 注意:脚注不论在何处定义,最终都是显示在文末。部分平台不识别该语法,GitHub 依旧不识别。 139 | 140 | ***** 141 | 142 | ## 控制图片大小 143 | 144 | 使用 `{:width="300" height="100"}` 或者 HTML 格式可以控制图片显示大小,图片有宽度(width)和高度(height)两个属性,如果只指定了一个,另一个会按照比例缩放。 145 | 146 | ``` 147 | ![](http://ww1.sinaimg.cn/large/005Xtdi2jw1f9k7b8a6vmj312w0rg143.jpg){:width="300"} 148 | 149 | 150 | ``` 151 | 152 | ![](http://ww1.sinaimg.cn/large/005Xtdi2jw1f9k7b8a6vmj312w0rg143.jpg){:width="300"} 153 | 154 | 右键,新标签页打开图片,你会发现原图其实挺大的。 155 | 注意: 部分平台可能不识别 `{:width="300" height="100"}` 标签,你正在看的这个 GitHub 依旧不识别。 156 | 157 | ***** 158 | 159 | ## 总结: 160 | 161 | Markdown 存在很多的变种,对其语法进行了不同程度的拓展,使其更加的强大,但是使用拓展语法之前请三思。个人建议如下: 162 | 163 | * 如果文章(文档)只在单一平台发布,使用任何该平台支持的拓展语法都没问题。 164 | * 如果文章(文档)需要在多个平台发布,尽量使用标准语法,使用拓展语法之前请注意测试平台兼容性。 165 | * 图片尽量使用图床管理,而且要进行本地备份。 166 | 167 | 168 | ***** 169 | 170 | ## About Me 171 | 172 | ### 作者微博: @GcsSloop 173 | 174 | 175 | 176 | ## 参考链接: 177 | 178 | [Markdown-基础语法](http://www.markdown.cn/) 179 | [Markdown语法:在新窗口新标签页中打开](http://yinping4256.github.io/cn/Markdown%E8%AF%AD%E6%B3%95%E5%9C%A8%E6%96%B0%E7%AA%97%E5%8F%A3%E6%96%B0%E6%A0%87%E7%AD%BE%E9%A1%B5%E4%B8%AD%E6%89%93%E5%BC%80/) 180 | 181 | 182 | ### 注释: 183 | 184 | [^1]: GcsSloop:非著名程序员。 185 | [^2]: 魔法师:会魔法的人类 186 | 187 | 188 | 189 | 190 | [markdown-start]: http://www.gcssloop.com/markdown/markdown-star "Markdown实用技巧-快速入门" 191 | 192 | [markdown-grammar]: http://www.gcssloop.com/markdown/markdown-grammar "Markdown实用技巧-基础语法" 193 | 194 | [gcssloop]: http://www.gcssloop.com "点击访问GcsSloop的博客" 195 | -------------------------------------------------------------------------------- /Course/Markdown/markdown-start.md: -------------------------------------------------------------------------------- 1 | # Markdown 快速入门 2 | 3 | 自从接触了 Markdown 之后,就一直用 Markdown 作为自己的主要书写工具,不论是平时做一些简单的纪录,还是用来写博客,写文档都是非常方便。你现在看到的这篇文章就是用 Markdown 进行书写的。 4 | 5 | > 我最早因为 GitHub 而了解到 Markdown,当是支持 Markdown 的平台并不多,现在发现很多平台都已经开始支持 Markdown了,不论是老牌的 CSDN 还是比较新的 简书、掘金、DiyCode 等都支持使用Markdown进行写作,借此趋势,赶紧向还不了解 Markdown 的魔法师强势安利一波。 6 | > 7 | > **如果你已经开始使用 Markdown了,那么本文作用对你可以能并不大,请看后续文章。** 8 | 9 | 10 | 11 | **** 12 | 13 | ## 什么是 Markdown ? 14 | 15 | **Markdown 是一种轻量级标记语言,创始人为約翰·格魯伯(John Gruber)。 它允许人们“使用易读易写的纯文本格式编写文档,然后转换成有效的XHTML(或者HTML)文档”。** 16 | 17 | 相比于 HTML (~~How To Make Love~~ 大雾), **Markdown 更加精简,更加注重内容,其主要宗旨是「易读易写」** 一般 Markdown 最终都是要转换为 HTML 的,可用于书写博客或者网页,但借助某些工具,可以讲 Markdown 转换为 pdf,word,Latex 等其他常见的文件格式。 18 | 19 | 20 | 21 | **** 22 | 23 | ## 为什么选择 Markdown ? 24 | 25 | **选择 Markdown 但理由只有一个:方便,节省时间!** 26 | 27 | 至于为什么这样说,请看下面内容: 28 | 29 | * **语法简洁**,没有任何编程基础的人十几分钟语言即可入门。 30 | * **注重内容**,专注于内容编写,不再因为格式拍版而苦恼 (word格式刷工具哭晕在厕所)。 31 | * **易阅读性**,即便是没有经过转换的 Markdown 文件,大部分文字内容仍可阅读。 32 | * **易编辑性**,任何文本编辑器都能编辑 Markdown 文件。 33 | * **跨平台性**,任何平台均能打开 Markdown 文件,由于是纯文本文件,不存在格式兼容的问题。 34 | * **导出方便**,支持导出为 HTML,PDF,Word(.docx),LaTex 等常见格式(需要工具支持)。 35 | 36 | 在 Windows 上编写的文档,非常方便的就能在 Mac 上继续编辑,方便数据迁移,降低沟通成本。 37 | 38 | 39 | 40 | **** 41 | 42 | ## Markdown 存在的问题 43 | 44 | 前面吹嘘了 Markdown 的那么多优点,下面就说一下其中的不足: 45 | 46 | * **图片问题**,很多人都觉得 Markdown 文件插入图片麻烦,还要自己上传找链接。 47 | * **语法兼容**,基础语法是兼容的,但不同工具(平台)的扩展语法不兼容(由于没有统一标准)。 48 | * **细节控制**,Markdown只提供最基础的格式,其显示样式主要由CSS控制,很难针对性的控制部分内容。 49 | 50 | 以上应该是 Markdown 最常见的一些麻烦,不过不必担心,**后续文章会教大家来如何解决这些问题,取其精华,去其糟粕,让 Markdown 运用得心应手**。 51 | 52 | **** 53 | 54 | ## Markdown 编辑器推荐 55 | 56 | 俗话说,工欲善其事,必先利其器,虽然 Markdown 用任何文本编辑器都能打开编辑,但仍需要专业工具进行转化,常见 Markdown 编辑器我基本上都尝试用过,在此简单推荐几种,大家找适合自己的就行。 57 | 58 | **仅推荐本地编辑器,在线编辑器根据需要自己选择,很多平台都已经支持直接用 Markdown 进行编辑了。** 59 | 60 | | 编辑器 | 支持平台 | 支持导出格式 | 61 | | ---------------------------------------- | --------------------------- | ---------------------------------------- | 62 | | [**Typora**](http://www.typora.io/)
正在开发, 界面简洁, 对 HTML 语法支持较弱, 支持导出文件类型较多。 | Mac、
Windows、
Linux | HTML、
Word(.docx)、
PDF、
LaTex 等 | 63 | | [**EME**](https://eme.moe/)
正在开发, 界面简洁,对 HTML 语法支持友好,支持导出文件类型较少。 | Mac、
Windows、
Linux | 仅 PDF | 64 | | [**Mou**](http://25.io/mou/)
停止开发, 界面简洁, 对 HTML 语法支持友好, 但支持导出文件类型较少。 | Mac | HTML、
PDF | 65 | | [**Sublime Text**](http://www.sublimetext.com/3)
神兵利器,需要安装插件才能使用, 相对比较麻烦, 适合高级魔法师。 | Mac、
Windows、
Linux | 多种格式(插件) | 66 | | [**Atom**](https://atom.io/)
神兵利器, 支持多种常见的编程语言, 如果仅仅是为了写 Markdown 不推荐安装。 | Mac、
Windows、
Linux | HTML、
PDF(插件) | 67 | 68 | **** 69 | 70 | ## 快速入门 71 | 72 | 本文是为了帮助还不了解 Markdown 的魔法师有一个简单的认知,所以快速入门只说最基本的一些内容。 73 | 74 | 75 | 76 | ### 标题 77 | 78 | 通过在行首插入 1 到 6 个 `#` ,来定义对应的 1 到 6 阶 标题: 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
Markdown预览
# 一级标题

一级标题

## 二级标题

二级标题

### 三级标题

三级标题

98 | 99 | 100 | 101 | ### 分段 102 | 103 | 在 Markdown 中段落由一行或者多行文本组成,相邻的两行文字会被视为同一段落,如果存在空行则被视为不同段落( Markdown 对空行的定义是看起来是空行就是空行,即使空行中存在 `空格` `TAB` `回车` 等不可见字符,同样会被视为空行)。 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 115 | 120 | 121 | 122 | 127 | 135 | 136 |
Markdown预览
112 | 第一行
113 | 相邻被视为同一段落。 114 |
116 |

117 | 第一行 相邻被视为同一段落。 118 |

119 |
123 | 第一行
124 |
125 | 两行之间存在空行,视为不同段落。 126 |
128 |

129 | 第一行 130 |

131 |

132 | 两行之间存在空行,视为不同段落。 133 |

134 |
137 | 138 | 139 | 140 | ### 链接和图片 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 |
Markdown预览
[GcsSloop](http://www.gcssloop.com)GcsSloop
![GcsSloop Blog](http://www.gcssloop.com/assets/siteinfo/friends/gcssloop.jpg)GcsSloop Blog
156 | 157 | 158 | 159 | 知道了上面这些内容就已经算是入门了,可以用 Markdown 进行快乐的写作,如果想了解更多语法相关内容,请看下一篇 [Markdown实用技巧-基础语法][markdown-grammar] 160 | 161 | 162 | ## 参考资料 163 | 164 | [Markdown-基础语法](http://www.markdown.cn/) 165 | 166 | 167 | ## About Me 168 | 169 | ### 作者微博: @GcsSloop 170 | 171 | 172 | 173 | [markdown-grammar]: /markdown/markdown-grammar "Markdown实用技巧-语法" 174 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /Course/README.md: -------------------------------------------------------------------------------- 1 | # 教程类 2 | 3 | 4 |

5 | 6 | 7 | 8 |

9 | -------------------------------------------------------------------------------- /Course/ReleaseLibraryByJitPack.md: -------------------------------------------------------------------------------- 1 | # 优雅的发布Android开源库(论JitPack的优越性) 2 | ### 作者微博: @GcsSloop 3 | ### [JitPack地址](https://jitpack.io) 4 | 5 | ## 前言 6 | 自从谷歌宣布不支持Eclipse之后,大批Android程序猿情愿或者不情愿的迁移到了AndroidStudio,从此过上了使用Gradle构建程序的"优越"生活。 7 | 8 | > 关于Gradle的坑,就不吐槽了,我怕一会儿控制不住情绪。今天我们就谈一下Gradle的优越性。 9 | 10 | 说到Gradle的优越性,其中有一点比较明显的就是依赖开源库更加方便了,基本上一两行代码就能搞定。免去了还要手动下载自己配置的痛苦。 11 | 12 | 然而,这也仅仅是对使用者而言,而对于发布这些开源库的人就苦逼了,主要是上传太痛苦。 13 | 14 | 目前来说,比较常见的 Android 开源库托管地址有以下几类: 15 | 16 | 类型 | 吐槽 17 | ------------- | ------------ 18 | Maven Central | 发布过程繁杂冗长, 每次发布成功都应该感谢一下上苍的厚爱。 19 | jCenter | jCenter貌似稍微简单一点,但也不是省油的灯。 20 | 自定义仓库 | 一般的猿猿玩不起,企业内部可能会见到。 21 | 22 | 在这些托管地址上面发布过项目的都应该能理解其中的痛苦,不说了,让我哭会儿(我就是那个每次发布都折腾半天的“bug狂魔”,从未一次发布就成功过)。 23 | 24 | 然而,现在福音来了,JitPack可以帮助你简单快速的发布开源库。 25 | 26 | ## 在正式讲解之前我们先了解一下JitPack 27 | 28 | **JitPack是什么?** 29 | 30 | > **JitPack是一个自定义的Maven仓库。** 31 | 32 | **JitPack安全吗?** 33 | 34 | > **个人还是比较安全的,毕竟开源库都是给大家用的,源码都能分享出来,如果你是担心它在里面插入恶意代码的话,在AndroidStudio的 External Libraies里面能够看到反编译的依赖库的源码,可以查看一下。** 35 | 36 | **JitPack好处都有啥!说对我就给他用(金坷垃,雾)** 37 | 38 | > **省时间,省时间,省时间,省下的时间都够你修复好几个bug了。** 39 | 40 | 简单的了解了JitPack之后,开始本篇的正文。 41 | 42 | 43 | # 如何在JitPack上发布你的Library 44 | 45 | **首先,假设大家已经具备了以下条件:** 46 | 47 | 序号 | 条件 48 | :---:|--------- 49 | 1 | 会使用GitHub,能提交项目到GitHub上 50 | 2 | 使用AndroidStudio,且Gradle版本在2.4以上 51 | 52 | 在具备了这些条件之后,正式开始发布一个项目(以我的一个[工具仓库Sutil](https://github.com/GcsSloop/SUtil)为例)。 53 | 54 | ## 第 1 步: 新建一个Project 55 | 56 | 在AndroidStudio中新建一个Project用于发布项目,新建完成之后结果是这样子: 57 | 58 | ![](http://ww1.sinaimg.cn/large/005Xtdi2jw1f239wl5amtj30rs0gon0g.jpg) 59 | 60 | ## 第 2 步: 在这个Project中添加一个Library 61 | 62 | 添加的这个Library就是我要发布的仓库,Library的名字无所谓,可以随便起(*我这里就叫library*)。添加完成之后是这样子: 63 | 64 | ![](http://ww2.sinaimg.cn/large/005Xtdi2jw1f239xb835xj30rs0gowiv.jpg) 65 | 66 | ### 图中的几个标注 67 | 68 | 序号 | 解释 69 | :---:|------- 70 | 1 | 新添加的Library 71 | 2 | Library的build.gradle 72 | 3 | Library的plugin 73 | 74 | **其中library的plugin是下面这样子:** 75 | 76 | ``` gradle 77 | apply plugin: 'com.android.library' 78 | ``` 79 | 80 | ## 第 3 步: 给你的项目添加配置(重点) 81 | 82 | 你需要对你的项目简单的配置一下: 83 | 84 | ![](http://ww4.sinaimg.cn/large/005Xtdi2jw1f239y5xsj4j30rs0gowit.jpg) 85 | 86 | **在你项目的根节点的 build.gradle(图示1) 中添加如下代码:** 87 | 88 | ``` gradle 89 | buildscript { 90 | dependencies { 91 | // 重点就是下面这一行(上面两行是为了定位这一行的添加位置) 92 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3' 93 | ``` 94 | **[完整示例](https://github.com/GcsSloop/SUtil/blob/master/build.gradle)** 95 | 96 | **在你要发布的library的 build.gradle(图示2) 中添加如下代码:** 97 | 98 | ``` gradle 99 | apply plugin: 'com.github.dcendents.android-maven' 100 | 101 | group='com.github.YourUsername' 102 | ``` 103 | **[完整示例](https://github.com/GcsSloop/SUtil/blob/master/library/build.gradle)** 104 | 105 | ## 第 4 步: 提交项目到GitHub仓库 106 | 107 | 这一步就不多啰嗦了,不论你是用命令行还是客户端都可以。 108 | 109 | **为了提交更加快速,你可以删除无用的文件(文件夹),至于需要保留哪些文件你可以参考官方给出的[示例仓库](https://github.com/jitpack/android-example)** 110 | 111 | ## 第 5 步: Release你的仓库或者给你的仓库打一个Tag(重点) 112 | 113 | ### 1.点击图示进入Release界面 114 | ![](http://ww3.sinaimg.cn/large/005Xtdi2jw1f239yqr44cj30rs0goadk.jpg) 115 | 116 | ### 2.创建一个Release或Tag 117 | ![](http://ww3.sinaimg.cn/large/005Xtdi2jw1f239z1dnr8j30rs0goaco.jpg) 118 | 119 | ### 3.填写基本信息 120 | ![](http://ww4.sinaimg.cn/large/005Xtdi2jw1f239zctc5xj30rs0goq7w.jpg) 121 | 122 | ### 4.完成 123 | ![](http://ww1.sinaimg.cn/large/005Xtdi2jw1f239zpfkcwj30rs0gogns.jpg) 124 | 125 | ## 第 6 步: 将你的仓库地址提交到JitPack(重点) 126 | 127 | ### 1.将你的仓库地址提交到[JitPack](https://jitpack.io) 128 | **[JitPack地址戳这里](https://jitpack.io)** 129 | 130 | ![](http://ww3.sinaimg.cn/large/005Xtdi2jw1f23a055uoej30rs0godi0.jpg) 131 | 132 | 序号 | 解释 133 | :---:|-------- 134 | 1 | 粘贴你的仓库地址 135 | 2 | 点击这里查看 136 | 3 | 版本号 137 | 4 | 点击这里提交该版本 138 | 5 | 提交完成后自动生成的日志 139 | 140 | ### 2.JitPack自动生成的配置信息 141 | 在上传完成之后,JitPack会自动生成引用该仓库的配置信息,如下: 142 | 143 | ![](http://ww4.sinaimg.cn/large/005Xtdi2jw1f23a0fd5iyj30rs0gotb2.jpg) 144 | 145 | **以上就是教程的全部内容,各位小伙伴可以回去愉快的发布自己的开源库了。** 146 | ### [JitPack地址](https://jitpack.io) 147 | 148 | ## About Me 149 | 150 | ### 作者微博: @GcsSloop 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | -------------------------------------------------------------------------------- /Course/jitpack-javadoc.md: -------------------------------------------------------------------------------- 1 | # 用JitPack发布时附加文档和源码 2 | 3 | 很早之前写过一篇[用JitPack发布Android开源库](http://www.gcssloop.com/course/PublishLibraryByJitPack/)的文章,有小伙伴反馈说**发布到JitPack上的开源库没有文档注释,使用起来很不方便**,这是我的失误,上一篇文章只是讲解了如何使用JitPack发布开源库,最终发布的只有arr(即编译好的动态链接库),不仅没有文档注释(Javadoc),也没有源码(sources),本次就教大家如何在发布同时添加上注释和源码。 4 | 5 | **由于JitPack本身就是一个自定义Maven仓库,所以配置方式与Maven基本一样。** 6 | 7 | 8 | 9 | ### 配置项目的 build.gradle 10 | 11 | 项目的 build.gradle 配置和上一篇一样,没有变化。 12 | 13 | ```java 14 | buildscript { 15 | dependencies { 16 | // 重点就是下面这一行(上面两行是为了定位这一行的添加位置) 17 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3' 18 | ``` 19 | 20 | 21 | 22 | ### 配置 Library 的 build.gradle 23 | 24 | 完整示例(重点内容已经用注释标出): 25 | 26 | ```java 27 | apply plugin: 'com.android.library' 28 | apply plugin: 'com.github.dcendents.android-maven' // 添加这个 29 | 30 | group='com.github.GcsSloop' // 指定group,com.github.<用户名> 31 | 32 | android { 33 | compileSdkVersion 23 34 | buildToolsVersion "23.0.3" 35 | 36 | defaultConfig { 37 | minSdkVersion 7 38 | targetSdkVersion 23 39 | versionCode 1 40 | versionName "1.0" 41 | } 42 | buildTypes { 43 | release { 44 | minifyEnabled false 45 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 46 | } 47 | } 48 | } 49 | 50 | dependencies { 51 | compile fileTree(dir: 'libs', include: ['*.jar']) 52 | testCompile 'junit:junit:4.12' 53 | compile 'com.android.support:appcompat-v7:23.4.0' 54 | } 55 | 56 | //--------------------------------------------- 57 | 58 | // 指定编码 59 | tasks.withType(JavaCompile) { 60 | options.encoding = "UTF-8" 61 | } 62 | 63 | // 打包源码 64 | task sourcesJar(type: Jar) { 65 | from android.sourceSets.main.java.srcDirs 66 | classifier = 'sources' 67 | } 68 | 69 | task javadoc(type: Javadoc) { 70 | failOnError false 71 | source = android.sourceSets.main.java.sourceFiles 72 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 73 | classpath += configurations.compile 74 | } 75 | 76 | // 制作文档(Javadoc) 77 | task javadocJar(type: Jar, dependsOn: javadoc) { 78 | classifier = 'javadoc' 79 | from javadoc.destinationDir 80 | } 81 | 82 | artifacts { 83 | archives sourcesJar 84 | archives javadocJar 85 | } 86 | ``` 87 | 88 | 89 | 90 | ### 发布参照上一篇文章: [使用JitPack发布开源库](http://www.gcssloop.com/course/PublishLibraryByJitPack/) 91 | 92 | 93 | 94 | ### 查看在线文档 95 | 96 | 如果你在JitPack配置了文档和源码支持,在引用同时就能看到源码的文档,不仅如此,你也可以在线查看。 97 | 98 | 查看地址是 `https://jitpack.io/com/github/USER/REPO/VERSION/javadoc/` 99 | 100 | 例如我的一个开源库: https://jitpack.io/com/github/GcsSloop/ViewSupport/v1.2.2/javadoc/ 101 | 102 | 在线API文档样式: 103 | ![](http://ww1.sinaimg.cn/large/005Xtdi2jw1f7o8gabelfj31400mbjy0.jpg) -------------------------------------------------------------------------------- /CustomView/Advance/Code/CheckView.java: -------------------------------------------------------------------------------- 1 | package com.sloop.canvas; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.BitmapFactory; 6 | import android.graphics.Canvas; 7 | import android.graphics.Paint; 8 | import android.graphics.Rect; 9 | import android.os.Handler; 10 | import android.os.Message; 11 | import android.util.AttributeSet; 12 | import android.util.Log; 13 | import android.view.View; 14 | 15 | /** 16 | * 25 | */ 26 | public class CheckView extends View { 27 | 28 | private static final int ANIM_NULL = 0; //动画状态-没有 29 | private static final int ANIM_CHECK = 1; //动画状态-开启 30 | private static final int ANIM_UNCHECK = 2; //动画状态-结束 31 | 32 | private Context mContext; // 上下文 33 | private int mWidth, mHeight; // 宽高 34 | private Handler mHandler; // handler 35 | 36 | private Paint mPaint; 37 | private Bitmap okBitmap; 38 | 39 | private int animCurrentPage = -1; // 当前页码 40 | private int animMaxPage = 13; // 总页数 41 | private int animDuration = 500; // 动画时长 42 | private int animState = ANIM_NULL; // 动画状态 43 | 44 | private boolean isCheck = false; // 是否只选中状态 45 | 46 | public CheckView(Context context) { 47 | super(context, null); 48 | 49 | } 50 | 51 | public CheckView(Context context, AttributeSet attrs) { 52 | super(context, attrs); 53 | init(context); 54 | } 55 | 56 | /** 57 | * 初始化 58 | * @param context 59 | */ 60 | private void init(Context context) { 61 | mContext = context; 62 | 63 | mPaint = new Paint(); 64 | mPaint.setColor(0xffFF5317); 65 | mPaint.setStyle(Paint.Style.FILL); 66 | mPaint.setAntiAlias(true); 67 | 68 | okBitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.checkres); 69 | 70 | mHandler = new Handler() { 71 | @Override 72 | public void handleMessage(Message msg) { 73 | super.handleMessage(msg); 74 | if (animCurrentPage < animMaxPage && animCurrentPage >= 0) { 75 | invalidate(); 76 | if (animState == ANIM_NULL) 77 | return; 78 | if (animState == ANIM_CHECK) { 79 | 80 | animCurrentPage++; 81 | } else if (animState == ANIM_UNCHECK) { 82 | animCurrentPage--; 83 | } 84 | 85 | this.sendEmptyMessageDelayed(0, animDuration / animMaxPage); 86 | Log.e("AAA", "Count=" + animCurrentPage); 87 | } else { 88 | if (isCheck) { 89 | animCurrentPage = animMaxPage - 1; 90 | } else { 91 | animCurrentPage = -1; 92 | } 93 | invalidate(); 94 | animState = ANIM_NULL; 95 | } 96 | } 97 | }; 98 | } 99 | 100 | 101 | /** 102 | * View大小确定 103 | * @param w 104 | * @param h 105 | * @param oldw 106 | * @param oldh 107 | */ 108 | @Override 109 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 110 | super.onSizeChanged(w, h, oldw, oldh); 111 | mWidth = w; 112 | mHeight = h; 113 | } 114 | 115 | /** 116 | * 绘制内容 117 | * @param canvas 118 | */ 119 | @Override 120 | protected void onDraw(Canvas canvas) { 121 | super.onDraw(canvas); 122 | 123 | // 移动坐标系到画布中央 124 | canvas.translate(mWidth / 2, mHeight / 2); 125 | 126 | // 绘制背景圆形 127 | canvas.drawCircle(0, 0, 240, mPaint); 128 | 129 | // 得出图像边长 130 | int sideLength = okBitmap.getHeight(); 131 | 132 | // 得到图像选区 和 实际绘制位置 133 | Rect src = new Rect(sideLength * animCurrentPage, 0, sideLength * (animCurrentPage + 1), sideLength); 134 | Rect dst = new Rect(-200, -200, 200, 200); 135 | 136 | // 绘制 137 | canvas.drawBitmap(okBitmap, src, dst, null); 138 | } 139 | 140 | 141 | /** 142 | * 选择 143 | */ 144 | public void check() { 145 | if (animState != ANIM_NULL || isCheck) 146 | return; 147 | animState = ANIM_CHECK; 148 | animCurrentPage = 0; 149 | mHandler.sendEmptyMessageDelayed(0, animDuration / animMaxPage); 150 | isCheck = true; 151 | } 152 | 153 | /** 154 | * 取消选择 155 | */ 156 | public void unCheck() { 157 | if (animState != ANIM_NULL || (!isCheck)) 158 | return; 159 | animState = ANIM_UNCHECK; 160 | animCurrentPage = animMaxPage - 1; 161 | mHandler.sendEmptyMessageDelayed(0, animDuration / animMaxPage); 162 | isCheck = false; 163 | } 164 | 165 | /** 166 | * 设置动画时长 167 | * @param animDuration 168 | */ 169 | public void setAnimDuration(int animDuration) { 170 | if (animDuration <= 0) 171 | return; 172 | this.animDuration = animDuration; 173 | } 174 | 175 | /** 176 | * 设置背景圆形颜色 177 | * @param color 178 | */ 179 | public void setBackgroundColor(int color){ 180 | mPaint.setColor(color); 181 | } 182 | } -------------------------------------------------------------------------------- /CustomView/Advance/Code/CheckView.md: -------------------------------------------------------------------------------- 1 | ## CheckView源代码 2 | 3 | [下载代码 ( 右键 -> 另存为 )](https://raw.githubusercontent.com/GcsSloop/AndroidNote/master/CustomView/Advance/Code/CheckView.java) 4 | 5 | ``` java 6 | package com.sloop.canvas; 7 | 8 | import android.content.Context; 9 | import android.graphics.Bitmap; 10 | import android.graphics.BitmapFactory; 11 | import android.graphics.Canvas; 12 | import android.graphics.Paint; 13 | import android.graphics.Rect; 14 | import android.os.Handler; 15 | import android.os.Message; 16 | import android.util.AttributeSet; 17 | import android.util.Log; 18 | import android.view.View; 19 | 20 | /** 21 | * 30 | */ 31 | public class CheckView extends View { 32 | 33 | private static final int ANIM_NULL = 0; //动画状态-没有 34 | private static final int ANIM_CHECK = 1; //动画状态-开启 35 | private static final int ANIM_UNCHECK = 2; //动画状态-结束 36 | 37 | private Context mContext; // 上下文 38 | private int mWidth, mHeight; // 宽高 39 | private Handler mHandler; // handler 40 | 41 | private Paint mPaint; 42 | private Bitmap okBitmap; 43 | 44 | private int animCurrentPage = -1; // 当前页码 45 | private int animMaxPage = 13; // 总页数 46 | private int animDuration = 500; // 动画时长 47 | private int animState = ANIM_NULL; // 动画状态 48 | 49 | private boolean isCheck = false; // 是否只选中状态 50 | 51 | public CheckView(Context context) { 52 | super(context, null); 53 | 54 | } 55 | 56 | public CheckView(Context context, AttributeSet attrs) { 57 | super(context, attrs); 58 | init(context); 59 | } 60 | 61 | /** 62 | * 初始化 63 | * @param context 64 | */ 65 | private void init(Context context) { 66 | mContext = context; 67 | 68 | mPaint = new Paint(); 69 | mPaint.setColor(0xffFF5317); 70 | mPaint.setStyle(Paint.Style.FILL); 71 | mPaint.setAntiAlias(true); 72 | 73 | okBitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.checkres); 74 | 75 | mHandler = new Handler() { 76 | @Override 77 | public void handleMessage(Message msg) { 78 | super.handleMessage(msg); 79 | if (animCurrentPage < animMaxPage && animCurrentPage >= 0) { 80 | invalidate(); 81 | if (animState == ANIM_NULL) 82 | return; 83 | if (animState == ANIM_CHECK) { 84 | 85 | animCurrentPage++; 86 | } else if (animState == ANIM_UNCHECK) { 87 | animCurrentPage--; 88 | } 89 | 90 | this.sendEmptyMessageDelayed(0, animDuration / animMaxPage); 91 | Log.e("AAA", "Count=" + animCurrentPage); 92 | } else { 93 | if (isCheck) { 94 | animCurrentPage = animMaxPage - 1; 95 | } else { 96 | animCurrentPage = -1; 97 | } 98 | invalidate(); 99 | animState = ANIM_NULL; 100 | } 101 | } 102 | }; 103 | } 104 | 105 | 106 | /** 107 | * View大小确定 108 | * @param w 109 | * @param h 110 | * @param oldw 111 | * @param oldh 112 | */ 113 | @Override 114 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 115 | super.onSizeChanged(w, h, oldw, oldh); 116 | mWidth = w; 117 | mHeight = h; 118 | } 119 | 120 | /** 121 | * 绘制内容 122 | * @param canvas 123 | */ 124 | @Override 125 | protected void onDraw(Canvas canvas) { 126 | super.onDraw(canvas); 127 | 128 | // 移动坐标系到画布中央 129 | canvas.translate(mWidth / 2, mHeight / 2); 130 | 131 | // 绘制背景圆形 132 | canvas.drawCircle(0, 0, 240, mPaint); 133 | 134 | // 得出图像边长 135 | int sideLength = okBitmap.getHeight(); 136 | 137 | // 得到图像选区 和 实际绘制位置 138 | Rect src = new Rect(sideLength * animCurrentPage, 0, sideLength * (animCurrentPage + 1), sideLength); 139 | Rect dst = new Rect(-200, -200, 200, 200); 140 | 141 | // 绘制 142 | canvas.drawBitmap(okBitmap, src, dst, null); 143 | } 144 | 145 | 146 | /** 147 | * 选择 148 | */ 149 | public void check() { 150 | if (animState != ANIM_NULL || isCheck) 151 | return; 152 | animState = ANIM_CHECK; 153 | animCurrentPage = 0; 154 | mHandler.sendEmptyMessageDelayed(0, animDuration / animMaxPage); 155 | isCheck = true; 156 | } 157 | 158 | /** 159 | * 取消选择 160 | */ 161 | public void unCheck() { 162 | if (animState != ANIM_NULL || (!isCheck)) 163 | return; 164 | animState = ANIM_UNCHECK; 165 | animCurrentPage = animMaxPage - 1; 166 | mHandler.sendEmptyMessageDelayed(0, animDuration / animMaxPage); 167 | isCheck = false; 168 | } 169 | 170 | /** 171 | * 设置动画时长 172 | * @param animDuration 173 | */ 174 | public void setAnimDuration(int animDuration) { 175 | if (animDuration <= 0) 176 | return; 177 | this.animDuration = animDuration; 178 | } 179 | 180 | /** 181 | * 设置背景圆形颜色 182 | * @param color 183 | */ 184 | public void setBackgroundColor(int color){ 185 | mPaint.setColor(color); 186 | } 187 | } 188 | 189 | ``` 190 | -------------------------------------------------------------------------------- /CustomView/Advance/Code/SearchView.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Author: GcsSloop 3 | *

4 | * Created Date: 16/5/31 5 | *

6 | * Copyright (C) 2016 GcsSloop. 7 | *

8 | * GitHub: https://github.com/GcsSloop 9 | */ 10 | public class SearchView extends View { 11 | 12 | // 画笔 13 | private Paint mPaint; 14 | 15 | // View 宽高 16 | private int mViewWidth; 17 | private int mViewHeight; 18 | 19 | public SearchView(Context context) { 20 | this(context,null); 21 | } 22 | 23 | public SearchView(Context context, AttributeSet attrs) { 24 | super(context, attrs); 25 | initAll(); 26 | } 27 | 28 | public void initAll() { 29 | 30 | initPaint(); 31 | 32 | initPath(); 33 | 34 | initListener(); 35 | 36 | initHandler(); 37 | 38 | initAnimator(); 39 | 40 | // 进入开始动画 41 | mCurrentState = State.STARTING; 42 | mStartingAnimator.start(); 43 | 44 | } 45 | 46 | // 这个视图拥有的状态 47 | public static enum State { 48 | NONE, 49 | STARTING, 50 | SEARCHING, 51 | ENDING 52 | } 53 | 54 | // 当前的状态(非常重要) 55 | private State mCurrentState = State.NONE; 56 | 57 | // 放大镜与外部圆环 58 | private Path path_srarch; 59 | private Path path_circle; 60 | 61 | // 测量Path 并截取部分的工具 62 | private PathMeasure mMeasure; 63 | 64 | // 默认的动效周期 2s 65 | private int defaultDuration = 2000; 66 | 67 | // 控制各个过程的动画 68 | private ValueAnimator mStartingAnimator; 69 | private ValueAnimator mSearchingAnimator; 70 | private ValueAnimator mEndingAnimator; 71 | 72 | // 动画数值(用于控制动画状态,因为同一时间内只允许有一种状态出现,具体数值处理取决于当前状态) 73 | private float mAnimatorValue = 0; 74 | 75 | // 动效过程监听器 76 | private ValueAnimator.AnimatorUpdateListener mUpdateListener; 77 | private Animator.AnimatorListener mAnimatorListener; 78 | 79 | // 用于控制动画状态转换 80 | private Handler mAnimatorHandler; 81 | 82 | // 判断是否已经搜索结束 83 | private boolean isOver = false; 84 | 85 | private int count = 0; 86 | 87 | 88 | 89 | private void initPaint() { 90 | mPaint = new Paint(); 91 | mPaint.setStyle(Paint.Style.STROKE); 92 | mPaint.setColor(Color.WHITE); 93 | mPaint.setStrokeWidth(15); 94 | mPaint.setStrokeCap(Paint.Cap.ROUND); 95 | mPaint.setAntiAlias(true); 96 | } 97 | 98 | private void initPath() { 99 | path_srarch = new Path(); 100 | path_circle = new Path(); 101 | 102 | mMeasure = new PathMeasure(); 103 | 104 | // 注意,不要到360度,否则内部会自动优化,测量不能取到需要的数值 105 | RectF oval1 = new RectF(-50, -50, 50, 50); // 放大镜圆环 106 | path_srarch.addArc(oval1, 45, 359.9f); 107 | 108 | RectF oval2 = new RectF(-100, -100, 100, 100); // 外部圆环 109 | path_circle.addArc(oval2, 45, -359.9f); 110 | 111 | float[] pos = new float[2]; 112 | 113 | mMeasure.setPath(path_circle, false); // 放大镜把手的位置 114 | mMeasure.getPosTan(0, pos, null); 115 | 116 | path_srarch.lineTo(pos[0], pos[1]); // 放大镜把手 117 | 118 | Log.i("TAG", "pos=" + pos[0] + ":" + pos[1]); 119 | } 120 | 121 | private void initListener() { 122 | mUpdateListener = new ValueAnimator.AnimatorUpdateListener() { 123 | @Override 124 | public void onAnimationUpdate(ValueAnimator animation) { 125 | mAnimatorValue = (float) animation.getAnimatedValue(); 126 | invalidate(); 127 | } 128 | }; 129 | 130 | mAnimatorListener = new Animator.AnimatorListener() { 131 | @Override 132 | public void onAnimationStart(Animator animation) { 133 | 134 | } 135 | 136 | @Override 137 | public void onAnimationEnd(Animator animation) { 138 | // getHandle发消息通知动画状态更新 139 | mAnimatorHandler.sendEmptyMessage(0); 140 | } 141 | 142 | @Override 143 | public void onAnimationCancel(Animator animation) { 144 | 145 | } 146 | 147 | @Override 148 | public void onAnimationRepeat(Animator animation) { 149 | 150 | } 151 | }; 152 | } 153 | 154 | private void initHandler() { 155 | mAnimatorHandler = new Handler() { 156 | @Override 157 | public void handleMessage(Message msg) { 158 | super.handleMessage(msg); 159 | switch (mCurrentState) { 160 | case STARTING: 161 | // 从开始动画转换好搜索动画 162 | isOver = false; 163 | mCurrentState = State.SEARCHING; 164 | mStartingAnimator.removeAllListeners(); 165 | mSearchingAnimator.start(); 166 | break; 167 | case SEARCHING: 168 | if (!isOver) { // 如果搜索未结束 则继续执行搜索动画 169 | mSearchingAnimator.start(); 170 | Log.e("Update", "RESTART"); 171 | 172 | count++; 173 | if (count>2){ // count大于2则进入结束状态 174 | isOver = true; 175 | } 176 | } else { // 如果搜索已经结束 则进入结束动画 177 | mCurrentState = State.ENDING; 178 | mEndingAnimator.start(); 179 | } 180 | break; 181 | case ENDING: 182 | // 从结束动画转变为无状态 183 | mCurrentState = State.NONE; 184 | break; 185 | } 186 | } 187 | }; 188 | } 189 | 190 | private void initAnimator() { 191 | mStartingAnimator = ValueAnimator.ofFloat(0, 1).setDuration(defaultDuration); 192 | mSearchingAnimator = ValueAnimator.ofFloat(0, 1).setDuration(defaultDuration); 193 | mEndingAnimator = ValueAnimator.ofFloat(1, 0).setDuration(defaultDuration); 194 | 195 | mStartingAnimator.addUpdateListener(mUpdateListener); 196 | mSearchingAnimator.addUpdateListener(mUpdateListener); 197 | mEndingAnimator.addUpdateListener(mUpdateListener); 198 | 199 | mStartingAnimator.addListener(mAnimatorListener); 200 | mSearchingAnimator.addListener(mAnimatorListener); 201 | mEndingAnimator.addListener(mAnimatorListener); 202 | } 203 | 204 | 205 | @Override 206 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 207 | super.onSizeChanged(w, h, oldw, oldh); 208 | mViewWidth = w; 209 | mViewHeight = h; 210 | } 211 | 212 | @Override 213 | protected void onDraw(Canvas canvas) { 214 | super.onDraw(canvas); 215 | 216 | drawSearch(canvas); 217 | } 218 | 219 | private void drawSearch(Canvas canvas) { 220 | 221 | mPaint.setColor(Color.WHITE); 222 | 223 | 224 | canvas.translate(mViewWidth / 2, mViewHeight / 2); 225 | 226 | canvas.drawColor(Color.parseColor("#0082D7")); 227 | 228 | switch (mCurrentState) { 229 | case NONE: 230 | canvas.drawPath(path_srarch, mPaint); 231 | break; 232 | case STARTING: 233 | mMeasure.setPath(path_srarch, false); 234 | Path dst = new Path(); 235 | mMeasure.getSegment(mMeasure.getLength() * mAnimatorValue, mMeasure.getLength(), dst, true); 236 | canvas.drawPath(dst, mPaint); 237 | break; 238 | case SEARCHING: 239 | mMeasure.setPath(path_circle, false); 240 | Path dst2 = new Path(); 241 | float stop = mMeasure.getLength() * mAnimatorValue; 242 | float start = (float) (stop - ((0.5 - Math.abs(mAnimatorValue - 0.5)) * 200f)); 243 | mMeasure.getSegment(start, stop, dst2, true); 244 | canvas.drawPath(dst2, mPaint); 245 | break; 246 | case ENDING: 247 | mMeasure.setPath(path_srarch, false); 248 | Path dst3 = new Path(); 249 | mMeasure.getSegment(mMeasure.getLength() * mAnimatorValue, mMeasure.getLength(), dst3, true); 250 | canvas.drawPath(dst3, mPaint); 251 | break; 252 | } 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /CustomView/Advance/Code/SearchView.md: -------------------------------------------------------------------------------- 1 | ## SearchView 源代码 2 | 3 | [下载代码 (右键 -> 另存为)](https://raw.githubusercontent.com/GcsSloop/AndroidNote/master/CustomView/Advance/Code/SearchView.java) 4 | 5 | ``` java 6 | /** 7 | * Author: GcsSloop 8 | *

9 | * Created Date: 16/5/31 10 | *

11 | * Copyright (C) 2016 GcsSloop. 12 | *

13 | * GitHub: https://github.com/GcsSloop 14 | */ 15 | public class SearchView extends View { 16 | 17 | // 画笔 18 | private Paint mPaint; 19 | 20 | // View 宽高 21 | private int mViewWidth; 22 | private int mViewHeight; 23 | 24 | public SearchView(Context context) { 25 | this(context,null); 26 | } 27 | 28 | public SearchView(Context context, AttributeSet attrs) { 29 | super(context, attrs); 30 | initAll(); 31 | } 32 | 33 | public void initAll() { 34 | 35 | initPaint(); 36 | 37 | initPath(); 38 | 39 | initListener(); 40 | 41 | initHandler(); 42 | 43 | initAnimator(); 44 | 45 | // 进入开始动画 46 | mCurrentState = State.STARTING; 47 | mStartingAnimator.start(); 48 | 49 | } 50 | 51 | // 这个视图拥有的状态 52 | public static enum State { 53 | NONE, 54 | STARTING, 55 | SEARCHING, 56 | ENDING 57 | } 58 | 59 | // 当前的状态(非常重要) 60 | private State mCurrentState = State.NONE; 61 | 62 | // 放大镜与外部圆环 63 | private Path path_srarch; 64 | private Path path_circle; 65 | 66 | // 测量Path 并截取部分的工具 67 | private PathMeasure mMeasure; 68 | 69 | // 默认的动效周期 2s 70 | private int defaultDuration = 2000; 71 | 72 | // 控制各个过程的动画 73 | private ValueAnimator mStartingAnimator; 74 | private ValueAnimator mSearchingAnimator; 75 | private ValueAnimator mEndingAnimator; 76 | 77 | // 动画数值(用于控制动画状态,因为同一时间内只允许有一种状态出现,具体数值处理取决于当前状态) 78 | private float mAnimatorValue = 0; 79 | 80 | // 动效过程监听器 81 | private ValueAnimator.AnimatorUpdateListener mUpdateListener; 82 | private Animator.AnimatorListener mAnimatorListener; 83 | 84 | // 用于控制动画状态转换 85 | private Handler mAnimatorHandler; 86 | 87 | // 判断是否已经搜索结束 88 | private boolean isOver = false; 89 | 90 | private int count = 0; 91 | 92 | 93 | 94 | private void initPaint() { 95 | mPaint = new Paint(); 96 | mPaint.setStyle(Paint.Style.STROKE); 97 | mPaint.setColor(Color.WHITE); 98 | mPaint.setStrokeWidth(15); 99 | mPaint.setStrokeCap(Paint.Cap.ROUND); 100 | mPaint.setAntiAlias(true); 101 | } 102 | 103 | private void initPath() { 104 | path_srarch = new Path(); 105 | path_circle = new Path(); 106 | 107 | mMeasure = new PathMeasure(); 108 | 109 | // 注意,不要到360度,否则内部会自动优化,测量不能取到需要的数值 110 | RectF oval1 = new RectF(-50, -50, 50, 50); // 放大镜圆环 111 | path_srarch.addArc(oval1, 45, 359.9f); 112 | 113 | RectF oval2 = new RectF(-100, -100, 100, 100); // 外部圆环 114 | path_circle.addArc(oval2, 45, -359.9f); 115 | 116 | float[] pos = new float[2]; 117 | 118 | mMeasure.setPath(path_circle, false); // 放大镜把手的位置 119 | mMeasure.getPosTan(0, pos, null); 120 | 121 | path_srarch.lineTo(pos[0], pos[1]); // 放大镜把手 122 | 123 | Log.i("TAG", "pos=" + pos[0] + ":" + pos[1]); 124 | } 125 | 126 | private void initListener() { 127 | mUpdateListener = new ValueAnimator.AnimatorUpdateListener() { 128 | @Override 129 | public void onAnimationUpdate(ValueAnimator animation) { 130 | mAnimatorValue = (float) animation.getAnimatedValue(); 131 | invalidate(); 132 | } 133 | }; 134 | 135 | mAnimatorListener = new Animator.AnimatorListener() { 136 | @Override 137 | public void onAnimationStart(Animator animation) { 138 | 139 | } 140 | 141 | @Override 142 | public void onAnimationEnd(Animator animation) { 143 | // getHandle发消息通知动画状态更新 144 | mAnimatorHandler.sendEmptyMessage(0); 145 | } 146 | 147 | @Override 148 | public void onAnimationCancel(Animator animation) { 149 | 150 | } 151 | 152 | @Override 153 | public void onAnimationRepeat(Animator animation) { 154 | 155 | } 156 | }; 157 | } 158 | 159 | private void initHandler() { 160 | mAnimatorHandler = new Handler() { 161 | @Override 162 | public void handleMessage(Message msg) { 163 | super.handleMessage(msg); 164 | switch (mCurrentState) { 165 | case STARTING: 166 | // 从开始动画转换好搜索动画 167 | isOver = false; 168 | mCurrentState = State.SEARCHING; 169 | mStartingAnimator.removeAllListeners(); 170 | mSearchingAnimator.start(); 171 | break; 172 | case SEARCHING: 173 | if (!isOver) { // 如果搜索未结束 则继续执行搜索动画 174 | mSearchingAnimator.start(); 175 | Log.e("Update", "RESTART"); 176 | 177 | count++; 178 | if (count>2){ // count大于2则进入结束状态 179 | isOver = true; 180 | } 181 | } else { // 如果搜索已经结束 则进入结束动画 182 | mCurrentState = State.ENDING; 183 | mEndingAnimator.start(); 184 | } 185 | break; 186 | case ENDING: 187 | // 从结束动画转变为无状态 188 | mCurrentState = State.NONE; 189 | break; 190 | } 191 | } 192 | }; 193 | } 194 | 195 | private void initAnimator() { 196 | mStartingAnimator = ValueAnimator.ofFloat(0, 1).setDuration(defaultDuration); 197 | mSearchingAnimator = ValueAnimator.ofFloat(0, 1).setDuration(defaultDuration); 198 | mEndingAnimator = ValueAnimator.ofFloat(1, 0).setDuration(defaultDuration); 199 | 200 | mStartingAnimator.addUpdateListener(mUpdateListener); 201 | mSearchingAnimator.addUpdateListener(mUpdateListener); 202 | mEndingAnimator.addUpdateListener(mUpdateListener); 203 | 204 | mStartingAnimator.addListener(mAnimatorListener); 205 | mSearchingAnimator.addListener(mAnimatorListener); 206 | mEndingAnimator.addListener(mAnimatorListener); 207 | } 208 | 209 | 210 | @Override 211 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 212 | super.onSizeChanged(w, h, oldw, oldh); 213 | mViewWidth = w; 214 | mViewHeight = h; 215 | } 216 | 217 | @Override 218 | protected void onDraw(Canvas canvas) { 219 | super.onDraw(canvas); 220 | 221 | drawSearch(canvas); 222 | } 223 | 224 | private void drawSearch(Canvas canvas) { 225 | 226 | mPaint.setColor(Color.WHITE); 227 | 228 | 229 | canvas.translate(mViewWidth / 2, mViewHeight / 2); 230 | 231 | canvas.drawColor(Color.parseColor("#0082D7")); 232 | 233 | switch (mCurrentState) { 234 | case NONE: 235 | canvas.drawPath(path_srarch, mPaint); 236 | break; 237 | case STARTING: 238 | mMeasure.setPath(path_srarch, false); 239 | Path dst = new Path(); 240 | mMeasure.getSegment(mMeasure.getLength() * mAnimatorValue, mMeasure.getLength(), dst, true); 241 | canvas.drawPath(dst, mPaint); 242 | break; 243 | case SEARCHING: 244 | mMeasure.setPath(path_circle, false); 245 | Path dst2 = new Path(); 246 | float stop = mMeasure.getLength() * mAnimatorValue; 247 | float start = (float) (stop - ((0.5 - Math.abs(mAnimatorValue - 0.5)) * 200f)); 248 | mMeasure.getSegment(start, stop, dst2, true); 249 | canvas.drawPath(dst2, mPaint); 250 | break; 251 | case ENDING: 252 | mMeasure.setPath(path_srarch, false); 253 | Path dst3 = new Path(); 254 | mMeasure.getSegment(mMeasure.getLength() * mAnimatorValue, mMeasure.getLength(), dst3, true); 255 | canvas.drawPath(dst3, mPaint); 256 | break; 257 | } 258 | } 259 | } 260 | 261 | ``` 262 | -------------------------------------------------------------------------------- /CustomView/Advance/Code/SetPolyToPoly.java: -------------------------------------------------------------------------------- 1 | package com.gcssloop.canvas; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.BitmapFactory; 6 | import android.graphics.Canvas; 7 | import android.graphics.Matrix; 8 | import android.graphics.Paint; 9 | import android.util.AttributeSet; 10 | import android.view.MotionEvent; 11 | import android.view.View; 12 | 13 | import com.gcssloop.view.utils.CanvasAidUtils; 14 | 15 | /** 16 | * Author: GcsSloop 17 | *

18 | * Created Date: 16/8/26 19 | *

20 | * Copyright (C) 2016 GcsSloop. 21 | *

22 | * GitHub: https://github.com/GcsSloop 23 | */ 24 | public class SetPolyToPoly extends View{ 25 | private static final String TAG = "SetPolyToPoly"; 26 | 27 | private int testPoint = 0; 28 | private int triggerRadius = 180; // 触发半径为180px 29 | 30 | private Bitmap mBitmap; // 要绘制的图片 31 | private Matrix mPolyMatrix; // 测试setPolyToPoly用的Matrix 32 | 33 | private float[] src = new float[8]; 34 | private float[] dst = new float[8]; 35 | 36 | private Paint pointPaint; 37 | 38 | public SetPolyToPoly(Context context) { 39 | this(context, null); 40 | } 41 | 42 | public SetPolyToPoly(Context context, AttributeSet attrs) { 43 | this(context, attrs, 0); 44 | } 45 | 46 | public SetPolyToPoly(Context context, AttributeSet attrs, int defStyleAttr) { 47 | super(context, attrs, defStyleAttr); 48 | initBitmapAndMatrix(); 49 | } 50 | 51 | private void initBitmapAndMatrix() { 52 | mBitmap = BitmapFactory.decodeResource(getResources(), 53 | R.drawable.poly_test2); 54 | 55 | float[] temp = {0, 0, // 左上 56 | mBitmap.getWidth(), 0, // 右上 57 | mBitmap.getWidth(), mBitmap.getHeight(), // 右下 58 | 0, mBitmap.getHeight()}; // 左下 59 | src = temp.clone(); 60 | dst = temp.clone(); 61 | 62 | pointPaint = new Paint(); 63 | pointPaint.setAntiAlias(true); 64 | pointPaint.setStrokeWidth(50); 65 | pointPaint.setColor(0xffd19165); 66 | pointPaint.setStrokeCap(Paint.Cap.ROUND); 67 | 68 | mPolyMatrix = new Matrix(); 69 | mPolyMatrix.setPolyToPoly(src, 0, src, 0, 4); 70 | } 71 | 72 | @Override 73 | public boolean onTouchEvent(MotionEvent event) { 74 | 75 | switch (event.getAction()){ 76 | case MotionEvent.ACTION_MOVE: 77 | float tempX = event.getX(); 78 | float tempY = event.getY(); 79 | 80 | // 根据触控位置改变dst 81 | for (int i=0; i 4 || testPoint < 0 ? 4 : testPoint; 125 | dst = src.clone(); 126 | resetPolyMatrix(this.testPoint); 127 | invalidate(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /CustomView/Advance/Code/SetPolyToPoly.md: -------------------------------------------------------------------------------- 1 | # Matrix setPolyTOPoly 测试代码 2 | 3 | ## SetPolyToPoly.java 4 | 5 | [下载代码 (右键 -> 另存为)](https://raw.githubusercontent.com/GcsSloop/AndroidNote/master/CustomView/Advance/Code/SetPolyToPoly.java) 6 | 7 | ```java 8 | public class SetPolyToPoly extends View{ 9 | private static final String TAG = "SetPolyToPoly"; 10 | 11 | private int testPoint = 0; 12 | private int triggerRadius = 180; // 触发半径为180px 13 | 14 | private Bitmap mBitmap; // 要绘制的图片 15 | private Matrix mPolyMatrix; // 测试setPolyToPoly用的Matrix 16 | 17 | private float[] src = new float[8]; 18 | private float[] dst = new float[8]; 19 | 20 | private Paint pointPaint; 21 | 22 | public SetPolyToPoly(Context context) { 23 | this(context, null); 24 | } 25 | 26 | public SetPolyToPoly(Context context, AttributeSet attrs) { 27 | this(context, attrs, 0); 28 | } 29 | 30 | public SetPolyToPoly(Context context, AttributeSet attrs, int defStyleAttr) { 31 | super(context, attrs, defStyleAttr); 32 | initBitmapAndMatrix(); 33 | } 34 | 35 | private void initBitmapAndMatrix() { 36 | mBitmap = BitmapFactory.decodeResource(getResources(), 37 | R.drawable.poly_test2); 38 | 39 | float[] temp = {0, 0, // 左上 40 | mBitmap.getWidth(), 0, // 右上 41 | mBitmap.getWidth(), mBitmap.getHeight(), // 右下 42 | 0, mBitmap.getHeight()}; // 左下 43 | src = temp.clone(); 44 | dst = temp.clone(); 45 | 46 | pointPaint = new Paint(); 47 | pointPaint.setAntiAlias(true); 48 | pointPaint.setStrokeWidth(50); 49 | pointPaint.setColor(0xffd19165); 50 | pointPaint.setStrokeCap(Paint.Cap.ROUND); 51 | 52 | mPolyMatrix = new Matrix(); 53 | mPolyMatrix.setPolyToPoly(src, 0, src, 0, 4); 54 | } 55 | 56 | @Override 57 | public boolean onTouchEvent(MotionEvent event) { 58 | 59 | switch (event.getAction()){ 60 | case MotionEvent.ACTION_MOVE: 61 | float tempX = event.getX(); 62 | float tempY = event.getY(); 63 | 64 | // 根据触控位置改变dst 65 | for (int i=0; i 4 || testPoint < 0 ? 4 : testPoint; 109 | dst = src.clone(); 110 | resetPolyMatrix(this.testPoint); 111 | invalidate(); 112 | } 113 | } 114 | ``` 115 | 116 | ***** 117 | 118 | ## 布局文件 119 | 120 | ``` xml 121 | 122 | 129 | 130 | 135 | 136 | 143 | 144 | 150 | 156 | 161 | 166 | 171 | 176 | 177 | 178 | 179 | 180 | ``` 181 | 182 | ***** 183 | 184 | ## MainActivity 185 | 186 | ``` java 187 | setContentView(R.layout.activity_main); 188 | 189 | final SetPolyToPoly poly = (SetPolyToPoly) findViewById(R.id.poly); 190 | 191 | RadioGroup group = (RadioGroup) findViewById(R.id.group); 192 | assert group != null; 193 | group.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { 194 | @Override 195 | public void onCheckedChanged(RadioGroup group, int checkedId) { 196 | switch (group.getCheckedRadioButtonId()){ 197 | case R.id.point0: poly.setTestPoint(0); break; 198 | case R.id.point1: poly.setTestPoint(1); break; 199 | case R.id.point2: poly.setTestPoint(2); break; 200 | case R.id.point3: poly.setTestPoint(3); break; 201 | case R.id.point4: poly.setTestPoint(4); break; 202 | } 203 | } 204 | }); 205 | ``` 206 | 207 | ***** 208 | 209 | ## 依赖的库 210 | 211 | 绘制坐标系部分依赖了一个开源库。 212 | 213 | * [ViewSupport](https://github.com/GcsSloop/ViewSupport ) 214 | 215 | 216 | -------------------------------------------------------------------------------- /CustomView/Advance/Res/Checkmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GcsSloop/AndroidNote/21ba33e3d40c7f462d3c7c90322f4318060fe3ff/CustomView/Advance/Res/Checkmark.png -------------------------------------------------------------------------------- /CustomView/Advance/Res/poly_test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GcsSloop/AndroidNote/21ba33e3d40c7f462d3c7c90322f4318060fe3ff/CustomView/Advance/Res/poly_test.jpg -------------------------------------------------------------------------------- /CustomView/Advance/[01]CustomViewProcess.md: -------------------------------------------------------------------------------- 1 | # 自定义View分类与流程 2 | 3 | ### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop) 4 | ### [【本系列相关文章】](https://github.com/GcsSloop/AndroidNote/tree/master/CustomView/README.md) 5 | 6 | 经历过前面三篇啰啰嗦嗦的基础篇之后,终于到了进阶篇,正式进入解析自定义View的阶段。 7 | 8 | ## 前言 9 | **本章节为什么要叫进阶篇?(虽然讲的是基础内容),因为从本篇开始,将会逐渐揭开自定义View的神秘面纱,每一篇都将比上一篇内容更加深入,利用所学的知识能够制作更加炫酷自定义View,就像在台阶上一样,每一篇都更上一层,~~帮助大家一步步走向人生巅峰,出任CEO,迎娶白富美。~~ 误,是帮助大家更加了解那些炫酷的自定义View是如何制作的,达到举一反三的效果。** 10 | 11 | 自定义View绘制流程函数调用链(简化版) 12 | 13 | ![](http://ww4.sinaimg.cn/large/005Xtdi2jw1f638wreu74j30fc0heaay.jpg) 14 | 15 | 16 | ## 一.自定义View分类 17 | 18 | **我将自定义View分为了两类(sloop个人分类法,非官方):** 19 | 20 | ### 1.自定义ViewGroup 21 | 22 | **自定义ViewGroup一般是利用现有的组件根据特定的布局方式来组成新的组件,大多继承自ViewGroup或各种Layout,包含有子View。** 23 | 24 | > 例如:应用底部导航条中的条目,一般都是上面图标(ImageView),下面文字(TextView),那么这两个就可以用自定义ViewGroup组合成为一个Veiw,提供两个属性分别用来设置文字和图片,使用起来会更加方便。 25 | 26 | ### 2.自定义View 27 | 28 | **在没有现成的View,需要自己实现的时候,就使用自定义View,一般继承自View,SurfaceView或其他的View,不包含子View。** 29 | 30 | > 例如:制作一个支持自动加载网络图片的ImageView,制作图表等。 31 | 32 | **PS: 自定义View在大多数情况下都有替代方案,利用图片或者组合动画来实现,但是使用后者可能会面临内存耗费过大,制作麻烦等诸多问题。** 33 | 34 | ******* 35 | 36 | ## 二.几个重要的函数 37 | 38 | ### 1.构造函数 39 | 40 | 构造函数是View的入口,可以用于**初始化一些的内容,和获取自定义属性**。 41 | 42 | View的构造函数有四种重载分别如下: 43 | ``` java 44 | public void SloopView(Context context) {} 45 | public void SloopView(Context context, AttributeSet attrs) {} 46 | public void SloopView(Context context, AttributeSet attrs, int defStyleAttr) {} 47 | public void SloopView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {} 48 | ``` 49 | 可以看出,关于View构造函数的参数有多有少,先排除几个不常用的,留下常用的再研究。 50 | 51 | **有四个参数的构造函数在API21的时候才添加上,暂不考虑。** 52 | 53 | 有三个参数的构造函数中第三个参数是默认的Style,这里的默认的Style是指它在当前Application或Activity所用的Theme中的默认Style,且只有在明确调用的时候才会生效,以系统中的ImageButton为例说明: 54 | ``` java 55 | public ImageButton(Context context, AttributeSet attrs) { 56 | //调用了三个参数的构造函数,明确指定第三个参数 57 | this(context, attrs, com.android.internal.R.attr.imageButtonStyle); 58 | } 59 | 60 | public ImageButton(Context context, AttributeSet attrs, int defStyleAttr) { 61 | //此处调了四个参数的构造函数,无视即可 62 | this(context, attrs, defStyleAttr, 0); 63 | } 64 | ``` 65 | **注意:即使你在View中使用了Style这个属性也不会调用三个参数的构造函数,所调用的依旧是两个参数的构造函数。** 66 | 67 | **由于三个参数的构造函数第三个参数一般不用,暂不考虑,第三个参数的具体用法会在以后用到的时候详细介绍。** 68 | 69 | 排除了两个之后,只剩下一个参数和两个参数的构造函数,他们的详情如下: 70 | ``` java 71 | //一般在直接New一个View的时候调用。 72 | public void SloopView(Context context) {} 73 | 74 | //一般在layout文件中使用的时候会调用,关于它的所有属性(包括自定义属性)都会包含在attrs中传递进来。 75 | public void SloopView(Context context, AttributeSet attrs) {} 76 | ``` 77 | **以下方法调用的是一个参数的构造函数:** 78 | ``` java 79 | //在Avtivity中 80 | SloopView view = new SloopView(this); 81 | ``` 82 | **以下方法调用的是两个参数的构造函数:** 83 | ``` xml 84 | //在layout文件中 - 格式为: 包名.View名 85 | 88 | ``` 89 | 关于构造函数先讲这么多,关于如何自定义属性和使用attrs中的内容,在后面会详细讲解,目前只需要知道这两个构造函数在何时调用即可。 90 | 91 | ======== 92 | 93 | ### 2.测量View大小(onMeasure) 94 | 95 | **Q: 为什么要测量View大小?** 96 | 97 | **A: View的大小不仅由自身所决定,同时也会受到父控件的影响,为了我们的控件能更好的适应各种情况,一般会自己进行测量。** 98 | 99 | 测量View大小使用的是onMeasure函数,我们可以从onMeasure的两个参数中取出宽高的相关数据: 100 | ``` java 101 | @Override 102 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 103 | int widthsize = MeasureSpec.getSize(widthMeasureSpec); //取出宽度的确切数值 104 | int widthmode = MeasureSpec.getMode(widthMeasureSpec); //取出宽度的测量模式 105 | 106 | int heightsize = MeasureSpec.getSize(heightMeasureSpec); //取出高度的确切数值 107 | int heightmode = MeasureSpec.getMode(heightMeasureSpec); //取出高度的测量模式 108 | } 109 | ``` 110 | 从上面可以看出 onMeasure 函数中有 widthMeasureSpec 和 heightMeasureSpec 这两个 int 类型的参数, 毫无疑问他们是和宽高相关的, **但它们其实不是宽和高, 而是由宽、高和各自方向上对应的测量模式来合成的一个值:** 111 | 112 | **测量模式一共有三种, 被定义在 Android 中的 View 类的一个内部类View.MeasureSpec中:** 113 | 114 | | 模式 | 二进制数值 | 描述 | 115 | | ----------- | :---: | -------------------------------------- | 116 | | UNSPECIFIED | 00 | 默认值,父控件没有给子view任何限制,子View可以设置为任意大小。 | 117 | | EXACTLY | 01 | 表示父控件已经确切的指定了子View的大小。 | 118 | | AT_MOST | 10 | 表示子View具体大小没有尺寸限制,但是存在上限,上限一般为父View大小。 | 119 | 120 | **在int类型的32位二进制位中,31-30这两位表示测量模式,29~0这三十位表示宽和高的实际值,实际上如下:** 121 | 122 | 以数值1080(二进制为: 1111011000)为例(其中模式和实际数值是连在一起的,为了展示我将他们分开了): 123 | 124 | | 模式名称 | 模式数值 | 实际数值 | 125 | | ----------- | ---: | ------------------------------ | 126 | | UNSPECIFIED | 00 | 000000000000000000001111011000 | 127 | | EXACTLY | 01 | 000000000000000000001111011000 | 128 | | AT_MOST | 10 | 000000000000000000001111011000 | 129 | 130 | **PS: 实际上关于上面的东西了解即可,在实际运用之中只需要记住有三种模式,用 MeasureSpec 的 getSize是获取数值, getMode是获取模式即可。** 131 | 132 | #### 注意: 133 | **如果对View的宽高进行修改了,不要调用*super.onMeasure(widthMeasureSpec,heightMeasureSpec);*要调用*setMeasuredDimension(widthsize,heightsize);* 这个函数。** 134 | 135 | ====== 136 | 137 | ### 3.确定View大小(onSizeChanged) 138 | 这个函数在视图大小发生改变时调用。 139 | 140 | **Q: 在测量完View并使用setMeasuredDimension函数之后View的大小基本上已经确定了,那么为什么还要再次确定View的大小呢?** 141 | 142 | **A: 这是因为View的大小不仅由View本身控制,而且受父控件的影响,所以我们在确定View大小的时候最好使用系统提供的onSizeChanged回调函数。** 143 | 144 | onSizeChanged如下: 145 | ``` java 146 | @Override 147 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 148 | super.onSizeChanged(w, h, oldw, oldh); 149 | } 150 | ``` 151 | 可以看出,它又四个参数,分别为 宽度,高度,上一次宽度,上一次高度。 152 | 153 | 这个函数比较简单,我们只需关注 宽度(w), 高度(h) 即可,这两个参数就是View最终的大小。 154 | 155 | ========= 156 | 157 | ### 4.确定子View布局位置(onLayout) 158 | 159 | **确定布局的函数是onLayout,它用于确定子View的位置,在自定义ViewGroup中会用到,他调用的是子View的layout函数。** 160 | 161 | 在自定义ViewGroup中,onLayout一般是循环取出子View,然后经过计算得出各个子View位置的坐标值,然后用以下函数设置子View位置。 162 | 163 | ``` java 164 | child.layout(l, t, r, b); 165 | ``` 166 | 四个参数分别为: 167 | 168 | | 名称 | 说明 | 对应的函数 | 169 | | ---- | ----------------- | ------------ | 170 | | l | View左侧距父View左侧的距离 | getLeft(); | 171 | | t | View顶部距父View顶部的距离 | getTop(); | 172 | | r | View右侧距父View左侧的距离 | getRight(); | 173 | | b | View底部距父View顶部的距离 | getBottom(); | 174 | 175 | 具体可以参考 [坐标系](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Base/%5B01%5DCoordinateSystem.md) 这篇文章。 176 | 177 | ![](http://ww2.sinaimg.cn/large/005Xtdi2gw1f1qzqwvkkbj308c0dwgm9.jpg) 178 | 179 | PS:关于onLayout这个函数在讲解自定义ViewGroup的时候会详细讲解。 180 | 181 | 182 | ======== 183 | 184 | ### 5.绘制内容(onDraw) 185 | 186 | onDraw是实际绘制的部分,也就是我们真正关心的部分,使用的是Canvas绘图。 187 | ``` java 188 | @Override 189 | protected void onDraw(Canvas canvas) { 190 | super.onDraw(canvas); 191 | } 192 | ``` 193 | 关于Canvas绘图是本章节的重点,会分几篇文章进行详细讲解,敬请期待OwO。 194 | 195 | ====== 196 | 197 | ### 6.对外提供操作方法和监听回调 198 | 自定义完View之后,一般会对外暴露一些接口,用于控制View的状态等,或者监听View的变化. 199 | 200 | 本内容会在后续文章中以实例的方式进讲解。 201 | 202 | ************ 203 | 204 | ## 三.重点知识梳理 205 | 206 | ### 自定义View分类 207 | 208 | > PS :实际上ViewGroup是View的一个子类。 209 | 210 | | 类别 | 继承自 | 特点 | 211 | | --------- | ------------------- | ------- | 212 | | View | View SurfaceView 等 | 不含子View | 213 | | ViewGroup | ViewGroup xxLayout等 | 包含子View | 214 | 215 | ### 自定义View流程: 216 | | 步骤 | 关键字 | 作用 | 217 | | ---- | ------------- | ---------------------------- | 218 | | 1 | 构造函数 | View初始化 | 219 | | 2 | onMeasure | 测量View大小 | 220 | | 3 | onSizeChanged | 确定View大小 | 221 | | 4 | onLayout | 确定子View布局(自定义View包含子View时有用) | 222 | | 5 | onDraw | 实际绘制内容 | 223 | | 6 | 提供接口 | 控制View或监听View某些状态。 | 224 | 225 | 226 | 227 | ## About Me 228 | 229 | ### 作者微博: @GcsSloop 230 | 231 | 232 | 233 | 234 | ## 参考资料: 235 | [View](http://developer.android.com/reference/android/view/View.html)
236 | [ViewGroup](http://developer.android.com/reference/android/view/ViewGroup.html)
237 | [View.MeasureSpec](http://developer.android.com/reference/android/view/View.MeasureSpec.html)
238 | [onMeasure,MeasureSpec源码 流程 思路详解](http://blog.csdn.net/a396901990/article/details/36475213)
239 |
240 | [Android中自定义样式与View的构造函数中的第三个参数defStyle的意义](http://www.cnblogs.com/angeldevil/p/3479431.html)
241 | [android view构造函数研究](http://blog.csdn.net/z103594643/article/details/6755017)
242 | [Android View构造方法第三参数使用方法详解](http://blog.csdn.net/mybeta/article/details/39993449)
243 |
244 | [Android 自定义View onMeasure方法的实现](http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2014/1102/1891.html)
245 | [Android API指南(二)自定义控件02之 onMeasure](http://wangkuiwu.github.io/2014/06/20/View-OnMeasure/)
246 | [Android中View的绘制过程 onMeasure方法简述](http://www.cnblogs.com/mengdd/p/3332882.html)
247 | 248 |
249 |
250 | -------------------------------------------------------------------------------- /CustomView/Advance/[07]Path_Over.md: -------------------------------------------------------------------------------- 1 | # Path之完结篇(伪) 2 | 3 | ### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop) 4 | ### [【本系列相关文章】](https://github.com/GcsSloop/AndroidNote/tree/master/CustomView/README.md) 5 | 6 | 经历过前两篇 [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的收尾篇,本篇结束后Path的大部分相关方法都已经讲解完了,但Path还有一些更有意思的玩法,应该会在后续的文章中出现吧,嗯,应该会的ˊ_>ˋ 7 | 8 | ****** 9 | 10 | ## 一.Path常用方法表 11 | > 为了兼容性(_偷懒_) 本表格中去除了在API21(即安卓版本5.0)以上才添加的方法。忍不住吐槽一下,为啥看起来有些顺手就能写的重载方法要等到API21才添加上啊。宝宝此刻内心也是崩溃的。 12 | 13 | | 作用 | 相关方法 | 备注 | 14 | | ----------- | ---------------------------------------- | ---------------------------------------- | 15 | | 移动起点 | moveTo | 移动下一次操作的起点位置 | 16 | | 设置终点 | setLastPoint | 重置当前path中最后一个点位置,如果在绘制之前调用,效果和moveTo相同 | 17 | | 连接直线 | lineTo | 添加上一个点到当前点之间的直线到Path | 18 | | 闭合路径 | close | 连接第一个点连接到最后一个点,形成一个闭合区域 | 19 | | 添加内容 | addRect, addRoundRect, addOval, addCircle, addPath, addArc, arcTo | 添加(矩形, 圆角矩形, 椭圆, 圆, 路径, 圆弧) 到当前Path (注意addArc和arcTo的区别) | 20 | | 是否为空 | isEmpty | 判断Path是否为空 | 21 | | 是否为矩形 | isRect | 判断path是否是一个矩形 | 22 | | 替换路径 | set | 用新的路径替换到当前路径所有内容 | 23 | | 偏移路径 | offset | 对当前路径之前的操作进行偏移(不会影响之后的操作) | 24 | | 贝塞尔曲线 | quadTo, cubicTo | 分别为二次和三次贝塞尔曲线的方法 | 25 | | rXxx方法 | rMoveTo, rLineTo, rQuadTo, rCubicTo | **不带r的方法是基于原点的坐标系(偏移量), rXxx方法是基于当前点坐标系(偏移量)** | 26 | | 填充模式 | setFillType, getFillType, isInverseFillType, toggleInverseFillType | 设置,获取,判断和切换填充模式 | 27 | | 提示方法 | incReserve | 提示Path还有多少个点等待加入**(这个方法貌似会让Path优化存储结构)** | 28 | | 布尔操作(API19) | op | 对两个Path进行布尔运算(即取交集、并集等操作) | 29 | | 计算边界 | computeBounds | 计算Path的边界 | 30 | | 重置路径 | reset, rewind | 清除Path中的内容
**reset不保留内部数据结构,但会保留FillType.**
**rewind会保留内部的数据结构,但不保留FillType** | 31 | | 矩阵操作 | transform | 矩阵变换 | 32 | 33 | 34 | ## 二、Path方法详解 35 | 36 | ### rXxx方法 37 | 38 | 此类方法可以看到和前面的一些方法看起来很像,只是在前面多了一个r,那么这个rXxx和前面的一些方法有什么区别呢? 39 | 40 | > **rXxx方法的坐标使用的是相对位置(基于当前点的位移),而之前方法的坐标是绝对位置(基于当前坐标系的坐标)。** 41 | 42 | **举个例子:** 43 | 44 | ``` java 45 | Path path = new Path(); 46 | 47 | path.moveTo(100,100); 48 | path.lineTo(100,200); 49 | 50 | canvas.drawPath(path,mDeafultPaint); 51 | ``` 52 | ![](http://ww2.sinaimg.cn/large/005Xtdi2gw1f43livlg7ej308c0etmx4.jpg) 53 | 54 | 在这个例子中,先移动点到坐标(100,100)处,之后再连接 _点(100,100)_ 到 _(100,200)_ 之间点直线,非常简单,画出来就是一条竖直的线,那接下来看下一个例子: 55 | 56 | ``` java 57 | Path path = new Path(); 58 | 59 | path.moveTo(100,100); 60 | path.rLineTo(100,200); 61 | 62 | canvas.drawPath(path,mDeafultPaint); 63 | ``` 64 | ![](http://ww4.sinaimg.cn/large/005Xtdi2gw1f43lj76wckj308c0etaa1.jpg) 65 | 66 | 这个例子中,将 lineTo 换成了 rLineTo 可以看到在屏幕上原本是竖直的线变成了倾斜的线。这是因为最终我们连接的是 _(100,100)_ 和 _(200, 300)_ 之间的线段。 67 | 68 | 在使用rLineTo之前,当前点的位置在 (100,100) , 使用了 rLineTo(100,200) 之后,下一个点的位置是在当前点的基础上加上偏移量得到的,即 (100+100, 100+200) 这个位置,故最终结果如上所示。 69 | 70 | **PS: 此处仅以 rLineTo 为例,只要理解 “绝对坐标” 和 “相对坐标” 的区别,其他方法类比即可。** 71 | 72 | ### 填充模式 73 | 74 | 我们在之前的文章中了解到,Paint有三种样式,“描边” “填充” 以及 “描边加填充”,我们这里所了解到就是在Paint设置为后两种样式时**不同的填充模式对图形渲染效果的影响**。 75 | 76 | **我们要给一个图形内部填充颜色,首先需要分清哪一部分是外部,哪一部分是内部,机器不像我们人那么聪明,机器是如何判断内外呢?** 77 | 78 | 机器判断图形内外,一般有以下两种方法: 79 | 80 | > PS:此处所有的图形均为封闭图形,不包括图形不封闭这种情况。 81 | 82 | | 方法 | 判定条件 | 解释 | 83 | | ------- | --------------------- | ---------------------------------------- | 84 | | 奇偶规则 | 奇数表示在图形内,偶数表示在图形外 | 从任意位置p作一条射线, 若与该射线相交的图形边的数目为奇数,则p是图形内部点,否则是外部点。 | 85 | | 非零环绕数规则 | 若环绕数为0表示在图形外,非零表示在图形内 | 首先使图形的边变为矢量。将环绕数初始化为零。再从任意位置p作一条射线。当从p点沿射线方向移动时,对在每个方向上穿过射线的边计数,每当图形的边从右到左穿过射线时,环绕数加1,从左到右时,环绕数减1。处理完图形的所有相关边之后,若环绕数为非零,则p为内部点,否则,p是外部点。 | 86 | 87 | 接下来我们先了解一下两种判断方法是如何工作的。 88 | 89 | #### 奇偶规则(Even-Odd Rule) 90 | 91 | 这一个比较简单,也容易理解,直接用一个简单示例来说明。 92 | 93 | ![](http://ww4.sinaimg.cn/large/005Xtdi2jw1f417d963qxj308c0dwq33.jpg) 94 | 95 | 在上图中有一个四边形,我们选取了三个点来判断这些点是否在图形内部。 96 | 97 | > 98 | >P1: 从P1发出一条射线,发现图形与该射线相交边数为0,偶数,故P1点在图形外部。
99 | >P2: 从P2发出一条射线,发现图形与该射线相交边数为1,奇数,故P2点在图形内部。
100 | >P3: 从P3发出一条射线,发现图形与该射线相交边数为2,偶数,故P3点在图形外部。
101 | 102 | 103 | #### 非零环绕数规则(Non-Zero Winding Number Rule) 104 | 105 | 非零环绕数规则相对来说比较难以理解一点。 106 | 107 | 我们在之前的文章 [Path之基本操作](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B05%5DPath_Basic.md) 中我们了解到,在给Path中添加图形时需要指定图形的添加方式,是用顺时针还是逆时针,另外我们不论是使用lineTo,quadTo,cubicTo还是其他连接线的方法,都是从一个点连接到另一个点,换言之,**Path中任何线段都是有方向性的**,这也是使用非零环绕数规则的基础。 108 | 109 | 我们依旧用一个简单的例子来说明非零环绕数规则的用法: 110 | 111 | > **PS: 注意图形中线段的方向性!** 112 | 113 | ![](http://ww2.sinaimg.cn/large/005Xtdi2jw1f42368af2jj308c0dwt8z.jpg) 114 | 115 | > 116 | >P1: 从P1点发出一条射线,沿射线方向移动,并没有与边相交点部分,环绕数为0,故P1在图形外边。
117 | >P2: 从P2点发出一条射线,沿射线方向移动,与图形点左侧边相交,该边从左到右穿过射线,环绕数-1,最终环绕数为-1,故P2在图形内部。
118 | >P3: 从P3点发出一条射线,沿射线方向移动,在第一个交点处,底边从右到左穿过射线,环绕数+1,在第二个交点处,右侧边从左到右穿过射线,环绕数-1,最终环绕数为0,故P3在图形外部。
119 | 120 | 通常,这两种方法的判断结果是相同的,但也存在两种方法判断结果不同的情况,如下面这种情况: 121 | 122 | > 注意图形线段的方向,就不详细解释了,用上面的方法进行判断即可。 123 | 124 | ![](http://ww1.sinaimg.cn/large/005Xtdi2gw1f42cvwvlr7j308c0dwgm8.jpg) 125 | 126 | #### 自相交图形 127 | 128 | **自相交图形定义:多边形在平面内除顶点外还有其他公共点。** 129 | 130 | 简单的提一下自相交图形,了解概念即可,下图就是一个简单的自相交图形: 131 | 132 | ![](http://ww3.sinaimg.cn/large/005Xtdi2gw1f42dp5drq4j308c0dw74b.jpg) 133 | 134 | #### Android中的填充模式 135 | 136 | Android中的填充模式有四种,是封装在Path中的一个枚举。 137 | 138 | | 模式 | 简介 | 139 | | ---------------- | -------- | 140 | | EVEN_ODD | 奇偶规则 | 141 | | INVERSE_EVEN_ODD | 反奇偶规则 | 142 | | WINDING | 非零环绕数规则 | 143 | | INVERSE_WINDING | 反非零环绕数规则 | 144 | 145 | 我们可以看到上面有四种模式,分成两对,例如 "奇偶规则" 与 "反奇偶规则" 是一对,它们之间有什么关系呢? 146 | 147 | Inverse 的含义是“相反,对立”,说明反奇偶规则刚好与奇偶规则相反,例如对于一个矩形而言,使用奇偶规则会填充矩形内部,而使用反奇偶规则会填充矩形外部,这个会在后面示例中代码展示两者的区别。 148 | 149 | #### Android与填充模式相关的方法 150 | 151 | > 这些都是Path中的方法。 152 | 153 | | 方法 | 作用 | 154 | | --------------------- | ------------------------ | 155 | | setFillType | 设置填充规则 | 156 | | getFillType | 获取当前填充规则 | 157 | | isInverseFillType | 判断是否是反向(INVERSE)规则 | 158 | | toggleInverseFillType | 切换填充规则(即原有规则与反向规则之间相互切换) | 159 | 160 | #### 示例演示: 161 | 162 | 本演示着重于帮助理解填充模式中的一些难点和易混淆的问题,对于一些比较简单的问题,读者可自行验证,本文中不会过多赘述。 163 | 164 | ##### 奇偶规则与反奇偶规则 165 | 166 | ``` java 167 | mDeafultPaint.setStyle(Paint.Style.FILL); // 设置画布模式为填充 168 | 169 | canvas.translate(mViewWidth / 2, mViewHeight / 2); // 移动画布(坐标系) 170 | 171 | Path path = new Path(); // 创建Path 172 | 173 | //path.setFillType(Path.FillType.EVEN_ODD); // 设置Path填充模式为 奇偶规则 174 | path.setFillType(Path.FillType.INVERSE_EVEN_ODD); // 反奇偶规则 175 | 176 | path.addRect(-200,-200,200,200, Path.Direction.CW); // 给Path中添加一个矩形 177 | ``` 178 | 179 | 下面两张图片分别是在奇偶规则与反奇偶规则的情况下绘制的结果,可以看出其填充的区域刚好相反: 180 | 181 | > PS: 白色为背景色,黑色为填充色。 182 | 183 | ![](http://ww4.sinaimg.cn/large/005Xtdi2gw1f42jji5nm9j308c0et749.jpg) 184 | ![](http://ww1.sinaimg.cn/large/005Xtdi2gw1f42jjtay96j308c0etaa1.jpg) 185 | 186 | ##### 图形边的方向对非零奇偶环绕数规则填充结果的影响 187 | 188 | 我们之前讨论过给Path添加图形时顺时针与逆时针的作用,除了上次讲述的方便记录外,就是本文所涉及的另外一个重要作用了: **"作为非零环绕数规则的判断依据。"** 189 | 190 | 通过前面我们已经大致了解了在图形边的方向会如何影响到填充效果,我们这里验证一下: 191 | 192 | ``` java 193 | mDeafultPaint.setStyle(Paint.Style.FILL); // 设置画笔模式为填充 194 | 195 | canvas.translate(mViewWidth / 2, mViewHeight / 2); // 移动画布(坐系) 196 | 197 | Path path = new Path(); // 创建Path 198 | 199 | // 添加小正方形 (通过这两行代码来控制小正方形边的方向,从而演示不同的效果) 200 | // path.addRect(-200, -200, 200, 200, Path.Direction.CW); 201 | path.addRect(-200, -200, 200, 200, Path.Direction.CCW); 202 | 203 | // 添加大正方形 204 | path.addRect(-400, -400, 400, 400, Path.Direction.CCW); 205 | 206 | path.setFillType(Path.FillType.WINDING); // 设置Path填充模式为非零环绕规则 207 | 208 | canvas.drawPath(path, mDeafultPaint); // 绘制Path 209 | ``` 210 | 211 | ![](http://ww1.sinaimg.cn/large/005Xtdi2gw1f430h944zhj308c0et3yj.jpg) 212 | ![](http://ww3.sinaimg.cn/large/005Xtdi2gw1f430fono6uj308c0et74a.jpg) 213 | 214 | 215 | ### 布尔操作(API19) 216 | 217 | 布尔操作与我们中学所学的集合操作非常像,只要知道集合操作中的交集,并集,差集等操作,那么理解布尔操作也是很容易的。 218 | 219 | **布尔操作是两个Path之间的运算,主要作用是用一些简单的图形通过一些规则合成一些相对比较复杂,或难以直接得到的图形**。 220 | 221 | 如太极中的阴阳鱼,如果用贝塞尔曲线制作的话,可能需要六段贝塞尔曲线才行,而在这里我们可以用四个Path通过布尔运算得到,而且会相对来说更容易理解一点。 222 | 223 | ![](http://ww1.sinaimg.cn/large/005Xtdi2jw1f43b9o4yfuj308c0etq2y.jpg) 224 | 225 | ``` java 226 | canvas.translate(mViewWidth / 2, mViewHeight / 2); 227 | 228 | Path path1 = new Path(); 229 | Path path2 = new Path(); 230 | Path path3 = new Path(); 231 | Path path4 = new Path(); 232 | 233 | path1.addCircle(0, 0, 200, Path.Direction.CW); 234 | path2.addRect(0, -200, 200, 200, Path.Direction.CW); 235 | path3.addCircle(0, -100, 100, Path.Direction.CW); 236 | path4.addCircle(0, 100, 100, Path.Direction.CCW); 237 | 238 | 239 | path1.op(path2, Path.Op.DIFFERENCE); 240 | path1.op(path3, Path.Op.UNION); 241 | path1.op(path4, Path.Op.DIFFERENCE); 242 | 243 | canvas.drawPath(path1, mDeafultPaint); 244 | ``` 245 | 246 | 前面演示了布尔运算的作用,接下来我们了解一下布尔运算的核心:布尔逻辑。 247 | 248 | Path的布尔运算有五种逻辑,如下: 249 | 250 | | 逻辑名称 | 类比 | 说明 | 示意图 | 251 | | ------------------ | ---- | ------------------------ | ---------------------------------------- | 252 | | DIFFERENCE | 差集 | Path1中减去Path2后剩下的部分 | ![](http://ww2.sinaimg.cn/large/005Xtdi2gw1f43j85gcaqj305k03c0sn.jpg) | 253 | | REVERSE_DIFFERENCE | 差集 | Path2中减去Path1后剩下的部分 | ![](http://ww2.sinaimg.cn/large/005Xtdi2gw1f43jbaaw80j305k03c0sn.jpg) | 254 | | INTERSECT | 交集 | Path1与Path2相交的部分 | ![](http://ww3.sinaimg.cn/large/005Xtdi2gw1f43jbj4iddj305k03c746.jpg) | 255 | | UNION | 并集 | 包含全部Path1和Path2 | ![](http://ww2.sinaimg.cn/large/005Xtdi2gw1f43jbqk8rbj305k03cmx4.jpg) | 256 | | XOR | 异或 | 包含Path1与Path2但不包括两者相交的部分 | ![](http://ww3.sinaimg.cn/large/005Xtdi2gw1f43jby8c60j305k03c0sp.jpg) | 257 | 258 | #### 布尔运算方法 259 | 260 | 通过前面的理论知识铺垫,相信大家对布尔运算已经有了基本的认识和理解,下面我们用代码演示一下布尔运算: 261 | 262 | 在Path中的布尔运算有两个方法 263 | 264 | ``` java 265 | boolean op (Path path, Path.Op op) 266 | boolean op (Path path1, Path path2, Path.Op op) 267 | ``` 268 | 269 | 两个方法中的返回值用于判断布尔运算是否成功,它们使用方法如下: 270 | 271 | ``` java 272 | // 对 path1 和 path2 执行布尔运算,运算方式由第二个参数指定,运算结果存入到path1中。 273 | path1.op(path2, Path.Op.DIFFERENCE); 274 | 275 | // 对 path1 和 path2 执行布尔运算,运算方式由第三个参数指定,运算结果存入到path3中。 276 | path3.op(path1, path2, Path.Op.DIFFERENCE) 277 | ``` 278 | 279 | #### 布尔运算示例 280 | 281 | ![](http://ww1.sinaimg.cn/large/005Xtdi2gw1f43jz8xnbxj308c0etwes.jpg) 282 | 283 | 代码: 284 | 285 | ``` java 286 | int x = 80; 287 | int r = 100; 288 | 289 | canvas.translate(250,0); 290 | 291 | Path path1 = new Path(); 292 | Path path2 = new Path(); 293 | Path pathOpResult = new Path(); 294 | 295 | path1.addCircle(-x, 0, r, Path.Direction.CW); 296 | path2.addCircle(x, 0, r, Path.Direction.CW); 297 | 298 | pathOpResult.op(path1,path2, Path.Op.DIFFERENCE); 299 | canvas.translate(0, 200); 300 | canvas.drawText("DIFFERENCE", 240,0,mDeafultPaint); 301 | canvas.drawPath(pathOpResult,mDeafultPaint); 302 | 303 | pathOpResult.op(path1,path2, Path.Op.REVERSE_DIFFERENCE); 304 | canvas.translate(0, 300); 305 | canvas.drawText("REVERSE_DIFFERENCE", 240,0,mDeafultPaint); 306 | canvas.drawPath(pathOpResult,mDeafultPaint); 307 | 308 | pathOpResult.op(path1,path2, Path.Op.INTERSECT); 309 | canvas.translate(0, 300); 310 | canvas.drawText("INTERSECT", 240,0,mDeafultPaint); 311 | canvas.drawPath(pathOpResult,mDeafultPaint); 312 | 313 | pathOpResult.op(path1,path2, Path.Op.UNION); 314 | canvas.translate(0, 300); 315 | canvas.drawText("UNION", 240,0,mDeafultPaint); 316 | canvas.drawPath(pathOpResult,mDeafultPaint); 317 | 318 | pathOpResult.op(path1,path2, Path.Op.XOR); 319 | canvas.translate(0, 300); 320 | canvas.drawText("XOR", 240,0,mDeafultPaint); 321 | canvas.drawPath(pathOpResult,mDeafultPaint); 322 | ``` 323 | 324 | ### 计算边界 325 | 326 | 这个方法主要作用是计算Path所占用的空间以及所在位置,方法如下: 327 | 328 | ``` java 329 | void computeBounds (RectF bounds, boolean exact) 330 | ``` 331 | 332 | 它有两个参数: 333 | 334 | | 参数 | 作用 | 335 | | ------ | ------------------------------- | 336 | | bounds | 测量结果会放入这个矩形 | 337 | | exact | 是否精确测量,目前这一个参数作用已经废弃,一般写true即可 | 338 | 339 | 关于exact如有疑问可参见Google官方的提交记录[Path.computeBounds()](https://code.google.com/p/android/issues/detail?id=4070) 340 | 341 | #### 计算边界示例 342 | 343 | 计算path边界的一个简单示例. 344 | 345 | ![](http://ww4.sinaimg.cn/large/005Xtdi2gw1f44gxz4k1vj308c0etaa4.jpg) 346 | 347 | 代码: 348 | 349 | ``` java 350 | // 移动canvas,mViewWidth与mViewHeight在 onSizeChanged 方法中获得 351 | canvas.translate(mViewWidth/2,mViewHeight/2); 352 | 353 | RectF rect1 = new RectF(); // 存放测量结果的矩形 354 | 355 | Path path = new Path(); // 创建Path并添加一些内容 356 | path.lineTo(100,-50); 357 | path.lineTo(100,50); 358 | path.close(); 359 | path.addCircle(-100,0,100, Path.Direction.CW); 360 | 361 | path.computeBounds(rect1,true); // 测量Path 362 | 363 | canvas.drawPath(path,mDeafultPaint); // 绘制Path 364 | 365 | mDeafultPaint.setStyle(Paint.Style.STROKE); 366 | mDeafultPaint.setColor(Color.RED); 367 | canvas.drawRect(rect1,mDeafultPaint); // 绘制边界 368 | ``` 369 | 370 | ### 重置路径 371 | 372 | 重置Path有两个方法,分别是reset和rewind,两者区别主要有以下两点: 373 | 374 | | 方法 | 是否保留FillType设置 | 是否保留原有数据结构 | 375 | | ------ | :------------: | :--------: | 376 | | reset | 是 | 否 | 377 | | rewind | 否 | 是 | 378 | 379 | **这个两个方法应该何时选择呢?** 380 | 381 | 选择权重: FillType > 数据结构 382 | 383 | _因为“FillType”影响的是显示效果,而“数据结构”影响的是重建速度。_ 384 | 385 | 386 | ## 总结 387 | 388 | Path中常用的方法到此已经结束,希望能够帮助大家加深对Path的理解运用,让大家能够用Path愉快的玩耍。( ̄▽ ̄) 389 | 390 | (,,• ₃ •,,) 391 | #### PS: 由于本人水平有限,某些地方可能存在误解或不准确,如果你对此有疑问可以提交Issues进行反馈。 392 | 393 | ## About Me 394 | 395 | ### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop) 396 | 397 | 398 | 399 | ## 参考资料 400 | [Path](https://developer.android.com/reference/android/graphics/Path.html)
401 | [维基百科-Nonzero-rule](https://en.wikipedia.org/wiki/Nonzero-rule)
402 | [android绘图之Path总结](http://ghui.me/post/2015/10/android-graphics-path/)
403 | [布尔逻辑](https://zh.wikipedia.org/wiki/%E5%B8%83%E5%B0%94%E9%80%BB%E8%BE%91)
404 | [GoogleCode-Path.computeBounds()](https://code.google.com/p/android/issues/detail?id=4070)
405 | [Path.reset vs Path.rewind](http://stackoverflow.com/questions/11505617/path-reset-vs-path-rewind)
406 | []()
407 | []()
408 | 409 | 410 | 411 | 412 | 413 | -------------------------------------------------------------------------------- /CustomView/Advance/[09]Matrix_Basic.md: -------------------------------------------------------------------------------- 1 | 本文内容偏向理论,和 [画布操作](http://www.gcssloop.com/customview/Canvas_Convert/) 有重叠的部分,本文会让你更加深入的了解其中的原理。 2 | 3 | 本篇的主角Matrix,是一个一直在后台默默工作的劳动模范,虽然我们所有看到View背后都有着Matrix的功劳,但我们却很少见到它,本篇我们就看看它是何方神圣吧。 4 | 5 | > 由于Google已经对这一部分已经做了很好的封装,所以跳过本部分对实际开发影响并不会太大,不想深究的粗略浏览即可,下一篇中将会详细讲解Matrix的具体用法和技巧。 6 | > 7 | > ## ⚠️ 警告:测试本文章示例之前请关闭硬件加速。 8 | 9 | ## Matrix简介 10 | 11 | **Matrix是一个矩阵,主要功能是坐标映射,数值转换。** 12 | 13 | 它看起来大概是下面这样: 14 | 15 | ![](http://ww1.sinaimg.cn/large/006tKfTcly1fdz72rjfnjj30ak01yglo.jpg) 16 | 17 | **Matrix作用就是坐标映射,那么为什么需要Matrix呢? 举一个简单的例子:** 18 | 19 | 我的的手机屏幕作为物理设备,其物理坐标系是从左上角开始的,但我们在开发的时候通常不会使用这一坐标系,而是使用内容区的坐标系。 20 | 21 | 以下图为例,我们的内容区和屏幕坐标系还相差一个通知栏加一个标题栏的距离,所以两者是不重合的,我们在内容区的坐标系中的内容最终绘制的时候肯定要转换为实际的物理坐标系来绘制,Matrix在此处的作用就是转换这些数值。 22 | 23 | > 假设通知栏高度为20像素,导航栏高度为40像素,那么我们在内容区的(0,0)位置绘制一个点,最终就要转化为在实际坐标系中的(0,60)位置绘制一个点。 24 | 25 | ![](http://ww4.sinaimg.cn/large/005Xtdi2jw1f624vi3eb6j30rs0goab5.jpg) 26 | 27 | 以上是仅作为一个简单的示例,实际上不论2D还是3D,我们要将图形显示在屏幕上,都离不开Matrix,所以说Matrix是一个在背后辛勤工作的劳模。 28 | 29 | 30 | 31 | ### Matrix特点 32 | 33 | - 作用范围更广,Matrix在View,图片,动画效果等各个方面均有运用,相比与之前讲解等画布操作应用范围更广。 34 | - 更加灵活,画布操作是对Matrix的封装,Matrix作为更接近底层的东西,必然要比画布操作更加灵活。 35 | - 封装很好,Matrix本身对各个方法就做了很好的封装,让开发者可以很方便的操作Matrix。 36 | - 难以深入理解,很难理解中各个数值的意义,以及操作规律,如果不了解矩阵,也很难理解前乘,后乘。 37 | 38 | ### 常见误解 39 | 40 | **1.认为Matrix最下面的一行的三个参数(MPERSP_0、MPERSP_1、MPERSP_2)没有什么太大的作用,在这里只是为了凑数。** 41 | 42 | 实际上最后一行参数在3D变换中有着至关重要的作用,这一点会在后面中Camera一文中详细介绍。 43 | 44 | **2.最后一个参数MPERSP_2被解释为scale** 45 | 46 | 的确,更改MPERSP_2的值能够达到类似缩放的效果,但这是因为齐次坐标的缘故,并非这个参数的实际功能。 47 | 48 | ## Matrix基本原理 49 | 50 | Matrix 是一个矩阵,最根本的作用就是坐标转换,下面我们就看看几种常见变换的原理: 51 | 52 | > 我们所用到的变换均属于仿射变换,仿射变换是 线性变换(缩放,旋转,错切) 和 平移变换(平移) 的复合,由于这些概念对于我们作用并不大,此处不过多介绍,有兴趣可自行了解。 53 | 54 | 基本变换有4种: 平移(translate)、缩放(scale)、旋转(rotate) 和 错切(skew)。 55 | 56 | 下面我们看一下四种变换都是由哪些参数控制的。 57 | 58 | ![](http://ww2.sinaimg.cn/large/005Xtdi2jw1f60gwrhlnyj30c008zdgy.jpg) 59 | ![](http://ww2.sinaimg.cn/large/005Xtdi2jw1f633hvklfnj30c008zdge.jpg) 60 | 61 | **从上图可以看到最后三个参数是控制透视的,这三个参数主要在3D效果中运用,通常为(0, 0, 1),不在本篇讨论范围内,暂不过多叙述,会在之后对文章中详述其作用。** 62 | 63 | 由于我们以下大部分的计算都是基于矩阵乘法规则,如果你已经把线性代数还给了老师,请参考一下这里: 64 | **[维基百科-矩阵乘法](https://zh.wikipedia.org/wiki/%E7%9F%A9%E9%99%A3%E4%B9%98%E6%B3%95)** 65 | 66 | ### 1.缩放(Scale) 67 | 68 | ![](http://ww2.sinaimg.cn/large/006tKfTcly1fdz7baj17gj302h01rdfr.jpg) 69 | 70 | 用矩阵表示: 71 | 72 | ![](http://ww3.sinaimg.cn/large/006tKfTcly1fdz7busaiej3062020mx4.jpg) 73 | 74 | > 你可能注意到了,我们坐标多了一个1,这是使用了齐次坐标系的缘故,在数学中我们的点和向量都是这样表示的(x, y),两者看起来一样,计算机无法区分,为此让计算机也可以区分它们,增加了一个标志位,增加之后看起来是这样:
75 | > 76 | > (x, y, 1) - 点
77 | > (x, y, 0) - 向量
78 | > 79 | > 另外,齐次坐标具有等比的性质,(2,3,1)、(4,6,2)...(2N,3N,N)表示的均是(2,3)这一个点。(**将MPERSP_2解释为scale这一误解就源于此**)。 80 | 81 | 图例: 82 | 83 | ![](http://ww1.sinaimg.cn/large/005Xtdi2jw1f6cnk02zy9j308c0dwwej.jpg) 84 | 85 | ### 2.错切(Skew) 86 | 87 | 错切存在两种特殊错切,水平错切(平行X轴)和垂直错切(平行Y轴)。 88 | 89 | #### 水平错切 90 | 91 | ![](http://ww3.sinaimg.cn/large/006tKfTcly1fdz7d0niaqj303601mglj.jpg) 92 | 93 | 用矩阵表示: 94 | 95 | ![](http://ww4.sinaimg.cn/large/006tKfTcly1fdz7dryrfcj305m020glk.jpg) 96 | 97 | 图例: 98 | 99 | ![](http://ww2.sinaimg.cn/large/005Xtdi2jw1f6cniifb0sj308c0dw3yz.jpg) 100 | 101 | #### 垂直错切 102 | 103 | ![](http://ww3.sinaimg.cn/large/006tKfTcly1fdz7esq5j4j303701pdfr.jpg) 104 | 105 | 用矩阵表示: 106 | 107 | ![](http://ww4.sinaimg.cn/large/006tKfTcly1fdz7ffdxauj305n024glk.jpg) 108 | 109 | 图例: 110 | 111 | ![](http://ww4.sinaimg.cn/large/005Xtdi2jw1f6cnkwyksij308c0dwq3f.jpg) 112 | 113 | #### 复合错切 114 | 115 | > 水平错切和垂直错切的复合。 116 | 117 | ![](http://ww4.sinaimg.cn/large/006tKfTcly1fdz7g0lmcaj303801mq2v.jpg) 118 | 119 | 用矩阵表示: 120 | 121 | ![](http://ww2.sinaimg.cn/large/006tKfTcly1fdz7gkg5dej3062021mx4.jpg) 122 | 123 | 图例: 124 | 125 | ![](http://ww3.sinaimg.cn/large/005Xtdi2jw1f6cqdu6olfj308c0dwdgi.jpg) 126 | 127 | ### 3.旋转(Rotate) 128 | 129 | 假定一个点 A(x0, y0) ,距离原点距离为 r, 与水平轴夹角为 α 度, 绕原点旋转 θ 度, 旋转后为点 B(x, y) 如下: 130 | 131 | ![](http://ww3.sinaimg.cn/large/006tKfTcly1fdz7h61ddsj30gm03twel.jpg) 132 | 133 | 用矩阵表示: 134 | 135 | ![](http://ww2.sinaimg.cn/large/006tKfTcly1fdz7hn7pbdj308i0240sq.jpg) 136 | 137 | 图例: 138 | 139 | ![](http://ww4.sinaimg.cn/large/005Xtdi2jw1f6cpp174twj308c0dwt8s.jpg) 140 | 141 | ### 4.平移(Translate) 142 | 143 | > 此处也是使用齐次坐标的优点体现之一,实际上前面的三个操作使用 2x2 的矩阵也能满足需求,但是使用 2x2 的矩阵,无法将平移操作加入其中,而将坐标扩展为齐次坐标后,将矩阵扩展为 3x3 就可以将算法统一,四种算法均可以使用矩阵乘法完成。 144 | 145 | ![](http://ww4.sinaimg.cn/large/006tKfTcly1fdz7igi28cj302w01kdfr.jpg) 146 | 147 | 用矩阵表示: 148 | 149 | ![](http://ww2.sinaimg.cn/large/006tKfTcly1fdz7izsq8hj306b022mx4.jpg) 150 | 151 | 图例: 152 | 153 | ![](http://ww3.sinaimg.cn/large/005Xtdi2jw1f6dqiw20xoj308c0dw0su.jpg) 154 | 155 | ## Matrix复合原理 156 | 157 | 其实Matrix的多种复合操作都是使用矩阵乘法实现的,从原理上理解很简单,但是,使用矩阵乘法也有其弱点,后面的操作可能会影响到前面到操作,所以在构造Matrix时顺序很重要。 158 | 159 | 我们常用的四大变换操作,每一种操作在Matrix均有三类,前乘(pre),后乘(post)和设置(set),可以参见文末对[Matrix方法表](#fangfa),由于矩阵乘法不满足交换律,所以前乘(pre),后乘(post)和设置(set)的区别还是很大的。 160 | 161 | ### 前乘(pre) 162 | 163 | 前乘相当于矩阵的右乘: 164 | 165 | ![](https://ww1.sinaimg.cn/large/006tKfTcgy1fhe1ul01s9j302m00gq2u.jpg) 166 | 167 | > 这表示一个矩阵与一个特殊矩阵前乘后构造出结果矩阵。 168 | 169 | ### 后乘(post) 170 | 171 | 后乘相当于矩阵的左乘: 172 | 173 | ![](https://ww3.sinaimg.cn/large/006tKfTcgy1fhe1vta7ooj302s00pq2u.jpg) 174 | 175 | > 这表示一个矩阵与一个特殊矩阵后乘后构造出结果矩阵。 176 | 177 | ### 设置(set) 178 | 179 | 设置使用的不是矩阵乘法,而是直接覆盖掉原来的数值,所以,**使用设置可能会导致之前的操作失效**。 180 | 181 | ## 组合 182 | 183 | **关于 Matrix 的文章终有一个问题,就是 pre 和 post 这一部分的理论非常别扭,国内大多数文章都是这样的,看起来貌似是对的但很难理解,部分内容违背直觉。** 184 | 185 | **我由于也受到了这些文章的影响,自然而然的继承了这一理论,直到在评论区有一位小伙伴提出了一个问题,才让我重新审视了这一部分的内容,并进行了一定反思。** 186 | 187 | 经过良久的思考之后,我决定抛弃国内大部分文章的那套理论和结论,只用严谨的数学逻辑和程序逻辑来阐述这一部分的理论,也许仍有疏漏,如有发现请指正。 188 | 189 | **首先澄清两个错误结论,记住,是错误结论,错误结论,错误结论。** 190 | 191 | ### ~~错误结论一:pre 是顺序执行,post 是逆序执行。~~ 192 | 193 | 这个结论很具有迷惑性,因为这个结论并非是完全错误的,你很容易就能证明这个结论,例如下面这样: 194 | 195 | ```java 196 | // 第一段 pre 顺序执行,先平移(T)后旋转(R) 197 | Matrix matrix = new Matrix(); 198 | matrix.preTranslate(pivotX,pivotY); 199 | matrix.preRotate(angle); 200 | Log.e("Matrix", matrix.toShortString()); 201 | 202 | // 第二段 post 逆序执行,先平移(T)后旋转(R) 203 | Matrix matrix = new Matrix(); 204 | matrix.postRotate(angle); 205 | matrix.postTranslate(pivotX,pivotY) 206 | Log.e("Matrix", matrix.toShortString()); 207 | ``` 208 | 209 | **这两段代码最终结果是等价的,于是轻松证得这个结论的正确性,但事实真是这样么?** 210 | 211 | 首先,从数学角度分析,pre 和 post 就是右乘或者左乘的区别,其次,它们不可能实际影响运算顺序(程序执行顺序)。以上这两段代码等价也仅仅是因为最终化简公式一样而已。 212 | 213 | > 设原始矩阵为 M,平移为 T ,旋转为 R ,单位矩阵为 I ,最终结果为 M' 214 | > 215 | > - 矩阵乘法不满足交换律,即 A\\*B ≠ B\\*A 216 | > - 矩阵乘法满足结合律,即 (A\\*B)\\*C = A\\*(B\\*C) 217 | > - 矩阵与单位矩阵相乘结果不变,即 A * I = A 218 | 219 | ``` 220 | 由于上面例子中原始矩阵(M)是一个单位矩阵(I),所以可得: 221 | 222 | // 第一段 pre 223 | M' = (M*T)*R = I*T*R = T*R 224 | 225 | // 第二段 post 226 | M' = T*(R*M) = T*R*I = T*R 227 | ``` 228 | 229 | 由于两者最终的化简公式是相同的,所以两者是等价的,但是,这结论不具备普适性。 230 | 231 | **即原始矩阵不为单位矩阵的时候,两者无法化简为相同的公式,结果自然也会不同。另外,执行顺序就是程序书写顺序,不存在所谓的正序逆序。** 232 | 233 | ### ~~错误结论二:pre 是先执行,而 post 是后执行。~~ 234 | 235 | 这一条结论比上一条更离谱。 236 | 237 | 之所以产生这个错误完全是因为写文章的人懂英语。 238 | 239 | ``` 240 | pre :先,和 before 相似。 241 | post :后,和 after 相似。 242 | ``` 243 | 244 | 所以就得出了 pre 先执行,而 post 后执行这一说法,但从严谨的数学和程序角度来分析,完全是不可能的,还是上面所说的,**pre 和 post 不能影响程序执行顺序,而程序每执行一条语句都会得出一个确定的结果,所以,它根本不能控制先后执行,属于完全扯淡型。** 245 | 246 | **如果非要用这套理论强行解释的话,反而看起来像是 post 先执行,例如:** 247 | 248 | ```java 249 | matrix.preRotate(angle); 250 | matrix.postTranslate(pivotX,pivotY); 251 | ``` 252 | 253 | 同样化简公式: 254 | 255 | ``` 256 | // 矩阵乘法满足结合律 257 | M‘ = T*(M*R) = T*M*R = (T*M)*R 258 | ``` 259 | 260 | **从实际上来说,由于矩阵乘法满足结合律,所以不论你说是靠右先执行还是靠左先执行,从结果上来说都没有错。** 261 | 262 | **之前基于这条错误的结论我进行了一次错误的证明:** 263 | 264 | > **(这段内容注定要成为我写作历程中不可抹灭的耻辱,既然是公开文章,就应该对读者负责,虽然我在发表每一篇文章之前都竭力的求证其中的问题,各种细节,避免出现这种错误,但终究还是留下了这样一段内容,在此我诚挚的向我所有的读者道歉。)** 265 | > 266 | > 关注我的读者请尽量看我在 [个人博客](http://www.gcssloop.com/#blog) 和 [GitHub](https://github.com/GcsSloop/AndroidNote/blob/master/README.md) 发布的版本,这两个平台都在博文修复计划之内,有任何错误或者纰漏,都会首先修复这两个平台的文章。另外,所有进行修复过的文章都会在我的微博 [@GcsSloop](http://weibo.com/GcsSloop) 重新发布说明,关注我的微博可以第一时间得到博文更新或者修复的消息。 267 | > 268 | > ------ 269 | > 270 | > ## 以下是错误证明: 271 | > 272 | > ~~在实际操作中,我们每一步操作都会得出准确的计算结果,但是为什么还会用存在先后的说法? 难道真的能够用pre和post影响计算顺序? 实则不然,下面我们用一个例子说明:~~ 273 | > 274 | > ```java 275 | > Matrix matrix = new Matrix(); 276 | > matrix.postScale(0.5f, 0.8f); 277 | > matrix.preTranslate(1000, 1000); 278 | > Log.e(TAG, "MatrixTest" + matrix.toShortString()); 279 | > ``` 280 | > 281 | > ~~在上面的操作中,如果按照正常的思路,先缩放,后平移,缩放操作执行在前,不会影响到后续的平移操作,但是执行结果却发现平移距离变成了(500, 800)。~~ 282 | > 283 | > ~~在上面例子中,计算顺序是没有问题的,先计算的缩放,然后计算的平移,而缩放影响到平移则是因为前一步缩放后的结果矩阵右乘了平移矩阵,这是符合矩阵乘法的运算规律的,也就是说缩放操作虽然在前却影响到了平移操作,**相当于先执行了平移操作,然后执行的缩放操作,因此才有pre操作会先执行,而post操作会后执行这一说法**。~~ 284 | > 285 | > ------ 286 | 287 | 上面的论证是完全错误的,因为可以轻松举出反例: 288 | 289 | ```java 290 | Matrix matrix = new Matrix(); 291 | matrix.preScale(0.5f, 0.8f); 292 | matrix.preTranslate(1000, 1000); 293 | Log.e(TAG, "MatrixTest" + matrix.toShortString()); 294 | ``` 295 | 296 | 反例中,虽然将 `postScale` 改为了 `preScale` ,但两者结果是完全相同的,所以先后论根本就是错误的。 297 | 298 | 他们结果相同是因为最终化简公式是相同的,都是 S*T 299 | 300 | 之所以平移距离是 MTRANS\_X = 500,MTRANS\_Y = 800,那是因为执行 Translate 之前 Matrix 已经具有了一个缩放比例。在右乘的时候影响到了具体的数值计算,可以用矩阵乘法计算一下。 301 | 302 | ![](http://ww3.sinaimg.cn/large/006tKfTcly1fdz7lhb20fj30lz01zgm8.jpg) 303 | 304 | 最终结果为: 305 | 306 | ![](http://ww2.sinaimg.cn/large/006tKfTcly1fdz7m2pgyuj303o022wef.jpg) 307 | 308 | 当 T*S 的时候,缩放比例则不会影响到 MTRANS\\_X 和 MTRANS\\_Y ,具体可以使用矩阵乘法自己计算一遍。 309 | 310 | ## 如何理解和使用 pre 和 post ? 311 | 312 | 不要去管什么先后论,顺序论,就按照最基本的矩阵乘法理解。 313 | 314 | ``` 315 | pre : 右乘, M‘ = M*A 316 | post : 左乘, M’ = A*M 317 | ``` 318 | 319 | **那么如何使用?** 320 | 321 | 正确使用方式就是先构造正常的 Matrix 乘法顺序,之后根据情况使用 pre 和 post 来把这个顺序实现。 322 | 323 | 还是用一个最简单的例子理解,假设需要围绕某一点旋转。 324 | 325 | 可以用这个方法 `xxxRotate(angle, pivotX, pivotY)` ,由于我们这里需要组合构造一个 Matrix,所以不直接使用这个方法。 326 | 327 | 首先,有两条基本定理: 328 | 329 | - 所有的操作(旋转、平移、缩放、错切)默认都是以坐标原点为基准点的。 330 | - 之前操作的坐标系状态会保留,并且影响到后续状态。 331 | 332 | 基于这两条基本定理,我们可以推算出要基于某一个点进行旋转需要如下步骤: 333 | 334 | ``` 335 | 1. 先将坐标系原点移动到指定位置,使用平移 T 336 | 2. 对坐标系进行旋转,使用旋转 S (围绕原点旋转) 337 | 3. 再将坐标系平移回原来位置,使用平移 -T 338 | ``` 339 | 340 | 具体公式如下: 341 | 342 | > M 为原始矩阵,是一个单位矩阵, M‘ 为结果矩阵, T 为平移, R为旋转 343 | 344 | ``` 345 | M' = M*T*R*-T = T*R*-T 346 | ``` 347 | 348 | 按照公式写出来的伪代码如下: 349 | 350 | ```java 351 | Matrix matrix = new Matrix(); 352 | matrix.preTranslate(pivotX,pivotY); 353 | matrix.preRotate(angle); 354 | matrix.preTranslate(-pivotX, -pivotY); 355 | ``` 356 | 357 | 358 | 359 | 360 | 361 | 围绕某一点操作可以拓展为通用情况,即: 362 | 363 | ```java 364 | Matrix matrix = new Matrix(); 365 | matrix.preTranslate(pivotX,pivotY); 366 | // 各种操作,旋转,缩放,错切等,可以执行多次。 367 | matrix.preTranslate(-pivotX, -pivotY); 368 | ``` 369 | 370 | 公式为: 371 | 372 | ``` 373 | M' = M*T* ... *-T = T* ... *-T 374 | ``` 375 | 376 | 但是这种方式,两个调整中心的平移函数就拉的太开了,所以通常采用这种写法: 377 | 378 | ```java 379 | Matrix matrix = new Matrix(); 380 | // 各种操作,旋转,缩放,错切等,可以执行多次。 381 | matrix.postTranslate(pivotX,pivotY); 382 | matrix.preTranslate(-pivotX, -pivotY); 383 | ``` 384 | 385 | 这样公式为: 386 | 387 | ``` 388 | M' = T*M* ... *-T = T* ... *-T 389 | ``` 390 | 391 | 可以看到最终化简结果是相同的。 392 | 393 | 所以说,pre 和 post 就是用来调整乘法顺序的,正常情况下应当正向进行构建出乘法顺序公式,之后根据实际情况调整书写即可。 394 | 395 | **在构造 Matrix 时,个人建议尽量使用一种乘法,前乘或者后乘,这样操作顺序容易确定,出现问题也比较容易排查。当然,由于矩阵乘法不满足交换律,前乘和后乘的结果是不同的,使用时应结合具体情景分析使用。** 396 | 397 | 398 | 399 | ### 下面我们用不同对方式来构造一个相同的矩阵: 400 | 401 | 注意: 402 | 403 | - 1.由于矩阵乘法不满足交换律,请保证使用初始矩阵(Initial Matrix),否则可能导致运算结果不同。 404 | - 2.注意构造顺序,顺序是会影响结果的。 405 | - 3.Initial Matrix是指new出来的新矩阵,或者reset后的矩阵,是一个单位矩阵。 406 | 407 | #### 1.仅用pre: 408 | 409 | ```java 410 | // 使用pre, M' = M*T*S = T*S 411 | Matrix m = new Matrix(); 412 | m.reset(); 413 | m.preTranslate(tx, ty); 414 | m.preScale(sx, sy); 415 | ``` 416 | 417 | 用矩阵表示: 418 | ![](http://ww4.sinaimg.cn/large/006tKfTcly1fdz7mv29jhj30gg02374b.jpg) 419 | 420 | #### 2.仅用post: 421 | 422 | ```java 423 | // 使用post, M‘ = T*S*M = T*S 424 | Matrix m = new Matrix(); 425 | m.reset(); 426 | m.postScale(sx, sy); //,越靠前越先执行。 427 | m.postTranslate(tx, ty); 428 | ``` 429 | 430 | 用矩阵表示: 431 | 432 | ![](http://ww1.sinaimg.cn/large/006tKfTcly1fdz7nde6gcj30gh020dfv.jpg) 433 | 434 | #### 3.混合: 435 | 436 | ```java 437 | // 混合 M‘ = T*M*S = T*S 438 | Matrix m = new Matrix(); 439 | m.reset(); 440 | m.preScale(sx, sy); 441 | m.postTranslate(tx, ty); 442 | ``` 443 | 444 | 或: 445 | 446 | ```java 447 | // 混合 M‘ = T*M*S = T*S 448 | Matrix m = new Matrix(); 449 | m.reset(); 450 | m.postTranslate(tx, ty); 451 | m.preScale(sx, sy); 452 | ``` 453 | 454 | > 由于此处只有两步操作,且指定了先后,所以代码上交换并不会影响结果。 455 | 456 | 用矩阵表示: 457 | 458 | ![](http://ww4.sinaimg.cn/large/006tKfTcly1fdz7o3i9kfj30gh021aa3.jpg) 459 | 460 | **注意: 由于矩阵乘法不满足交换律,请保证初始矩阵为单位矩阵,如果初始矩阵不为单位矩阵,则导致运算结果不同。** 461 | 462 | 上面虽然用了很多不同的写法,但最终的化简公式是一样的,这些不同的写法,都是根据同一个公式反向推算出来的。 463 | 464 | ## Matrix方法表 465 | 466 | 这个方法表,暂时放到这里让大家看看,方法的使用讲解放在下一篇文章中。 467 | 468 | | 方法类别 | 相关API | 摘要 | 469 | | -------- | ---------------------------------------- | -------------------------- | 470 | | 基本方法 | equals hashCode toString toShortString | 比较、 获取哈希值、 转换为字符串 | 471 | | 数值操作 | set reset setValues getValues | 设置、 重置、 设置数值、 获取数值 | 472 | | 数值计算 | mapPoints mapRadius mapRect mapVectors | 计算变换后的数值 | 473 | | 设置(set) | setConcat setRotate setScale setSkew setTranslate | 设置变换 | 474 | | 前乘(pre) | preConcat preRotate preScale preSkew preTranslate | 前乘变换 | 475 | | 后乘(post) | postConcat postRotate postScale postSkew postTranslate | 后乘变换 | 476 | | 特殊方法 | setPolyToPoly setRectToRect rectStaysRect setSinCos | 一些特殊操作 | 477 | | 矩阵相关 | invert isAffine isIdentity | 求逆矩阵、 是否为仿射矩阵、 是否为单位矩阵 ... | 478 | 479 | ## 总结 480 | 481 | 对于Matrix重在理解,理解了其中的原理之后用起来将会更加得心应手。 482 | 483 | 学完了本篇之后,推荐配合鸿洋大大的视频课程 [ 484 | 打造个性的图片预览与多点触控](http://www.imooc.com/learn/239) 食用,定然能够让你对Matrix对理解更上一层楼。 485 | 486 | 由于个人水平有限,文章中可能会出现错误,如果你觉得哪一部分有错误,或者发现了错别字等内容,欢迎在评论区告诉我,另外,据说关注 [作者微博](http://weibo.com/GcsSloop) 不仅能第一时间收到新文章消息,还能变帅哦。 487 | 488 | ## About 489 | 490 | [本系列相关文章](http://www.gcssloop.com/customview/CustomViewIndex/) 491 | 492 | 作者微博: [GcsSloop](http://weibo.com/GcsSloop) 493 | 494 | ## 参考资料 495 | 496 | [Matrix](https://developer.android.com/reference/android/graphics/Matrix.html)
497 | [Android中图像变换Matrix的原理、代码验证和应用](http://biandroid.iteye.com/blog/1399462)
498 | [Android中关于矩阵(Matrix)前乘后乘的一些认识](http://blog.csdn.net/linmiansheng/article/details/18820599)
499 | [维基百科-仿射变换](https://zh.wikipedia.org/wiki/%E4%BB%BF%E5%B0%84%E5%8F%98%E6%8D%A2)
500 | [维基百科-齐次坐标](https://zh.wikipedia.org/wiki/%E9%BD%90%E6%AC%A1%E5%9D%90%E6%A0%87)
501 | [维基百科-线性映射](https://zh.wikipedia.org/wiki/%E7%BA%BF%E6%80%A7%E6%98%A0%E5%B0%84)
502 | [齐次坐标系入门级思考](https://oncemore2020.github.io/blog/homogeneous/)
503 | [仿射变换与齐次坐标](https://guangchun.wordpress.com/2011/10/12/affineandhomogeneous/)
504 | -------------------------------------------------------------------------------- /CustomView/Advance/[12]Dispatch-TouchEvent-Theory.md: -------------------------------------------------------------------------------- 1 | # 事件分发机制原理 2 | 3 | 4 | 之前讲解了很多与View绘图相关的知识,你可以在 [安卓自定义View教程目录](http://www.gcssloop.com/customview/CustomViewIndex) 中查看到这些文章,如果你理解了这些文章,那么至少2D绘图部分不是难题了,大部分的需求都能满足,但是关于View还有很多知识点,例如: `让绘图更加炫酷的Paint`,`让View动起来的动画`,`与用户交互的触控事件` 等一系列内容。**本次就带大家简单的了解一下与交互息息相关的东西-事件分发原理**。 5 | 6 | > 本次魔法小火车的终点站是事件分发,请各位魔法师带好装备,准备登车启程。 7 | 8 | **注意:本文中所有源码分析部分均基于 API23(Android 6.0) 版本,由于安卓系统源码改变很多,可能与之前版本有所不同,但基本流程都是一致的。** 9 | 10 | 11 | 12 | ## 为什么要有事件分发机制? 13 | 14 | **安卓上面的View是树形结构的,View可能会重叠在一起,当我们点击的地方有多个View都可以响应的时候,这个点击事件应该给谁呢?为了解决这一个问题,就有了事件分发机制。** 15 | 16 | 如下图,View是一层一层嵌套的,当手指点击 `View1` 的时候,下面的`ViewGroupA`、 `RootView` 等也是能够响应的,为了确定到底应该是哪个View处理这次点击事件,就需要事件分发机制来帮忙。 17 | 18 | ![](http://ww1.sinaimg.cn/large/005Xtdi2jw1f87nsnluf5j308c0eamxg.jpg) 19 | 20 | 21 | 22 | ## View的结构: 23 | 24 | 我们的View是树形结构的,在上一个问题中实例View的结构大致如下: 25 | 26 | **layout文件:** 27 | 28 | ```XML 29 | 37 | 38 | 42 | 43 | 47 | 48 | 49 | 50 | 55 | 56 | 57 | ``` 58 | 59 | 60 | 61 | **View结构:** 62 | 63 | ![](http://ww4.sinaimg.cn/large/005Xtdi2jw1f87juodlepj308q09ut8v.jpg) 64 | 65 | **可以看到在上面的View结构中莫名多出来的两个东西,`PhoneWindow` 和 `DecorView` ,这两个我们并没有在Layout文件中定义过,但是为什么会存在呢?** 66 | 67 | > 仔细观察上面的 layout 文件,你会发现一个问题,我在 layout 文件中的最顶层 View(Group) 的大小并不是填满父窗体的,留下了大量的空白区域,由于我们的手机屏幕不能透明,所以这些空白区域肯定要显示一些东西,那么应该显示什么呢? 68 | > 69 | > 有过安卓开发经验的都知道,屏幕上没有View遮挡的部分会显示主题的颜色。不仅如此,最上面的一个标题栏也没有在 layout 文件中,这个标题栏又是显示在哪里的呢? 70 | > 71 | > **你没有猜错,这个主题颜色和标题栏等内容就是显示在`DecorView`中的。** 72 | 73 | 74 | 75 | **现在知道 `DecorView` 是干什么的了,那么`PhoneWindow` 又有什么作用?** 76 | 77 | > 要了解 PhoneWindow 是干啥的,首先要了解啥是 Window ,看官方说明: 78 | > 79 | > Abstract base class for a top-level window look and behavior policy. An instance of this class should be used as the top-level view added to the window manager. It provides standard UI policies such as a background, title area, default key processing, etc. 80 | > 81 | > 82 | > 简单来说,Window是一个抽象类,是所有视图的最顶层容器,视图的外观和行为都归他管,不论是背景显示,标题栏还是事件处理都是他管理的范畴,它其实就像是View界的太上皇(虽然能管的事情看似很多,但是没实权,因为抽象类不能直接使用)。 83 | > 84 | > 而 PhoneWindow 作为 Window 的唯一亲儿子(唯一实现类),自然就是 View 界的皇帝了,PhoneWindow 的权利可是非常大大,不过对于我们来说用处并不大,因为皇帝平时都是躲在深宫里面的,虽然偶尔用特殊方法能见上一面,但想要完全指挥 PhoneWindow 为你工作是很困难的。 85 | > 86 | > 而上面说的 DecorView 是 PhoneWindow 的一个内部类,其职位相当于小太监,就是跟在 PhoneWindow 身边专业为 PhoneWindow 服务的,除了自己要干活之外,也负责消息的传递,PhoneWindow 的指示通过 DecorView 传递给下面的 View,而下面 View 的信息也通过 DecorView 回传给 PhoneWindow。 87 | 88 | 89 | ## 事件分发、拦截与消费 90 | 91 | 下表省略了 PhoneWidow 和 DecorView。 92 | 93 | > `√` 表示有该方法。 94 | > 95 | > `X` 表示没有该方法。 96 | 97 | | 类型 | 相关方法 | Activity | ViewGroup | View | 98 | | :--: | :-------------------: | :------: | :-------: | :--: | 99 | | 事件分发 | dispatchTouchEvent | √ | √ | √ | 100 | | 事件拦截 | onInterceptTouchEvent | X | √ | X | 101 | | 事件消费 | onTouchEvent | √ | √ | √ | 102 | 103 | 104 | 这个三个方法均有一个 boolean(布尔) 类型的返回值,通过返回 true 和 false 来控制事件传递的流程。 105 | 106 | PS: 从上表可以看到 Activity 和 View 都是没有事件拦截的,这是因为: 107 | 108 | > Activity 作为原始的事件分发者,如果 Activity 拦截了事件会导致整个屏幕都无法响应事件,这肯定不是我们想要的效果。 109 | > 110 | > View最为事件传递的最末端,要么消费掉事件,要么不处理进行回传,根本没必要进行事件拦截。 111 | 112 | 113 | 114 | ## 事件分发流程 115 | 116 | 前面我们了解到了我们的View是树形结构的,基于这样的结构,我们的事件可以进行有序的分发。 117 | 118 | 事件收集之后最先传递给 Activity, 然后依次向下传递,大致如下: 119 | 120 | ``` 121 | Activity -> PhoneWindow -> DecorView -> ViewGroup -> ... -> View 122 | ``` 123 | 124 | 这样的事件分发机制逻辑非常清晰,可是,你是否注意到一个问题?如果最后分发到View,如果这个View也没有处理事件怎么办,就这样让事件浪费掉? 125 | 126 | 当然不会啦,如果没有任何View消费掉事件,那么这个事件会按照反方向回传,最终传回给Activity,如果最后 Activity 也没有处理,本次事件才会被抛弃: 127 | 128 | ``` 129 | Activity <- PhoneWindow <- DecorView <- ViewGroup <- ... <- View 130 | ``` 131 | 132 | **看到这里,我不禁微微一皱眉,这个东西咋看起来那么熟悉呢?再仔细一看,这不就是一个非常经典的[责任链模式](https://zh.wikipedia.org/wiki/%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8F)吗,** 如果我能处理就拦截下来自己干,如果自己不能处理或者不确定就交给责任链中下一个对象。 133 | 134 | **这种设计是非常精巧的,上层View既可以直接拦截该事件,自己处理,也可以先询问(分发给)子View,如果子View需要就交给子View处理,如果子View不需要还能继续交给上层View处理。既保证了事件的有序性,又非常的灵活。在我第一次将这个逻辑弄清楚的时候,看着这样精妙的设计,简直想欢呼庆贺一下。** 135 | 136 | 其实关于事件传递机制,吴小龙的 [Android事件传递机制分析](http://wuxiaolong.me/2015/12/19/MotionEvent/) 一文中的比喻非常有趣,本文也会借鉴一些其中的内容。 137 | 138 | 先确定几个角色: 139 | 140 | > Activity - 公司大老板 141 | > 142 | > RootView - 项目经理 143 | > 144 | > ViewGroupA - 技术小组长 145 | > 146 | > View1 - 码农小王(公司里唯一的码农) 147 | > 148 | > View2 - 跑龙套的路人甲,无视即可 149 | 150 | **PS:由于 PhoneWindow 和 DecorView 我们无法直接操作,以下所有示例均省略了 PhoneWindow 和 DecorView。** 151 | 152 | 153 | 154 | ### 1.点击 View1 区域但没有任何 View 消费事件 155 | 156 | ![](http://ww1.sinaimg.cn/large/005Xtdi2jw1f87nsnluf5j308c0eamxg.jpg) 157 | 158 | 当手指在 `View1` 区域点击了一下之后,如果所有View都不消耗事件,你就能看到一个完整的事件分发流程,大致如下: 159 | 160 | > 红色箭头方向表示事件分发方向。 161 | > 162 | > 绿色箭头方向表示事件回传方向。 163 | 164 | ![](http://ww2.sinaimg.cn/large/005Xtdi2jw1f88i0q8uozj30nm0kqwhm.jpg) 165 | 166 | 167 | > **注意: 上图显示分发流程仅仅是一个示意流程,并不代表实际情况,如果按照实际情况绘制,会导致流程图非常复杂和混乱,在纠结了好久之后做了一个艰难的决定,采用这样一个简化后的流程。** 168 | > 169 | > 上面的流程中存在部分不合理内容,请大家选择性接受。 170 | > 171 | > 1. 事件返回时 `dispatchTouchEvent` 直接指向了父View的 `onTouchEvent` 这一部分是不合理的,实际上它仅仅是给了父View的 `dispatchTouchEvent` 一个 false 返回值,父View根据返回值来调用自身的 `onTouchEvent`。 172 | > 2. ViewGroup 是根据 `onInterceptTouchEvent` 的返回值来确定是调用子View的 `dispatchTouchEvent` 还是自身的 `onTouchEvent`, 并没有将调用交给 `onInterceptTouchEvent`。 173 | > 3. ViewGroup 的事件分发机制伪代码如下,可以看出调用的顺序。 174 | > 175 | > ```java 176 | > public boolean dispatchTouchEvent(MotionEvent ev) { 177 | > boolean result = false; // 默认状态为没有消费过 178 | > 179 | > if (!onInterceptTouchEvent(ev)) { // 如果没有拦截交给子View 180 | > result = child.dispatchTouchEvent(ev); 181 | > } 182 | > 183 | > if (!result) { // 如果事件没有被消费,询问自身onTouchEvent 184 | > result = onTouchEvent(ev); 185 | > } 186 | > 187 | > return result; 188 | > } 189 | > ``` 190 | 191 | **测试:** 192 | 193 | > **情景:老板: 我看公司最近业务不咋地,准备发展一下电商业务,下周之前做个淘宝出来试试怎么样。** 194 | > 195 | > **事件顺序,老板(MainActivity)要做淘宝,这个事件通过各个部门(ViewGroup)一层一层的往下传,传到最底层的时候,码农小王(View1)发现做不了,于是消息又一层一层的回传到老板那里。** 196 | > 197 | > 可以看到整个事件传递路线非常有序。从Activity开始,最后回传给Activity结束(由于我们无法操作Phone Window和DecorView,所以没有它们的信息)。 198 | 199 | ``` 200 | MainActivity [老板]: dispatchTouchEvent 经理,我准备发展一下电商业务,下周之前做一个淘宝出来. 201 | RootView     [经理]: dispatchTouchEvent 呼叫技术部,老板要做淘宝,下周上线. 202 | RootView     [经理]: onInterceptTouchEvent (老板可能疯了,但又不是我做.) 203 | ViewGroupA   [组长]: dispatchTouchEvent 老板要做淘宝,下周上线? 204 | ViewGroupA   [组长]: onInterceptTouchEvent (看着不太靠谱,先问问小王怎么看) 205 | View1        [码农]: dispatchTouchEvent 做淘宝??? 206 | View1        [码农]: onTouchEvent 这个真心做不了啊. 207 | ViewGroupA   [组长]: onTouchEvent 小王说做不了. 208 | RootView     [经理]: onTouchEvent 报告老板, 技术部说做不了. 209 | MainActivity [老板]: onTouchEvent 这么简单都做不了,你们都是干啥的(愤怒). 210 | ``` 211 | 212 | 213 | 214 | ### 2.点击 View1 区域且事件被 View1 消费 215 | 216 | 如果事件被View1消费掉了则事件会回传告诉上层View这个事件已经被我解决了,上层View就无需再响应了。 217 | 218 | ![](http://ww2.sinaimg.cn/large/005Xtdi2jw1f88ll27wv9j30nm0kqtbo.jpg) 219 | 220 | > 注意:这张图中的事件回传路径才是正确的路径。 221 | 222 | **测试:** 223 | 224 | > **情景:老板: 我觉得咱们这个app按钮不好看,做的有光泽一点,要让人有一种想点的欲望。** 225 | > 226 | > **事件顺序,老板(MainActivity)要做改界面,这个事件通过各个部门(ViewGroup)一层一层的往下传,传到最底层的时候,码农小王(View1)就在按钮上添加了一道光(为啥是小王呢?因为公司没有设计师)。** 227 | > 228 | > 可以看出,事件一旦被消费就意味着消息传递的结束,上层View知道了事件已经被消费掉,就不再处理了。 229 | 230 | ``` 231 | MainActivity [老板]: dispatchTouchEvent 把按钮做的好看一点,要有光泽,给人一种点击的欲望. 232 | RootView     [经理]: dispatchTouchEvent 技术部,老板说按钮不好看,要加一道光. 233 | RootView     [经理]: onInterceptTouchEvent 234 | ViewGroupA   [组长]: dispatchTouchEvent 给按钮加上一道光. 235 | ViewGroupA   [组长]: onInterceptTouchEvent 236 | View1        [码农]: dispatchTouchEvent 加一道光. 237 | View1        [码农]: onTouchEvent 做好了. 238 | ``` 239 | 240 | > 加一道光: 241 | > 242 | > ![](http://ww4.sinaimg.cn/large/005Xtdi2jw1f88oqj0o4jj304j03paa4.jpg) 243 | 244 | 245 | 246 | ### 3.点击 View1 区域但事件被 ViewGroupA 拦截 247 | 248 | > 上层的View有权拦截事件,不传递给下层View,例如 ListView 滑动的时候,就不会将事件传递给下层的子 View。 249 | 250 | ![](http://ww4.sinaimg.cn/large/005Xtdi2jw1f88p3r45vfj30nm0kqn00.jpg) 251 | 252 | > 注意:可以看到,如果上层拦截了事件,下层View将接收不到事件信息。 253 | 254 | **测试:** 255 | 256 | > **情景:老板: 报告一下项目进度。** 257 | > 258 | > **事件顺序,老板(MainActivity)要知道项目进度,这个事件通过各个部门(ViewGroup)一层一层的往下传,传到技术组组长(ViewGroupA)的时候,组长(ViewGroupA)上报任务即可。无需告知码农小王(View1)。** 259 | 260 | ``` 261 | MainActivity [老板]: dispatchTouchEvent 现在项目做到什么程度了? 262 | RootView     [经理]: dispatchTouchEvent 技术部,你们的app快做完了么? 263 | RootView     [经理]: onInterceptTouchEvent 264 | ViewGroupA   [组长]: dispatchTouchEvent 项目进度? 265 | ViewGroupA   [组长]: onInterceptTouchEvent 266 | ViewGroupA   [组长]: onTouchEvent 正在测试,明天就测试完了 267 | ``` 268 | 269 | ### 其他情况 270 | 271 | 事件分发机制设计到到情形非常多,这里就不一一列举了,记住以下几条原则就行了。 272 | 273 | * 1.如果事件被消费,就意味着事件信息传递终止。 274 | * 2.如果事件一直没有被消费,最后会传给Activity,如果Activity也不需要就被抛弃。 275 | * 3.判断事件是否被消费是根据返回值,而不是根据你是否使用了事件。 276 | 277 | 278 | #### [文中测试用的源码下载](https://raw.githubusercontent.com/GcsSloop/AndroidNote/master/CustomView/Demo/dispatchTouchEventDemo.zip) 279 | 280 | ## 总结 281 | 282 | View的事件分发机制实际上就是一个非常经典的责任链模式,如果你了解责任链模式,那么事件分发对你来说并不是什么难题,如果你不了解责任链模式,刚好借此机会学习一下啦。 283 | 284 | > **责任链模式:** 285 | > 286 | > 当有多个对象均可以处理同一请求的时候,将这些对象串联成一条链,并沿着这条链传递改请求,直到有对象处理它为止。 287 | 288 | Android 中事件分发机制原理虽然非常简单,但由于实际场景非常复杂,一旦具体到某个场景中变得很麻烦,而本文仅仅是带你简单的了解一下事件分发机制,更详细的内容和具体的一些特殊情形处理会在后续文章中进行讲解。 289 | 290 | 由于个人水平有限,文章中可能会出现错误,如果你觉得哪一部分有错误,或者发现了错别字等内容,欢迎在评论区告诉我,另外,据说关注[作者微博](http://weibo.com/GcsSloop)不仅能第一时间收到新文章消息,还能变帅哦。 291 | 292 | 293 | 294 | ## 参考资料 295 | 296 | [Activity](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/app/Activity.java)
297 | [PhoneWindow](https://android.googlesource.com/platform/frameworks/base/+/696cba573e651b0e4f18a4718627c8ccecb3bda0/policy/src/com/android/internal/policy/impl/PhoneWindow.java)
298 | [ViewGroup](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewGroup.java)
299 | [View](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/View.java)
300 | [Android事件传递机制分析](http://wuxiaolong.me/2015/12/19/MotionEvent/)
301 | [Android ViewGroup/View 事件分发机制详解](http://anany.me/2015/11/08/touchevent/)
302 | [ Android事件分发机制完全解析,带你从源码的角度彻底理解(上)](http://blog.csdn.net/guolin_blog/article/details/9097463)
303 | [ Android事件分发机制完全解析,带你从源码的角度彻底理解(下)](http://blog.csdn.net/sinyu890807/article/details/9153747)
304 | [更简单的学习Android事件分发](http://www.idtkm.com/customview/customview11/)
305 | 《安卓开发艺术探索》 306 | 307 | 308 | 309 | ## About Me 310 | 311 | ### 作者微博: @GcsSloop 312 | 313 | 314 | 315 | -------------------------------------------------------------------------------- /CustomView/Advance/[19]gesture-detector.md: -------------------------------------------------------------------------------- 1 | # Android 手势检测(GestureDetector) 2 | 3 | Android 手势检测,主要是 GestureDetector 相关内容的用法和注意事项,本文依旧属于事件处理这一体系,部分内容会涉及到之前文章提及过的知识点,如果你没看过之前的文章,可以到 [自定义 View 系列](http://www.gcssloop.com/customview/CustomViewIndex) 来查看这些内容。 4 | 5 | 在开发 Android 手机应用过程中,可能需要对一些手势作出响应,如:单击、双击、长按、滑动、缩放等。这些都是很常用的手势。就拿最简单的双击来说吧,假如我们需要判断一个控件是否被双击(即在较短的时间内快速的点击两次),似乎是一个很容易的任务,但仔细考虑起来,要处理的细节问题也有不少,例如: 6 | 7 | 1. **记录点击次数**,为了判断是否被点击超过 1 次,所以必须记录点击次数。 8 | 2. **记录点击时间**,由于双击事件是较快速的点击两次,像点击一次后,过来几分钟再点击一次肯定不能算是双击事件,所以在记录点击次数的同时也要记录上一次的点击时间,我们可以设置本次点击距离上一次时间超过一定时间(例如:超过100ms)就不识别为双击事件。 9 | 3. **点击状态重置**,在响应双击事件,或者判断不是双击事件的时候要重置计数器和上一次点击时间。重置既可以在点击的时候判断并进行重新设置,也可以使用定时器等超过一定时间后重置状态。 10 | 11 | 这样看起来,判断一个双击事件就有这么多麻烦事情,更别其他的手势了,虽然这些看起来都很简单,但设计起来需要考虑的细节情况实在是太多了。 12 | 13 | 那么有没有一种更好的方法来方便的检测手势呢?当然有啦,因为这些手势很常用,系统早就封装了一些方法给我们用,接下来我们就看看它们是如何使用的。 14 | 15 | ## GestureDetector 16 | 17 | > GestureDetector 可以使用 MotionEvents 检测各种手势和事件。GestureDetector.OnGestureListener 是一个回调方法,在发生特定的事件时会调用 Listener 中对应的方法回调。这个类只能用于检测触摸事件的 MotionEvent,不能用于轨迹球事件。 18 | > (话说轨迹球已经消失多长时间了,估计很多人都没见过轨迹球这种东西)。 19 | > 20 | > 如何使用: 21 | > 22 | > - 创建一个 GestureDetector 实例。 23 | > - 在onTouchEvent(MotionEvent)方法中,确保调用 GestureDetector 实例的 onTouchEvent(MotionEvent)。回调中定义的方法将在事件发生时执行。 24 | > - 如果侦听 onContextClick(MotionEvent),则必须在 View 的 onGenericMotionEvent(MotionEvent)中调用 GestureDetector OnGenericMotionEvent(MotionEvent)。 25 | 26 | GestureDetector 本身的方法比较少,使用起来也非常简单,下面让我们先看一下它的简单使用示例,分解开来大概需要三个步骤。 27 | 28 | ```java 29 | // 1.创建一个监听回调 30 | SimpleOnGestureListener listener = new SimpleOnGestureListener() { 31 | @Override public boolean onDoubleTap(MotionEvent e) { 32 | Toast.makeText(MainActivity.this, "双击666", Toast.LENGTH_SHORT).show(); 33 | return super.onDoubleTap(e); 34 | } 35 | }; 36 | 37 | // 2.创建一个检测器 38 | final GestureDetector detector = new GestureDetector(this, listener); 39 | 40 | // 3.给监听器设置数据源 41 | view.setOnTouchListener(new View.OnTouchListener() { 42 | @Override public boolean onTouch(View v, MotionEvent event) { 43 | return detector.onTouchEvent(event); 44 | } 45 | }); 46 | ``` 47 | 48 | 接下来我们先了解一下 GestureDetector 里面都有哪些内容。 49 | 50 | ### 1. 构造函数 51 | 52 | GestureDetector 一共有 5 种构造函数,但有 2 种被废弃了,1 种是重复的,所以我们只需要关注其中的 2 种构造函数即可,如下: 53 | 54 | | 构造函数 | 55 | | ---------------------------------------- | 56 | | GestureDetector(Context context, GestureDetector.OnGestureListener listener) | 57 | | GestureDetector(Context context, GestureDetector.OnGestureListener listener, Handler handler) | 58 | 59 | 第 1 种构造函数里面需要传递两个参数,上下文(Context) 和 手势监听器(OnGestureListener),这个很容易理解,就不再过多叙述,上面的例子中使用的就是这一种。 60 | 61 | 第 2 种构造函数则需要多传递一个 Handler 作为参数,这个有什么作用呢?其实作用也非常简单,这个 Handler 主要是为了给 GestureDetector 提供一个 Looper。 62 | 63 | 在通常情况下是不需这个 Handler 的,因为它会在内部自动创建一个 Handler 用于处理数据,如果你在主线程中创建 GestureDetector,那么它内部创建的 Handler 会自动获得主线程的 Looper,然而如果你在一个没有创建 Looper 的子线程中创建 GestureDetector 则需要传递一个带有 Looper 的 Handler 给它,否则就会因为无法获取到 Looper 导致创建失败。 64 | 65 | 第 2 种构造函数使用方式如下(下面是两种在子线程中创建 GestureDetector 的方法): 66 | 67 | ```java 68 | // 方式一、在主线程创建 Handler 69 | final Handler handler = new Handler(); 70 | new Thread(new Runnable() { 71 | @Override public void run() { 72 | final GestureDetector detector = new GestureDetector(MainActivity.this, new 73 | GestureDetector.SimpleOnGestureListener() , handler); 74 | // ... 省略其它代码 ... 75 | } 76 | }).start(); 77 | 78 | // 方式二、在子线程创建 Handler,并且指定 Looper 79 | new Thread(new Runnable() { 80 | @Override public void run() { 81 | final Handler handler = new Handler(Looper.getMainLooper()); 82 | final GestureDetector detector = new GestureDetector(MainActivity.this, new 83 | GestureDetector.SimpleOnGestureListener() , handler); 84 | // ... 省略其它代码 ... 85 | } 86 | }).start(); 87 | ``` 88 | 89 | 当然了,使用其它创建 Handler 的方式也是可以的,重点传递的 Handler 一定要有 Looper,敲黑板,重点是 Handler 中的 Looper。假如子线程准备了 Looper 那么可以直接使用第 1 种构造函数进行创建,如下: 90 | 91 | ```java 92 | new Thread(new Runnable() { 93 | @Override public void run() { 94 | Looper.prepare(); // <- 重点在这里 95 | final GestureDetector detector = new GestureDetector(MainActivity.this, new 96 | GestureDetector.SimpleOnGestureListener()); 97 | // ... 省略其它代码 ... 98 | } 99 | }).start(); 100 | ``` 101 | 102 | ### 2.手势监听器 103 | 104 | 既然是手势检测,自然要在对应的手势出现的时候通知调用者,最合适的自然是事件监听器模式。目前 GestureDetecotr 有四种监听器。 105 | 106 | | 监听器 | 简介 | 107 | | ---------------------------------------- | ---------------------------------------- | 108 | | [OnContextClickListener](https://developer.android.com/reference/android/view/GestureDetector.OnContextClickListener.html) | 这个很容易让人联想到ContextMenu,然而它和ContextMenu并没有什么关系,它是在Android6.0(API 23)才添加的一个选项,是用于检测外部设备上的按钮是否按下的,例如蓝牙触控笔上的按钮,一般情况下,忽略即可。 | 109 | | [OnDoubleTapListener](https://developer.android.com/reference/android/view/GestureDetector.OnDoubleTapListener.html) | 双击事件,有三个回调类型:双击(DoubleTap)、单击确认(SingleTapConfirmed) 和 双击事件回调(DoubleTapEvent) | 110 | | [OnGestureListener](https://developer.android.com/reference/android/view/GestureDetector.OnGestureListener.html) | 手势检测,主要有以下类型事件:按下(Down)、 一扔(Fling)、长按(LongPress)、滚动(Scroll)、触摸反馈(ShowPress) 和 单击抬起(SingleTapUp) | 111 | | [SimpleOnGestureListener](https://developer.android.com/reference/android/view/GestureDetector.SimpleOnGestureListener.html) | 这个是上述三个接口的空实现,一般情况下使用这个比较多,也比较方便。 | 112 | 113 | #### 2.1 OnContextClickListener 114 | 115 | 由于 OnContextClickListener 主要是用于检测外部设备按钮的,关于它需要注意一点,如果侦听 onContextClick(MotionEvent),则必须在 View 的 onGenericMotionEvent(MotionEvent)中调用 GestureDetector 的 OnGenericMotionEvent(MotionEvent)。 116 | 117 | 由于目前我们用到这个监听器的场景并不多,所以也就不展开介绍了,重点关注后面几个监听器。 118 | 119 | #### 2.2 OnDoubleTapListener 120 | 121 | 这个很明显就是用于检测双击事件的,它有三个回调接口,分别是 onDoubleTap、onDoubleTapEvent 和 onSingleTapConfirmed。 122 | 123 | ##### **2.2.1 onDoubleTap 与 onSingleTapConfirmed** 124 | 125 | **如果你只想监听双击事件,那么只用关注 onDoubleTap 就行了,如果你同时要监听单击事件则需要关注 onSingleTapConfirmed 这个回调函数**。 126 | 127 | 有人可能会有疑问,监听单击事件为什么要使用 onSingleTapConfirmed,使用 OnClickListener 不行吗?从理论上是可行的,但是我并不推荐这样使用,主要有两个原因: 128 | 1.它们两个是存在一定冲突的,如果你看过 [事件分发机制详解](http://www.gcssloop.com/customview/dispatch-touchevent-source) 就会知道,如果想要两者同时被触发,则 setOnTouchListener 不能消费事件,如果 onTouchListener 消费了事件,就可能导致 OnClick 无法正常触发。 129 | 2.需要同时监听单击和双击,则说明单击和双击后响应逻辑不同,然而使用 OnClickListener 会在双击事件发生时触发两次,这显然不是我们想要的结果。而使用 onSingleTapConfirmed 就不用考虑那么多了,你完全可以把它当成单击事件来看待,而且在双击事件发生时,onSingleTapConfirmed 不会被调用,这样就不会引发冲突。 130 | 131 | 如果你需要同时监听两种点击事件可以这样写: 132 | 133 | ```java 134 | GestureDetector detector = new GestureDetector(this, new GestureDetector 135 | .SimpleOnGestureListener() { 136 | @Override public boolean onSingleTapConfirmed(MotionEvent e) { 137 | Toast.makeText(MainActivity.this, "单击", Toast.LENGTH_SHORT).show(); 138 | return false; 139 | } 140 | @Override public boolean onDoubleTap(MotionEvent e) { 141 | Toast.makeText(MainActivity.this, "双击", Toast.LENGTH_SHORT).show(); 142 | return false; 143 | } 144 | }); 145 | ``` 146 | 147 | 关于 onSingleTapConfirmed 原理也非常简单,这一个回调函数在单击事件发生后300ms后触发(注意,不是立即触发的),只有在确定不会有后续的事件后,既当前事件肯定是单击事件才触发 onSingleTapConfirmed,所以在进行点击操作时,onDoubleTap 和 onSingleTapConfirmed 只会有一个被触发,也就不存在冲突了。 148 | 149 | 当然,如果你对事件分发机制非常了解的话,随便怎么用都行,条条大路通罗马,我这里只是推荐一种最简单而且不容易出错的实现方案。 150 | 151 | ##### **2.2.2 onDoubleTapEvent** 152 | 153 | **有些细心的小伙伴可能注意到还有一个 onDoubleTapEvent 回调函数,它是干什么的呢?它在双击事件确定发生时会对第二次按下产生的 MotionEvent 信息进行回调。** 154 | 155 | 至于为什么要存在这样的回调,就要涉及到另一个比较细致的问题了,那就是 onDoubleTap 的触发时间,如果你在这些函数被调用时打印一条日志,那么你会看到这样的信息: 156 | 157 | ``` 158 | GCS-LOG: onDoubleTap 159 | GCS-LOG: onDoubleTapEvent - down 160 | GCS-LOG: onDoubleTapEvent - move 161 | GCS-LOG: onDoubleTapEvent - move 162 | GCS-LOG: onDoubleTapEvent - up 163 | ``` 164 | 165 | 通过观察这些信息你会发现它们的调用顺序非常有趣,首先是 onDoubleTap 被触发,之后依次触发 onDoubleTapEvent 的 down、move、up 等信息,为什么说它们有趣呢?是因为这样的调用顺序会引发两种猜想,第一种猜想是 onDoubleTap 是在第二次手指抬起(up)后触发的,而 onDoubleTapEvent 是一种延时回调。第二种猜想则是 onDoubleTap 在第二次手指按下(dowm)时触发,onDoubleTapEvent 是一种实时回调。 166 | 167 | 通过测试和观察源码发现第二种猜想是正确的,因为第二次按下手指时,即便不抬起也会触发 onDoubleTap 和 onDoubleTapEvent 的 down,而且源码中逻辑也表明 onDoubleTapEvent 是一种实时回调。 168 | 169 | 这就引发了另一个问题,双击的触发时间,虽然这是一个细微到很难让人注意到的问题,假如说我们想要在第二次按下抬起后才判定这是一个双击操作,触发后续的内容,则不能使用 onDoubleTap 了,需要使用 onDoubleTapEvent 来进行更细微的控制,如下: 170 | 171 | ```java 172 | final GestureDetector detector = new GestureDetector(MainActivity.this, new GestureDetector.SimpleOnGestureListener() { 173 | @Override public boolean onDoubleTap(MotionEvent e) { 174 | Logger.e("第二次按下时触发"); 175 | return super.onDoubleTap(e); 176 | } 177 | 178 | @Override public boolean onDoubleTapEvent(MotionEvent e) { 179 | switch (e.getActionMasked()) { 180 | case MotionEvent.ACTION_UP: 181 | Logger.e("第二次抬起时触发"); 182 | break; 183 | } 184 | return super.onDoubleTapEvent(e); 185 | } 186 | }); 187 | ``` 188 | 189 | 如果你不需要控制这么细微的话,忽略即可(Logger 是我自己封装的日志库,忽略即可)。 190 | 191 | #### 2.3 OnGestureListener 192 | 193 | 这个是手势检测中较为核心的一个部分了,主要检测以下类型事件:按下(Down)、 一扔(Fling)、长按(LongPress)、滚动(Scroll)、触摸反馈(ShowPress) 和 单击抬起(SingleTapUp)。 194 | 195 | ##### 2.3.1 onDown 196 | 197 | ```java 198 | @Override public boolean onDown(MotionEvent e) { 199 | return true; 200 | } 201 | ``` 202 | 203 | 看过前面的文章应该知道,down 在事件分发体系中是一个较为特殊的事件,为了保证事件被唯一的 View 消费,哪个 View 消费了 down 事件,后续的内容就会传递给该 View。如果我们想让一个 View 能够接收到事件,有两种做法: 204 | 205 | 1、让该 View 可以点击,因为可点击状态会默认消费 down 事件。 206 | 207 | 2、手动消费掉 down 事件。 208 | 209 | 由于图片、文本等一些控件默认是不可点击的,所以我们要么声明它们的 clickable 为 true,要么在发生 down 事件是返回 true。所以 onDown 在这里的作用就很明显了,就是为了保证让该控件能拥有消费事件的能力,以接受后续的事件。 210 | 211 | ##### 2.3.2 onFling 212 | 213 | Failing 中文直接翻译过来就是一扔、抛、甩,最常见的场景就是在 ListView 或者 RecyclerView 上快速滑动时手指抬起后它还会滚动一段时间才会停止。onFling 就是检测这种手势的。 214 | 215 | ```java 216 | @Override 217 | public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float 218 | velocityY) { 219 | return super.onFling(e1, e2, velocityX, velocityY); 220 | } 221 | ``` 222 | 223 | 在 onFling 的回调中共有四个参数,分别是: 224 | 225 | | 参数 | 简介 | 226 | | --------- | ------------------ | 227 | | e1 | 手指按下时的 Event。 | 228 | | e2 | 手指抬起时的 Event。 | 229 | | velocityX | 在 X 轴上的运动速度(像素/秒)。 | 230 | | velocityY | 在 Y 轴上的运动速度(像素/秒)。 | 231 | 232 | 我们可以通过 e1 和 e2 获取到手指按下和抬起时的坐标、时间等相关信息,通过 velocityX 和 velocityY 获取到在这段时间内的运动速度,单位是像素/秒(即 1 秒内滑动的像素距离)。 233 | 234 | 这个我们自己用到的地方比较少,但是也可以帮助我们简单的做出一些有趣的效果,例如下面的这种弹球效果,会根据滑动的力度和方向产生不同的弹跳效果。 235 | 236 | ![](https://ww2.sinaimg.cn/large/006tKfTcgy1fhe10uwa4fg308c0e6wn5.gif) 237 | 238 | 其实这种原理非常简单,简化之后如下: 239 | 240 | 1. 记录 velocityX 和 velocityY 作为初始速度,之后不断让速度衰减,直至为零。 241 | 2. 根据速度和当前小球的位置计算一段时间后的位置,并在该位置重新绘制小球。 242 | 3. 判断小球边缘是否碰触控件边界,如果碰触了边界则让速度反向。 243 | 244 | 根据这三条基本的逻辑就可以做出比较像的弹球效果,[具体的Demo可以看这里](https://raw.githubusercontent.com/GcsSloop/AndroidNote/master/CustomView/Demo/FailingBall.zip)。 245 | 246 | ##### 2.3.3 onLongPress 247 | 248 | 这个是检测长按事件的,即手指按下后不抬起,在一段时间后会触发该事件。 249 | 250 | ```java 251 | @Override 252 | public void onLongPress(MotionEvent e) { 253 | } 254 | ``` 255 | 256 | ##### 2.3.4 onScroll 257 | 258 | onScroll 就是监听滚动事件的,它看起来和 onFaling 比较像,不同的是,onSrcoll 后两个参数不是速度,而是滚动的距离。 259 | 260 | ```java 261 | @Override 262 | public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float 263 | distanceY) { 264 | return super.onScroll(e1, e2, distanceX, distanceY); 265 | } 266 | ``` 267 | 268 | | 参数 | | 269 | | --------- | ----------- | 270 | | e1 | 手指按下时的Event | 271 | | e2 | 手指抬起时的Event | 272 | | distanceX | 在 X 轴上划过的距离 | 273 | | distanceY | 在 Y 轴上划过的距离 | 274 | 275 | ##### 2.3.5 onShowPress 276 | 277 | 它是用户按下时的一种回调,主要作用是给用户提供一种视觉反馈,可以在监听到这种事件时可以让控件换一种颜色,或者产生一些变化,告诉用户他的动作已经被识别。 278 | 279 | 不过这个消息和 onSingleTapConfirmed 类似,也是一种延时回调,延迟时间是 180 ms,假如用户手指按下后立即抬起或者事件立即被拦截,时间没有超过 180 ms的话,这条消息会被 remove 掉,也就不会触发这个回调。 280 | 281 | ```java 282 | @Override 283 | public void onShowPress(MotionEvent e) { 284 | } 285 | ``` 286 | 287 | ##### 2.3.6 onSingleTapUp 288 | 289 | ```java 290 | @Override 291 | public boolean onSingleTapUp(MotionEvent e) { 292 | return super.onSingleTapUp(e); 293 | } 294 | ``` 295 | 296 | 这个也很容易理解,就是用户单击抬起时的回调,但是它和上面的 `onSingleTapConfirmed` 之间有何不同呢?和 `onClick` 又有何不同呢? 297 | 298 | 单击事件触发: 299 | 300 | ```java 301 | GCS: onSingleTapUp 302 | GCS: onClick 303 | GCS: onSingleTapConfirmed 304 | ``` 305 | 306 | | 类型 | 触发次数 | 摘要 | 307 | | -------------------- | ---- | ---- | 308 | | onSingleTapUp | 1 | 单击抬起 | 309 | | onSingleTapConfirmed | 1 | 单击确认 | 310 | | onClick | 1 | 单击事件 | 311 | 312 | 双击事件触发: 313 | 314 | ```java 315 | GCS: onSingleTapUp 316 | GCS: onClick 317 | GCS: onDoubleTap // <- 双击 318 | GCS: onClick 319 | ``` 320 | 321 | | 类型 | 触发次数 | 摘要 | 322 | | -------------------- | ---- | ------------ | 323 | | onSingleTapUp | 1 | 在双击的第一次抬起时触发 | 324 | | onSingleTapConfirmed | 0 | 双击发生时不会触发。 | 325 | | onClick | 2 | 在双击事件时触发两次。 | 326 | 327 | 可以看出来这三个事件还是有所不同的,根据自己实际需要进行使用即可 328 | 329 | #### 2.4 SimpleOnGestureListener 330 | 331 | 这个里面并没有什么内容,只是对上面三种 Listener 的空实现,在上面的例子中使用的基本都是这监听器。因为它用起来更方便一点。 332 | 333 | 这主要是 GestureDetector 构造函数的设计问题,以只监听 OnDoubleTapListener 为例,如果想要使用 OnDoubleTapListener 接口则需要这样进行设置: 334 | 335 | ```java 336 | GestureDetector detector = new GestureDetector(this, new GestureDetector 337 | .SimpleOnGestureListener()); 338 | detector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() { 339 | @Override public boolean onSingleTapConfirmed(MotionEvent e) { 340 | Toast.makeText(MainActivity.this, "单击确认", Toast.LENGTH_SHORT).show(); 341 | return false; 342 | } 343 | 344 | @Override public boolean onDoubleTap(MotionEvent e) { 345 | Toast.makeText(MainActivity.this, "双击", Toast.LENGTH_SHORT).show(); 346 | return false; 347 | } 348 | 349 | @Override public boolean onDoubleTapEvent(MotionEvent e) { 350 | // Toast.makeText(MainActivity.this,"",Toast.LENGTH_SHORT).show(); 351 | return false; 352 | } 353 | }); 354 | ``` 355 | 356 | 既然都已经创建 SimpleOnGestureListener 了,再创建一个 OnDoubleTapListener 显然十分浪费,如果构造函数不使用 SimpleOnGestureListener,而是使用 OnGestureListener 的话,会多出几个无用的空实现,显然很浪费,所以在一般情况下,老老实实的使用 SimpleOnGestureListener 就好了。 357 | 358 | ### 3. 相关方法 359 | 360 | 除了各类监听器之外,与 GestureDetector 相关的方法其实并不多,只有几个,下面来简单介绍一下。 361 | 362 | | 方法 | 摘要 | 363 | | ----------------------- | ---------------------------------------- | 364 | | setIsLongpressEnabled | 通过布尔值设置是否允许触发长按事件,true 表示允许,false 表示不允许。 | 365 | | isLongpressEnabled | 判断当前是否允许触发长按事件,true 表示允许,false 表示不允许。 | 366 | | onTouchEvent | 这个是其中一个重要的方法,在最开始已经演示过使用方式了。 | 367 | | onGenericMotionEvent | 这个是在 API 23 之后才添加的内容,主要是为 OnContextClickListener 服务的,暂时不用关注。 | 368 | | setContextClickListener | 设置 ContextClickListener 。 | 369 | | setOnDoubleTapListener | 设置 OnDoubleTapListener 。 | 370 | 371 | ### 结语 372 | 373 | 关于手势检测部分的 GestureDetector 相关内容基本就这么多了,其实手势检测还有一个 ScaleGestureDetector 也是为手势检测服务的,限于篇幅,本次就讲这么多吧。 374 | 375 | 其实手势检测辅助类 GestureDetector 本身并不是很复杂,带上注释等内容才不到1000行,感兴趣的可以自己研究一下实现方式。 376 | 377 | ## About Me 378 | 379 | ### 作者微博: @GcsSloop 380 | 381 | 382 | 383 | ## 参考资料 384 | 385 | [文档 · GestureDetector ](https://developer.android.com/reference/android/view/GestureDetector.html) 386 | [源码 · GestureDetector](https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/view/GestureDetector.java) -------------------------------------------------------------------------------- /CustomView/Advance/[99]DrawText.md: -------------------------------------------------------------------------------- 1 | # drawText之坐标、居中、绘制多行 2 | 3 | 本文用于讲解Canvas中关于DrawText相关的一些常见问题,如坐标,居中,和绘制多行。 4 | 5 | 之前由于个人的疏忽以及对问题的想当然,没有进行验证,在 [【安卓自定义View进阶 - 图片文字】](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B4%5DCanvas_PictureText.md) 这篇文章中出现了一个错误,有不少眼睛雪亮的网友都发现了该错误并给予了反馈,非常感谢这些网友的帮助。 6 | 7 | ![](http://ww2.sinaimg.cn/large/005Xtdi2jw1f3i3wsruf7j306o06o3yg.jpg) 8 | 9 | ## 错误原因 10 | 11 | 这个错误是drawText方法中坐标的一个问题,就一般的绘图而言,坐标一般都是指定左上角,然而drawText默认并不是左上角,甚至不是大家测试后认为的左下角,而是一个**由Paint中Align决定的对齐基准线**,该API注释原文如下: 12 | 13 | > Draw the text, with origin at (x,y), using the specified paint. **The origin is interpreted based on the Align setting in the paint.** 14 | 15 | 在默认情况下基线如下图所示: 16 | 17 | > PS:基线(BaseLine)有两条,是用户指定的坐标确定的,在默认情况下,基线X在字符串左侧,基线y在字符串下方(但并不是最低的部分)。 18 | 19 | ![](http://ww3.sinaimg.cn/large/005Xtdi2gw1f3jppylp6zj30dw08c74v.jpg) 20 | 21 | ## 分析这样设计的原因 22 | 23 | **Q: 为何基线y要放到文字下面,而不是上面?** 24 | 25 | > A : 既然采用这种对齐方式,必然有其缘由,至少不是为了坑我这种小白。据本人猜测可能有以下原因: 26 | * 1.符合人类书写习惯,不论是汉字还是英文或是其他语言,我们在书写对齐的时候都是以下面为基准线的,而不是上面,(**我们默认的基准线类似于四线格中的第三条线**)。
27 | ![](http://ww4.sinaimg.cn/large/005Xtdi2gw1f3knbp87ttj30dw08cq38.jpg)
28 | * 2.字的特点,字的显示不仅有有中文汉字,还有一些特殊字符,并且大部分是根据下面对齐的,如果把对齐的基线放到上面并使其显示整齐,设计太麻烦,如下:
29 | ![](http://ww1.sinaimg.cn/large/005Xtdi2gw1f3knvptqsrj30dw08cmxw.jpg)
30 | * 3.字体的特点,我们都知道,字有很多的字体风格,不同字体风格的大小是不同的,如果以以上面为基准线而让其底部对齐,这设计难度简直不敢想象:
31 | ![](http://ww3.sinaimg.cn/large/005Xtdi2gw1f3ko9oln9lj30dw08cmxo.jpg)
32 | **综上所述,基线y放到下面不仅符合人的书写习惯,而且更加便于设计。** 33 | 34 | ## drawText从入门到懵逼 35 | 36 | 虽然之前在 [图片文字](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B4%5DCanvas_PictureText.md) 这篇文章中已经简单的了解部分关于文字的方法,但Paint中关于文字的方法还有很多,本文会了解一些我们比较关心的一些内容,例如绘制居中的文本,多行文本等,在此之前我们先了解一下Paint中与文本相关的内部类或者枚举: 37 | 38 | 名称 | 类型 | 主要作用 39 | ---------------|:------:|------------------------------------------------------ 40 | Align | 枚举 | 指定基准线与文本的相对关系(包含 左对齐 右对齐 居中) 41 | Style | 枚举 | 设置样式,但不仅仅是为文本服务(包含 描边 填充 描边加填充) 42 | FontMetrics | 内部类 | 描述给定的文本大小,字体,间距等各种度量值(度量结果类型为float) 43 | FontMetricsInt | 内部类 | 作用同上,但度量结果返回值是int型的 44 | 45 | ### Align 46 | 47 | Align中文意思是对齐,其作用正式如此,我们使用过 Word 的人都知道文本有 *左对齐、居中、右对齐、两端对齐、分散对齐* 五种模式,Align作用就是设置文本的对齐模式,但是并没有这么多,仅有 **左对齐、居中、右对齐** 三种模式,如下: 48 | 49 | > 吐槽:就因为没有两端对齐和分散对齐,导致长文本,尤其是中英混合的长文本在手机上显示效果奇差,相信做阅读类软件的程序员深有体会,最终还要自定义View解决。 50 | 51 | 枚举类型 | 作用 52 | ---------|------------------------------------------------------------- 53 | LEFT | 左对齐 基线X在文本左侧,基线y在文本下方,文本出现在给定坐标的右侧,是默认的对齐方式 54 | RIGHT | 右对齐 基线x在文本右侧,基线y在文本下方,文本出现在给定坐标的左侧 55 | CENTER | 居中对齐 基线x在文本中间,基线y在文本下方,文本出现在给定坐标的两侧 56 | 57 | Align对应的方法如下: 58 | 59 | ``` java 60 | public Paint.Align getTextAlign () // 获取对齐方式 61 | 62 | public void setTextAlign (Paint.Align align) // 设置对齐方式 63 | ``` 64 | 65 | 在实际运用中基线与模式之间的关系则如下图所示: 66 | 67 | ![](http://ww2.sinaimg.cn/large/005Xtdi2gw1f3l2d6gw0nj30dw08c74q.jpg) 68 | 69 | > PS 该图片是截屏加工所得,其中红色的先是各自的基准线。注意汉字有一部分是在基准线下面的。 70 | 71 | 其核心代码如下: 72 | 73 | ``` java 74 | // 平移画布 - 如有疑问请参考画布操作这篇文章 75 | canvas.translate(mCenterX,0); // mCenterX 是屏幕宽度的一半,在onSizeChanged中获取 76 | 77 | // 设置字体颜色 78 | mPaint.setColor(Color.rgb(0x06,0xaf,0xcd)); 79 | 80 | // 在不同模式下绘制文字 81 | mPaint.setTextAlign(Paint.Align.LEFT); 82 | canvas.drawText("左对齐", 0, 200, mPaint); 83 | 84 | mPaint.setTextAlign(Paint.Align.CENTER); 85 | canvas.drawText("居中对齐", 0, 400, mPaint); 86 | 87 | mPaint.setTextAlign(Paint.Align.RIGHT); 88 | canvas.drawText("右对齐", 0, 600, mPaint); 89 | ``` 90 | 91 | ### Style 92 | 93 | Style的意思是样式,这个在 [Canvas之绘制基本形状](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B2%5DCanvas_BasicGraphics.md) 这篇文章中讲过,它有三种状态: 94 | 95 | 枚举类型 | 作用 96 | ----------------|---------------- 97 | FILL | 填充 98 | STROKE | 描边 99 | FILL_AND_STROKE | 填充加描边 100 | 101 | Style 对应的方法如下: 102 | 103 | ``` java 104 | public Paint.Style getStyle () // 获取当然样式 105 | 106 | public void setStyle (Paint.Style style) // 设置样式 107 | ``` 108 | 109 | 效果如下: 110 | 111 | ![](http://ww3.sinaimg.cn/large/005Xtdi2gw1f3nh9r8ih0j30dw08c3z4.jpg) 112 | 113 | 核心代码: 114 | 115 | ``` java 116 | // 平移画布 - 如有疑问请参考画布操作这篇文章 117 | canvas.translate(mCenterX,0); // mCenterX 是屏幕宽度的一半,在onSizeChanged中获取 118 | 119 | mPaint.setTextAlign(Paint.Align.CENTER); 120 | mPaint.setColor(Color.rgb(0x06,0xaf,0xcd)); 121 | 122 | // 设置不同的样式 123 | 124 | // 填充 125 | mPaint.setStyle(Paint.Style.FILL); 126 | canvas.drawText("GcsSloop 中文", 0, 200, mPaint); 127 | 128 | // 描边 129 | mPaint.setStyle(Paint.Style.STROKE); 130 | canvas.drawText("GcsSloop 中文", 0, 400, mPaint); 131 | 132 | // 描边加填充 133 | mPaint.setStyle(Paint.Style.FILL_AND_STROKE); 134 | canvas.drawText("GcsSloop 中文", 0, 600, mPaint); 135 | ``` 136 | 137 | ### FontMetrics 138 | 139 | FontMetrics 是 Paint 的一个内部类,根据名字我们可以大致知道这是一个描述**字体规格**相关的类,关于这个类的描述原文是这样的: 140 | 141 | > Class that describes the various metrics for a font at a given text size. Remember, Y values increase going down, so those values will be positive, and values that measure distances going up will be negative. This class is returned by getFontMetrics(). 142 | 143 | 翻译一下: 144 | 145 | > 简单来说,FontMetrics包含了当前文本相关的各项参数,你可以通过 Paint中 _getFontMetrics()_ 这个方法来获取到这个类。 146 | (另外,原文中还特地强调了一下关于坐标正负的问题。) 147 | 148 | ##### FontMetrics包含了以下几个数值: 149 | 150 | > 如果没有特殊说明,一下文章中“基线”默认代表“基线Y”。 151 | 152 | 名称 | 正负| 释义 153 | --------|:---:|-------------------------------------------------------- 154 | top | - | 在指定字形和大小的情况下,字符最高点与基线之间的距离 155 | ascent | - | 单个字符的最高点与基线距离的推荐值(不包括字母上面的符号) 156 | descent | + | 单个字符的最低点与基线距离的推荐值 157 | bottom | + | 在指定字形和大小的情况下,字符最低点与基线之间的距离 158 | leading | + | 行间距,当前行bottom与下一行top之间的距离的推荐值 (通常为0,因为top与ascent,bottom与leading之间的距离足够作为行间距了) 159 | 160 | ![](http://ww1.sinaimg.cn/large/005Xtdi2gw1f3syr8fbd1j30dw08c755.jpg) 161 | 162 | 看了上面啰啰嗦嗦讲了一堆,你可能会产生一些疑问,这里我简单解释一下: 163 | 164 | 0、 FontMetrics到底有什么用? 165 | 166 | > FontMetrics 是字体规格,比较精确的测量出文字相关的各种数据,在很多地方都是用得到的,比较常见的就是我们的音乐播放器中的歌词显示,需要实时的变字体位置,这里就需要比较精确是数值来进行计算以确保不会出现两行文字重叠等问题。 167 | 168 | 1、为什么表格中最高点距离基线的值是负值,而最低点反而是正值? 169 | 170 | > 这是因为手机屏幕坐标系的特殊性,在数学中常见的坐标系y轴是向上的,而对于手机而言,y轴默认是向下的,所以数值越小越靠近屏幕顶端,即最高点的数值比基线小,最高点减去基线结果自然为负数。 更多参考 [坐标系](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Base/%5B1%5DCoordinateSystem.md) 这篇文章。 171 | 172 | 2、为什么表格中很多都是推荐值? 173 | 174 | > 这是因为字的规格不仅受字大小的影响,而且受字体风格的影响,不同的字体风格差别很大,其结果可能会有偏差,故大部分都是推荐值。 175 | 176 | 3、 具体绘制的文本不同会影响 FontMetrics 中的数值吗? 177 | 178 | > 不会, **FontMetrics中的数值受字体大小(FontSize) 和 字体风格(Typeface) 影响**, 而不会因为具体绘制的内容不同而改变,给Paint设置完字体大小和字体风格后就能获取到正确的FontMetrics。 179 | 180 | **获取FontMetrics的方法** 181 | ``` java 182 | Paint.FontMetrics mMetrics = mPaint.getFontMetrics(); 183 | 184 | Log.e("TAG", "top=" + mMetrics.top); 185 | Log.e("TAG", "ascent=" + mMetrics.ascent); 186 | Log.e("TAG", "descent=" + mMetrics.descent); 187 | Log.e("TAG", "bottom=" + mMetrics.bottom); 188 | Log.e("TAG", "leading=" + mMetrics.leading); 189 | ``` 190 | 结果: 191 | ``` 192 | 05-15 21:18:13.315 13112-13112/com.gcssloop.canvas E/TAG: top=-152.98828 193 | 05-15 21:18:13.315 13112-13112/com.gcssloop.canvas E/TAG: ascent=-129.88281 194 | 05-15 21:18:13.315 13112-13112/com.gcssloop.canvas E/TAG: descent=34.179688 195 | 05-15 21:18:13.315 13112-13112/com.gcssloop.canvas E/TAG: bottom=37.939453 196 | 05-15 21:18:13.315 13112-13112/com.gcssloop.canvas E/TAG: leading=0.0 197 | ``` 198 | 199 | **由于FontMetrics中的 ascent 与 descent 比较常用,所以可以直接通过Paint获取:** 200 | ``` java 201 | Log.e("TAG", "P.ascent=" + mPaint.ascent()); 202 | Log.e("TAG", "P.descent=" + mPaint.descent()); 203 | ``` 204 | 结果,可以看到与通过FontMetrics获得的结果相同。 205 | ``` 206 | 05-15 21:24:18.950 13112-13112/com.gcssloop.canvas E/TAG: P.ascent=-129.88281 207 | 05-15 21:24:18.955 13112-13112/com.gcssloop.canvas E/TAG: P.descent=34.179688 208 | ``` 209 | 210 | 211 | ## 文字居中 212 | 213 | 对于绘制居中的文本来说,我们可以封装一个方法用中心点作为绘制坐标,在绘制的时候转换为实际坐标。 214 | 215 | 根据前面的知识可知,想让文字水平居中很容易,只需要设置 TextAlign 为 CENTER,那么基线X(BaseLineX)自然就是这个字符串的中心了。 216 | 217 | 而让文字垂直居中则有些麻烦了,因为基线Y(BaseLineY)既不是顶部,底部,也不是中心,所以文本居中最难的内容就是计算BaseLineY的位置。 218 | 219 | 我们已知: 220 | 221 | > 1. 中心位置坐标,centerX, centerY 222 | 2. 文本高度:height = descent-ascent 223 | 3. descent的坐标:descentY = centerY + 1/2height 224 | 4. baseLineY坐标:baseLineY = descentY-descent 225 | 226 | 通过上面内容可以推算出如下公式: 227 | 228 | > baseLineY = centerY - 1/2(ascent + descent) 229 | 230 | **根据公式我们可以封装一个方法:** 231 | 232 | ``` java 233 | /** 234 | * 居中绘制文本 235 | * 236 | * @param text 237 | * @param canvas 238 | * @param paint 239 | */ 240 | public void drawTextByCenter(String text, float x, float y, Canvas canvas, Paint paint) { 241 | Paint tempPaint = new Paint(paint); // 创建一个临时画笔,防止影响原来画笔的状态 242 | tempPaint.setTextAlign(Paint.Align.CENTER); // 设置文本对齐方式为居中 243 | 244 | // 通过y计算出baseline的位置 245 | float baseline = y - (tempPaint.descent() + tempPaint.ascent()) / 2; 246 | 247 | canvas.drawText(text, x, baseline, tempPaint); //绘制文本 248 | } 249 | ``` 250 | 251 | **测试方法是否正确** 252 | 253 | ``` java 254 | canvas.translate(mCenterX, mCenterY); 255 | 256 | String text = "ASSA"; 257 | mPaint.setColor(Color.BLACK); 258 | 259 | drawTextByCenter(text, 0, 0, canvas, mPaint); 260 | ``` 261 | 262 | 结果: 263 | 264 | 265 | 266 | ## 绘制多行 267 | 268 | 前面讲的所有绘制文本都是绘制单行文本,而对于长文本就束手无策了,但是我们偶尔也需要绘制一些长文本,例如绘制一段文本,这样前面的就无法满足需求了。 269 | 270 | 我们先思考一下如何才能绘制一段长文本让其可以自动换行,如自动测量文本长度,然后根据测量的内容截开成n行,然后一行一行的绘制。 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | -------------------------------------------------------------------------------- /CustomView/Base/[01]CoordinateSystem.md: -------------------------------------------------------------------------------- 1 | # 安卓中的坐标系 2 | 3 | ### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop) 4 | ### [【本系列相关文章】](http://www.gcssloop.com/1970/01/CustomViewIndex/) 5 | 6 | 7 | ## 一.屏幕坐标系和数学坐标系的区别 8 | 由于移动设备一般定义屏幕左上角为坐标原点,向右为x轴增大方向,向下为y轴增大方向, 9 | 所以在手机屏幕上的坐标系与数学中常见的坐标系是稍微有点差别的,详情如下: 10 | 11 | (**PS:其中的∠a 是对应的,注意y轴方向!**) 12 | 13 | ![](http://ww2.sinaimg.cn/large/005Xtdi2jw1f1qygzfvhoj308c0dwglr.jpg) 14 | ![](http://ww1.sinaimg.cn/large/005Xtdi2jw1f1qyhbqvihj308c0dwjrh.jpg) 15 | 16 | **实际屏幕上的默认坐标系如下:** 17 | 18 | > PS: 假设其中棕色部分为手机屏幕 19 | 20 | ![](http://ww3.sinaimg.cn/large/005Xtdi2jw1f1qyhjy7h8j308c0dwq32.jpg) 21 | 22 | ## 二.View的坐标系 23 | 24 | **注意:View的坐标系统是相对于父控件而言的.** 25 | 26 | ``` java 27 | getTop(); //获取子View左上角距父View顶部的距离 28 | getLeft(); //获取子View左上角距父View左侧的距离 29 | getBottom(); //获取子View右下角距父View顶部的距离 30 | getRight(); //获取子View右下角距父View左侧的距离 31 | ``` 32 | 33 | **如下图所示:** 34 | 35 | ![](http://ww2.sinaimg.cn/large/005Xtdi2gw1f1qzqwvkkbj308c0dwgm9.jpg) 36 | 37 | ## 三.MotionEvent中 get 和 getRaw 的区别 38 | 39 | ``` 40 | event.getX(); //触摸点相对于其所在组件坐标系的坐标 41 | event.getY(); 42 | 43 | event.getRawX(); //触摸点相对于屏幕默认坐标系的坐标 44 | event.getRawY(); 45 | 46 | ``` 47 | 48 | **如下图所示:** 49 | 50 | > PS:其中相同颜色的内容是对应的,其中为了显示方便,蓝色箭头向左稍微偏移了一点. 51 | 52 | ![](http://ww1.sinaimg.cn/large/005Xtdi2jw1f1r2bdlqhbj308c0dwwew.jpg) 53 | 54 | ## 四.核心要点 55 | 56 | 序号 | 要点 57 | :---:|---- 58 | 1 | 在数学中常见的坐标系与屏幕默认坐标系的差别 59 | 2 | View的坐标系是相对于父控件而言的 60 | 3 | MotionEvent中get和getRaw的区别 61 | 62 | ## 五.参考文章: 63 | 64 | [Android 屏幕(View)坐标系统](http://blog.csdn.net/wangjinyu501/article/details/21827341) 65 | 66 | ## About Me 67 | 68 | ### 作者微博: @GcsSloop 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /CustomView/Base/[02]AngleAndRadian.md: -------------------------------------------------------------------------------- 1 | # 角度与弧度 2 | 3 | ### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop) 4 | ### [【本系列相关文章】](http://www.gcssloop.com/1970/01/CustomViewIndex/) 5 | 6 | 安卓中角度(angle)与弧度(radian)的有关问题。 7 | 8 | ## 一.前言 9 | 10 | ### 1.为什么讲这个? 11 | 12 | 在我们自定义View,尤其是制作一些复杂炫酷的效果的时候,实际上是将一些简单的东西通过数学上精密的计算组合到一起形成的效果。 13 | 14 | 这其中可能会涉及到画布的相关操作(旋转),以及一些正余弦函数的计算等,这些内容就会用到一些角度、弧度相关的知识。 15 | 16 | ### 2.为什么对角的描述存在角度与弧度两种单位? 17 | 18 | 简单来说就是为了方便,为了精确描述一个角的大小引入了角度与弧度的概念。 19 | 20 | 由于两者进制是不同的(**角度是60进制,弧度是10进制**),在合适的地方使用合适的单位来描述会更加方便。 21 | 22 | > **例如:** 23 | > 角度是60进位制,遇到30°6′这样的角,应该转化为10进制的30.1°。但弧度就不需要,因为弧度本身就是十进制的实数。 24 | 25 | 26 | ## 二.角度与弧度的定义 27 | 28 | 角度和弧度一样都是描述角的一种度量单位,下面是它们的定义: 29 | 30 | | 名称 | 定义 | 31 | | :--: | ---------------------------------------- | 32 | | 角度 | 两条射线从圆心向圆周射出,形成一个夹角和夹角正对的一段弧。**当这段弧长正好等于圆周长的360分之一时,两条射线的夹角的大小为1度.** | 33 | | 弧度 | 两条射线从圆心向圆周射出,形成一个夹角和夹角正对的一段弧。**当这段弧长正好等于圆的半径时,两条射线的夹角大小为1弧度.** | 34 | 35 | **如图:** 36 | 37 | ![](http://ww1.sinaimg.cn/large/005Xtdi2jw1f1s0f975hmj308c0dwmxh.jpg) 38 | ![](http://ww3.sinaimg.cn/large/005Xtdi2jw1f1s0g3rcg2j308c0dw3yw.jpg) 39 | 40 | 41 | ## 三.角度和弧度的换算关系 42 | **圆一周对应的角度为360度(角度),对应的弧度为2π弧度。** 43 | 44 | **故得等价关系:360(角度) = 2π(弧度) ==> 180(角度) = π(弧度)** 45 | 46 | 由等价关系可得如下换算公式: 47 | 48 | > rad 是弧度, deg 是角度 49 | 50 | | 公式 | 例子 | 51 | | ----------------------- | ------------------ | 52 | | **rad = deg x π / 180** | 2π = 360 x π / 180 | 53 | | **deg = rad x 180 / π** | 360 = 2π x 180 / π | 54 | 55 | 维基百科的公式: 56 | 57 | > rad 是弧度, deg 是角度 58 | 59 | ![](http://ww3.sinaimg.cn/large/005Xtdi2jw1f4hui2jaecj305m03lwee.jpg) 60 | 61 | 62 | ## 四.一些细节问题 63 | 64 | 由于默认屏幕坐标系和常见数学坐标系的小差别([坐标系问题点这里](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Base/%5B1%5DCoordinateSystem.md)),所以在角上必然也会存在一些区别,例如: 65 | 66 | **在常见的数学坐标系中角度增大方向为逆时针,** 67 | 68 | **在默认的屏幕坐标系中角度增大方向为顺时针。** 69 | 70 | ![](http://ww3.sinaimg.cn/large/005Xtdi2jw1f1s2wnsewfj308c0dwt94.jpg) 71 | 72 | ## About Me 73 | 74 | ### 作者微博: @GcsSloop 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /CustomView/Base/[03]Color.md: -------------------------------------------------------------------------------- 1 | # 颜色 2 | 3 | ### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop) 4 | 5 | ### [【本系列相关文章】](http://www.gcssloop.com/1970/01/CustomViewIndex/) 6 | 7 | 简要介绍安卓中的颜色相关内容,包括颜色的定义,创建颜色的几种方式,以及颜色的混合模式等。 8 | 9 | ## 一.简单介绍颜色 10 | 11 | 安卓支持的颜色模式: 12 | 13 | 颜色模式 | 备注 14 | :--- | --- 15 | ARGB8888 | 四通道高精度(32位) 16 | ARGB4444 | 四通道低精度(16位) 17 | RGB565 | **屏幕默认模式**(16位) 18 | Alpha8 | 仅有透明通道(8位) 19 | 20 | *PS:其中字母表示通道类型,数值表示该类型用多少位二进制来描述。如ARGB8888则表示有四个通道(ARGB),每个对应的通道均用8位来描述。* 21 | 22 | *注意:我们常用的是ARGB8888和ARGB4444,而在所有的安卓设备屏幕上默认的模式都是RGB565,请留意这一点。* 23 | 24 | **以ARGB8888为例介绍颜色定义:** 25 | 26 | 类型 | 解释 | 0(0x00) | 255(0xff) 27 | :-------- |:------ | :------ | :-------- 28 | A(Alpha) | 透明度 | 透明 | 不透明 29 | R(Red) | 红色 | 无色 | 红色 30 | G(Green) | 绿色 | 无色 | 绿色 31 | B(Blue) | 蓝色 | 无色 | 蓝色 32 | 33 | *其中 A R G B 的取值范围均为0~255(即16进制的0x00~0xff)* 34 | 35 | A 从0x00到0xff表示从透明到不透明。 36 | 37 | RGB 从0x00到0xff表示颜色从浅到深。 38 | 39 | **当RGB全取最小值(0或0x000000)时颜色为黑色,全取最大值(255或0xffffff)时颜色为白色** 40 | 41 | ## 二.几种创建或使用颜色的方式 42 | 43 | ### 1.java中定义颜色 44 | 45 | ``` java 46 | int color = Color.GRAY; //灰色 47 | ``` 48 | 49 | 由于Color类提供的颜色仅为有限的几个,通常还是用ARGB值进行表示。 50 | 51 | ``` java 52 | int color = Color.argb(127, 255, 0, 0); //半透明红色 53 | 54 | int color = 0xaaff0000; //带有透明度的红色 55 | ``` 56 | 57 | ### 2.在xml文件中定义颜色 58 | 59 | 在/res/values/color.xml 文件中如下定义: 60 | 61 | ``` xml 62 | 63 | 64 | #ff0000 65 | #00ff00 66 | 67 | ``` 68 | 69 | 详解: 在以上xml文件中定义了两个颜色,红色和蓝色,是没有alpha(透明)通道的。 70 | 71 | 定义颜色以‘#’开头,后面跟十六进制的值,有如下几种定义方式: 72 | 73 | ``` java 74 | #f00 //低精度 - 不带透明通道红色 75 | #af00 //低精度 - 带透明通道红色 76 | 77 | #ff0000 //高精度 - 不带透明通道红色 78 | #aaff0000 //高精度 - 带透明通道红色 79 | ``` 80 | 81 | ### 3.在java文件中引用xml中定义的颜色: 82 | 83 | ``` java 84 | int color = getResources().getColor(R.color.mycolor); 85 | 86 | int color = getColor(R.color.myColor); //API 23 及以上支持该方法 87 | ``` 88 | 89 | ### 4.在xml文件(layout或style)中引用或者创建颜色 90 | 91 | ``` xml 92 | 93 | 96 | ``` 97 | 98 | ``` java 99 | android:background="@color/red" //引用在/res/values/color.xml 中定义的颜色 100 | 101 | android:background="#ff0000" //创建并使用颜色 102 | ``` 103 | 104 | ## 三.取色工具 105 | 106 | 颜色都是用RGB值定义的,而我们一般是无法直观的知道自己需要颜色的值,需要借用取色工具直接从图片或者其他地方获取颜色的RGB值。 107 | 108 | ### 1.屏幕取色工具 109 | 110 | 取色调色工具,可以从屏幕取色或者使用调色板调制颜色,非常小而精简。 111 | 112 | **[点击这里获取屏幕取色工具](http://pan.baidu.com/s/1gdWkN0B)** 113 | 114 | ### 2.Picpick 115 | 116 | 功能更加强大的工具:PicPick。 117 | 118 | PicPick具备了截取全屏、活动窗口、指定区域、固定区域、手绘区域功能,支持滚动截屏,屏幕取色,支持双显示器,具备白板、屏幕标尺、直角座标或极座标显示与测量,具备强大的图像编辑和标注功能。 119 | 120 | **[点击这里获取PicPick](http://ngwin.com/picpick)** 121 | 122 | ## 四.颜色混合模式(Alpha通道相关) 123 | 124 | 通过前面介绍我们知道颜色一般都是四个通道(ARGB)的,其中(RGB)控制的是颜色,而A(Alpha)控制的是透明度。 125 | 126 | 因为我们的显示屏是没法透明的,因此最终显示在屏幕上的颜色里可以认为没有Alpha通道。Alpha通道主要在两个图像混合的时候生效。 127 | 128 | 默认情况下,当一个颜色绘制到Canvas上时的混合模式是这样计算的: 129 | 130 | **(RGB通道) 最终颜色 = 绘制的颜色 + (1 - 绘制颜色的透明度) × Canvas上的原有颜色。** 131 | 132 | **注意:** 133 | 134 | 1.这里我们一般把每个通道的取值从0(0x00)到255(0xff)映射到0到1的浮点数表示。 135 | 136 | 2.这里等式右边的“绘制的颜色"、“Canvas上的原有颜色”都是经过预乘了自己的Alpha通道的值。如绘制颜色:0x88ffffff,那么参与运算时的每个颜色通道的值不是1.0,而是(1.0 * 0.5333 = 0.5333)。 (其中0.5333 = 0x88/0xff) 137 | 138 | 使用这种方式的混合,就会造成后绘制的内容以半透明的方式叠在上面的视觉效果。 139 | 140 | 其实还可以有不同的混合模式供我们选择,用Paint.setXfermode,指定不同的PorterDuff.Mode。 141 | 142 | 下表是各个PorterDuff模式的混合计算公式:(D指原本在Canvas上的内容dst,S指绘制输入的内容src,a指alpha通道,c指RGB各个通道) 143 | 144 | 混合模式 | 计算公式 145 | ------- | --------- 146 | ADD | Saturate(S + D) 147 | CLEAR | [0, 0] 148 | DARKEN | [Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)] 149 | DST | [Da, Dc] 150 | DST_ATOP | [Sa, Sa * Dc + Sc * (1 - Da)] 151 | DST_IN | [Sa * Da, Sa * Dc] 152 | DST_OUT | [Da * (1 - Sa), Dc * (1 - Sa)] 153 | DST_OVER | [Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc] 154 | LIGHTEN | [Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)] 155 | MULTIPLY | [Sa * Da, Sc * Dc] 156 | SCREEN | [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc] 157 | SRC | [Sa, Sc] 158 | SRC_ATOP | [Da, Sc * Da + (1 - Sa) * Dc] 159 | SRC_IN | [Sa * Da, Sc * Da] 160 | SRC_OUT | [Sa * (1 - Da), Sc * (1 - Da)] 161 | SRC_OVER | [Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] 162 | XOR | [Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc] 163 | 164 | 用示例图来查看使用不同模式时的混合效果如下(src表示输入的图,dst表示原Canvas上的内容): 165 | 166 | ![](http://ww4.sinaimg.cn/large/005Xtdi2gw1f1wa0f0mzjj30hh0fsjt8.jpg) 167 | 168 | 169 | ## 五.参考资料 170 | 171 | [【安卓图形动画】](http://www.cnblogs.com/zhucai/p/android-graphics-animation.html) 172 | 173 | ## About Me 174 | 175 | ### 作者微博: @GcsSloop 176 | 177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /CustomView/CustomViewRule.md: -------------------------------------------------------------------------------- 1 | # 自定义View基本法 2 | 3 | 我们使用手机,是想要获取某些信息,而 View 是这些信息的直接展示界面,因为信息种类繁多,为了更好的展示这些信息, View 也必须有多种多样,Android 系统本身就给我们提供了不少类型的 View,但有时仍不能满足我们的需要,所以有时可能需要自定义 View 来完成任务。 4 | 5 | 自定义 View 有许多需要注意的地方,关于这些需要注意的内容,我都会整理在这里,其名为《自定义 View 基本法》。 6 | 7 | #### 第一条:尽量避免自定义 View。 8 | 9 | 由于 View 直接承载了与用户交互的重任,所以必须要考虑到各种情况,例如: 10 | 11 | * 当没有设置宽高属性时,View 默认应该多大。 12 | * 横竖屏转换时 View 可能重新设定大小,此时应如何处理。 13 | * View 因为特殊情况被销毁后重建,应如何保存和恢复数据。 14 | 15 | 由于某些情况很特殊,触发条件也特殊,我们简单的实现了一个自定义了一个 View,可能在 99% 的情况下都是正常的,但在某些特殊情况下就会出问题。 16 | 17 | 而系统提供给我们的组件都是经过千锤百炼的,基本上考虑到了各种特殊情况的处理,所以通常情况下,系统提供给我们的组件稳定性要好一些。所以我的建议是,能使用系统提供的组件的尽量使用系统的。 18 | 19 | 除此之外,使用系统提供的组件也方便于其他人快速读懂项目,便于交流。 20 | 21 | #### 第二条:尽量避免从头开始。 22 | 23 | 如果一定要使用自定义 View,那么尽量去继承系统已有的组件,并重写其中的部分方法,不要自己从头开始写。例如:图像相关的 View 可以考虑继承 ImageView,容器类 View 可以考虑继承 LinerLayout,RelativeLayout 等,原因同上。 24 | 25 | #### 第三条:处理特殊情况。 26 | 27 | 针对能想到的一些特殊情况进行处理并且测试,尽量保证自定义 View 能适应各种特殊场景。 28 | 29 | #### 第四条:留下文档。 30 | 31 | **程序员有两大痛苦,1、别人写的项目居然没有文档。2、自己写的项目居然要写文档。** 32 | 33 | 虽然有时候文档写起来确实挺麻烦,但是个人建议要留下一份文档,至少要为自己写的程序添加**有效的注释**,这不仅是方便他人,更重要的是方便自己以后修改项目。 34 | 35 | * 于自定义View而言,首先要标明这个自定义View是解决什么问题,有怎样的效果,使用的场景如何。 36 | * 其次要在关键部位标注实现原理。(例如:显示圆形的ImageView,要标明圆形是如何实现的,使用的是遮罩还是剪裁。) 37 | * 避免无效注释 (例如:在onDraw上面标注绘图),大家都知道这个是绘图,但绘制逻辑才是重点,要去标注绘制逻辑。 38 | 39 | #### 第五条:面向结果编程。 40 | 41 | 我们既然使用自定义View,自然是想要实现一些系统组件无法实现的效果,所以要时刻谨记自己所需要的内容,让其中的所有逻辑都为这个结果服务,我自己实现自定义 View 一般有如下步骤: 42 | 43 | 1. 原型,用我能想到的逻辑实现原型(demo),不管其代码复杂度,首先要得到结果,通常情况下,第一份代码可得性和整洁性都比较差。 44 | 2. 优化,在原型的基础上对代码进行优化,剔除不必要的内容,例如尝试优化逻辑,对与一些重复性的内容抽取函数进行封装,想办法消灭一些中间变量。同时添加上必要注释,让其逻辑更加清晰易懂。 45 | 3. 测试,对其进行场景测试,尽量保证其正常运行。 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /CustomView/Demo/FailingBall.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GcsSloop/AndroidNote/21ba33e3d40c7f462d3c7c90322f4318060fe3ff/CustomView/Demo/FailingBall.zip -------------------------------------------------------------------------------- /CustomView/Demo/PieView.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GcsSloop/AndroidNote/21ba33e3d40c7f462d3c7c90322f4318060fe3ff/CustomView/Demo/PieView.zip -------------------------------------------------------------------------------- /CustomView/Demo/dispatchTouchEventDemo.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GcsSloop/AndroidNote/21ba33e3d40c7f462d3c7c90322f4318060fe3ff/CustomView/Demo/dispatchTouchEventDemo.zip -------------------------------------------------------------------------------- /CustomView/README.md: -------------------------------------------------------------------------------- 1 | # 自定义View系列 2 | 3 | 从零起步,从入门到懵逼的自定义View教程。 4 | 5 | ## 基础篇 6 | 7 |

8 | 9 | 10 | 11 |

12 | 13 | ******* 14 | 15 | ## 进阶篇 16 |

17 | 18 | 19 | 20 |

21 | 22 | ******* 23 | 24 |

25 | 26 | 27 | 28 |

29 | 30 | ******* 31 | 32 |

33 | 34 | 35 | 36 |

37 | 38 | ******* 39 | 40 |

41 | 42 | 43 | 44 |

45 | 46 | ******* 47 | 48 |

49 | 50 | 51 | 52 |

53 | 54 | ### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop) 55 | 56 | 57 | -------------------------------------------------------------------------------- /Lecture/README.md: -------------------------------------------------------------------------------- 1 | # 演讲稿 2 | 3 | * [程序员练级指北(郑州GDG-2016DevFest)](https://github.com/GcsSloop/AndroidNote/blob/master/Lecture/gdg-developer-growth-guide.md) 4 | 5 | -------------------------------------------------------------------------------- /Lecture/gdg-developer-growth-guide.md: -------------------------------------------------------------------------------- 1 | # 程序员练级指北 2 | 3 | ![](http://ww4.sinaimg.cn/large/005Xtdi2jw1fa7c95aispj30rs07qmyp.jpg) 4 | 5 | 之前非常有幸收到 [脉脉不得语](https://github.com/inferjay) 的邀请参加 郑州GDG[^1] 举办的 DevFest[^2] 活动,并上台分享了一下自己的拙见,回来之后我将自己演讲的内容整理了一下,并分享给大家,希望对一些人有帮助,关于 郑州GDG 更多的活动内容,大家可以到 [GDGZhengzou](https://github.com/GDGZhengzhou/Events) 查看。另外,欢迎关注我的 [GitHub](https://github.com/GcsSloop) 和 [微博](http://weibo.com/GcsSloop) 。 6 | 7 | **我在本次活动中的演讲主题是《程序员练级指北》,主要内容如何从零开始,并逐渐成长为一名合格的程序员,里面的内容是基于自身的经历和见解所书写的,并不一定适合所有人,建议选择性采纳。** 8 | 9 | **为了演讲不那么枯燥乏味,我用了很多口语化的描述,并将程序员分了等级,从零装备的新手到钻石级别套装的大佬,其实各个等级之间并没有严格的分界点,只是大致的划分,大家理解便好。另外本文和现场演讲可能稍有差别,下面开始正文。** 10 | 11 | Hello,大家好,我是 GcsSloop,今天是我第一次在这么多陌生人面前装逼,啊不,演讲,心里很是忐忑,现在我还能感受到自己的心扑通扑通的跳。 12 | 13 | 我呢,主要学习 Android,爱好装逼,喜欢钱,目前正在向段子手的方向上越走越远。 14 | 15 | 非常高兴今天能站在这里向大家分享我自己的观点,因为在坐的的技术牛人太多了,作为一个没什么技术实力的新人,不敢在这里班门弄斧讲技术,要不然正讲着哪位大牛突然站起来说我讲的不对,这不就尴尬了么。 16 | 17 | 由于本人爱好装逼,自然也研究了一些心得,所以我今天分享一下程序员如何在不同阶段正确的装逼。 18 | 19 | ## 第一阶段 - 新手村 20 | 21 | ![](http://ww2.sinaimg.cn/large/005Xtdi2jw1fa7a60w9nqj30m80c0gma.jpg) 22 | 23 | 作为编程的新手,自然是不能在大佬面前装逼的,否则分分钟被吊打。这一阶段我们要先给自己定一个小目标,什么是小目标呢?例如挣他个一百万?这对于我们来说当然是不现实的,作为编程菜鸟,小目标当然是稍微努力一下就能完成的,例如:看完几个新手教程,时间不要太长,两三天能完成的任务量就够了。当然了,光看还不行的,要发在朋友圈,让大家觉得你是一个非常有上进心的人。 24 | 25 | **为什么要发在朋友圈呢?装逼是一方面,除此之外,过了两天你就会发现,这两天光顾着打游戏了,新手教程居然忘记看了,这时候去看看朋友圈,再看看上面的赞,心里想如果这要是不弄完以后还如何在朋友面前装逼,自己装的逼跪着也要学完。** 26 | 27 | 这一阶段注意两点: 28 | 29 | 1. **任务周期要短,任务量要小**,如果周期太长,例如一个月,到月底发现弄不完了,就会想反正也弄不完了,就这样吧,容易破罐子破摔。 30 | 2. **要形成周围的监督机制**,如果没人看着,反正也没人知道,那就随便学喽,过不了几天自己就放弃了。 31 | 32 | 如果按照计划执行了第一阶段的任务,那么经过大约一两个月的时间,新手教程基本上就看完了,相当于集齐了一套新手装备,顺利进入下一阶段。 33 | 34 | ## 第二阶段 - 初级套装 35 | 36 | ![](http://ww3.sinaimg.cn/large/005Xtdi2jw1fa7abwlqq8j30m80c0dgm.jpg) 37 | 38 | 初级套装虽然攻击和防御力都很弱,但至少能单挑一些野怪了,有了一定的装逼资本,这时候就要在新手村附近刷一些野怪练级。 39 | 40 | 既然是刷怪升级,自然要选一个怪物比较集中的地方,最好还要有不同等级的怪物,这样方便刷经验,这个地方是哪里呢?个人觉得最适合的地方就是学校OJ题库,不一定要是自己学校的,大部分学校的OJ题库都是开放的,这里面的题目比较多,难度也分了很多等级,是非常适合新手练习的场所。不熟悉OJ系统的建议搜索了解一下。 41 | 42 | **这一阶段非常容易卡关,经常发现自己有两点不会,就是这也不会,那也不会。** 43 | 44 | 然而作为编程界的新手,我们遇到的大部分问题肯定已经有大神踩过坑了,在99%的情况下,用百度或者Google搜索题目描述就能得到攻略方式,但不要偷懒直接复制,要自己亲自写一遍代码,这样有助于自己了解不同类型题目的模式。 45 | 46 | 如果你发现了一道题的答案搜索不到,但是自己解决了,那么恭喜你捡到宝了,说明你遇到了一个稀有的或者新增的怪物,赶紧整理一下攻略过程发博客吧,又能装逼了。 47 | 48 | 这样过了一段时间你就会发现,这些野怪大部分都很弱,只会几种常见的套路而已,除了最基础的题型外,归纳起来就是:堆、栈、树、表、图。 49 | 50 | 每一类野怪都有特定的攻略方法,常用的有:枚举,遍历,分治,贪婪,动态,线性,暴力打表 以及 抖机灵。 51 | 52 | 我在大一的时候加入了学校的打野攻略组,就是传说中的ACM,在里面待了大约有一年的时间,虽然没拿过大奖项,但好在通过刷题把自己的基础巩固了。 53 | 54 | 这一阶段反映自己进步的最好标准就是 做题量 和 排行榜,我最高在校内排到前5,两年没有更新的情况下目前依旧在前50之内,不要问我哪个学校的,是我学校的,看一下OJ排行榜自然能认出我,我在排行榜上也叫GcsSloop。 55 | 56 | 装逼完毕,进入下一阶段。 57 | 58 | ## 第三阶段 - 青铜套装 59 | 60 | ![](http://ww4.sinaimg.cn/large/005Xtdi2jw1fa7afft4xgj30m80c00tc.jpg) 61 | 62 | 刷野怪时间长了就会发现其实也挺没意思的,不过好在能够练装备,把数据结构知识和一些常见的算法弄明白,熟悉敌人的攻击套路,这样经过一段时间后,装备基本也升级到青铜了,这时候就应该尝试去挑战一下区域小boss了。 63 | 64 | 什么是挑战区域小boss呢,就是尝试制作一个完整的项目并发布出去,有人使用即可。 65 | 66 | 挑战小boss可以组团或者单挑,个人虽然喜欢组团,但无奈我们学校这个服务器太弱了,基本找不到合适的队友,尝试了几次组队最终都以失败而告终,无奈只能去单挑。 67 | 68 | 单挑boss要量力而行,不能找太强的boss,根据我自己的经验,单挑太强的boss会死的很难看,自己曾经有无数个项目胎死腹中或者半路夭折。 69 | 70 | 选择一个比较弱的boss,例如:发布一个简单的 app 到应用市场,获取到一定的下载量,或者写一个开源库,获得一点 star。 71 | 72 | **这一阶段锻炼的是项目构建能力,能够完整的构建一个项目并发布出去,有人使用就算成功了。** 73 | 74 | 这样既能带来成就感,又能装逼,发完之后记得告诉身边的人自己有内容发布了,再次装一波。 75 | 76 | ## 第四阶段 - 白银套装 77 | 78 | ![](http://ww4.sinaimg.cn/large/005Xtdi2jw1fa7ajkrs5aj30m80c0751.jpg) 79 | 80 | 单挑完一些区域小boss后,将套装升级为白银,有了一定的装逼资本,此时发现总在朋友圈装逼成就感太差了,这时候可以去论坛、贴吧、社区、微博等地方好好的装一把,让那些装备还没有升级起来的小白看看这套锃亮的白银套装是多么耀眼,不过仍要注意不能太过火,在出来帮助新手解决问题的时候顺便展示一下白银套装的威力就行了,装的太狠容易被大佬发现抓起来吊打,这不就尴尬了么。 81 | 82 | 顺便说一下,如果从培训机构出来的,正常应该是白银初级,自身有基础加上比较努力可能会达到白银中级,能拿到白银套装说明已经达到了职业程序员最基本的标准。 83 | 84 | **很多人打到白银就开始迷茫了,去挑战小boss吧太没成就感,那些看起来就很肥美的中级和高级boss基本已经被大公司瓜分了,自己根本无从插手。所以这时候就要考虑加入公司的问题了。** 85 | 86 | 那些比较好的公司,例如:网易、阿里、腾讯 等肯定有一堆人挤破头都想进,作为一个刚刚拿到白银套装的人想挤进去不能说没有机会,但肯定很难。但作为拥有白银套装的人,想加入一些小公司倒还算比较容易。 87 | 88 | 个人建议初期有条件优先选择比较大的公司,因为这些公司资源多,有成熟的体系,可以帮助我们快速的在某一方向上获得长足的发展。 89 | 90 | 后期则优先选择发展型的小公司,如果是从大公司跳向一个小公司,一般都会担任不错的职位,这可以帮助我们锻炼技术之外的能力,另外一个原因是随着公司成长可以分得一些红利。 91 | 92 | 加入公司自然要考虑自身和公司的关系。 93 | 94 | 个人觉得最核心的一点就是不要依附于公司,也不要抱有敌意,最好的办法就是和公司成为**利益共同体**,在自己为公司创造价值的同时利用公司强大的资源提升自己的能力。 95 | 96 | 如果太过于依附公司,认为自己努力工作一定能得到相应回报的,根据经济学的假设,人都是自私而理性的,老板亦是如此,所以这类人大部分情况下都不会得到预期的结果。 97 | 98 | 对公司抱有敌意,认为自己就是打工赚钱的,想偷懒干最少的活,拿最多的钱,因为在偷懒的同时错失了很多发展机会,所以这类人大部分会随着技术的升级而被淘汰掉。 99 | 100 | **如何与公司成为利益共同体呢?首先要明确自己的目标,自己加入公司是为了提升自己的能力,而不仅仅是来打工赚钱的,想要实现这一目标则是想办法干更多的活,这样老板肯定乐意,而我们通过干更多的活来接触更多的信息,并且把接触到的信息沉淀成为自己的经验积累,对自身发展也是有益的。** 101 | 102 | 当然了,我们也不是傻子,当我们自身产生的价值远远超过公司给我们的薪资并且公司资源已经无法支撑我们继续提升的时候,就该抛弃这家公司跳槽了,这一次跳槽势必能进入一个更好的公司深造,可以利用更庞大的资源来辅助我们发展,接触到更多的信息。在信息时代,信息就是金钱,将信息沉淀成为个人经验并不能第一时间转化为金钱,但是总能在未来的某一时刻爆发出惊人的力量。 103 | 104 | ## 第五阶段 - 黄金套装 105 | 106 | ![](http://ww4.sinaimg.cn/large/005Xtdi2jw1fa7ar2u1gjj30m80c03zb.jpg) 107 | 108 | 在白银阶段,就具备了职业程序员的基本素养,但是离出任CTO,迎娶白富美,走上人生巅峰还差的很远呢,想要实现这些目标,首先要拿到黄金套装。 109 | 110 | 一般来说技术发现有两个方向可走:**深度 和 广度。** 111 | 112 | 我自己使用的是树形型结构,大树的树,**一到两门追求深度,做树木主干,其余的最求广度,做树木枝叶**。 113 | 114 | > 有些人也称为T型知识结构,不过我觉得树形更佳贴切,因为我的所有周边技能都是围绕主干展开的,并且是可生长(拓展)的。 115 | 116 | 我的主方向 Android,自学了 C、Java、Python、Web、Photoshop 等相关知识,除了主方向Android之外,其他方向作为辅助方向,仅仅了解了最基础的内容,也就是能够靠着搜索引擎写一些简单小程序的程度。 117 | 118 | 树型知识架构有很多好处: 119 | 120 | 1. **不被单一语言束缚,在实际场景中可以选择最合适的语言**。 121 | 2. **可以跨领域突破,我在研究 Android 2D 绘图逻辑的时候就借鉴了很多 PS 的知识**。 122 | 3. **预留发展空间大,假设某一天,Android市场饱和了或者前景变差了,我可以快速的从树枝中选择一支比较擅长的发展成为主干,而不用每次都从零开始**。 123 | 124 | 其实我们大部分人都是俗人,是不是黄金套装都无所谓,能赚钱才行,很多人来做技术也是为了赚钱,所以接下来我说的是如何想办法用最小的代价产生最大的价值。想要赚钱首先要明白最基本的商业逻辑,如果我们通过正常的方式从别人那里赚到钱了,说明我们为别人提供了价值,这样别人才愿意付费给我们。 125 | 126 | 所以我们要想办法用最小的代价为别人提供最大的价值,我写代码之前会多思考,没错,就是学生时代老师经常说的多思考。不过那时候没有人告诉我们该思考什么,因此我自己摸索出来了一套东西。 127 | 128 | 1. **思考核心目标**,我们是为了某一件事情而写程序,并不是因为写程序了所以要完成这件事情。**核心目标是完成这件事情,而不是写程序。** 129 | 2. **思考是否可以偷懒**,我是否可以用更简单的方式完成目标,例如**不写代码,少写代码**。 130 | 3. **如果写代码了,是否可以产生附加价值**,例如我写这一段代码是否可以抽取成为一个开源库,是否可以将其中的设计思想整理成文共享出去。 131 | 4. **如何产生更大的价值**,我的开源库或者文章如何才能服务更多人,让更多人了解到我的东西。因为只有服务更多人,才能产生更大的价值,我们也才可能从中拿走一部分利润。 132 | 133 | 经历过这几步的思考,基本上就已经能将所写的每一段代码的价值潜力都发掘出来,进而用最小的力气服务最多的人。当然了,为了别人服务,只是产生了价值,在通常情况下,这部分价值是无法直接提现的,也就是说仅仅产生了价值但自身并没有收益。 134 | 135 | 不过非常值得庆幸的是,今年很有可能成为内容付费元年,今年已经出来了很多内容变现工具,我知道的就有,微博长文打赏,简书打赏,知乎live,diycode原创打赏 和 掘金原创打赏。 136 | 137 | 另外,最重要的是,**随着社会的快速发展,很多人有钱了,开始愿意为自己喜欢的事物打赏,这是一切的基础。** 138 | 139 | 140 | ## 第六阶段 - 钻石套装 141 | 142 | ![](http://ww4.sinaimg.cn/large/005Xtdi2jw1fa7aswevd3j30m80c0gm8.jpg) 143 | 144 | 能拿到钻石套装的人在全球范围内都是屈指可数的,这一类人通常是某一领域的开拓者,就是我们所说的 XXX之父。这些开拓者已经将自己的影响力渗透进了社会的各行各业,说他们推动了社会的进步也是可以的,我有生之年不说集齐套装,仅仅能拿一件钻石武器便足以令我心满意足了。 145 | 146 | ## 结语 147 | 148 | 前面啰嗦了这么多,最后告诉大家一个小秘密,如果你想要研究一个人发展的根本,不要直接去问他,因为别人很少会告诉你自己赖以生存的核心技能,他们只会告诉你看起来很光鲜的皮毛,就是所谓的心灵鸡汤,这些听起来很励志,但落实在自己身上通常没什么用,大多数情况下只会给你带来两三天的激情而已。 149 | 150 | 正确的方法是观察他在做什么事情,以及分析他做这些事情的背后逻辑。如果你能把这些逻辑想清楚了,就把别人真正的核心技能偷过来了。之后根据自身情况一步一步的学着做就行了。 151 | 152 | **所以说,你们如果你们想了解我在做什么,为什么不考虑关注一下我的 [新浪微博](http://weibo.com/GcsSloop) 呢?关注一下又不会吃亏!** 153 | 154 | ### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop) 155 | 156 | 157 | 158 | [^1]: GDG 全称 Google Developer Group,中文意思是 **谷歌开发者社区** 。 159 | 160 | [^2]: DevFest 开发者节,今年(2016)的举办时间是 9月1日 到 11月30日 之间,全球大部分谷歌开发者社区都会举办该活动,一年一次。 161 | 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /OpenGL/README.md: -------------------------------------------------------------------------------- 1 | # OpenGL 2 | 3 | OpenGL 全称 Open Graphics Library,是用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口(API)。OpenGL 常用于CAD、虚拟实境、科学可视化程序和电子游戏开发。 4 | -------------------------------------------------------------------------------- /QuickChart/Bezier.md: -------------------------------------------------------------------------------- 1 | # 贝塞尔曲线常用操作速查表 2 | 3 | > ### 贝塞尔曲线的操作方法均包含在Path中,详情可参考[Path之贝塞尔曲线](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B6%5DPath_Bessel.md) 4 | 5 | > **表格中演示动画均来自维基百科** 6 | 7 | 贝塞尔曲线 | 对应的方法 | 演示动画 8 | :-----------------------:|------------|------------------------------------------------------------------------------ 9 | 一阶曲线
(线性曲线) | lineTo | ![](https://upload.wikimedia.org/wikipedia/commons/0/00/B%C3%A9zier_1_big.gif) 10 | 二阶曲线 | quadTo | ![](https://upload.wikimedia.org/wikipedia/commons/3/3d/B%C3%A9zier_2_big.gif) 11 | 三阶曲线 | cubicTo | ![](https://upload.wikimedia.org/wikipedia/commons/d/db/B%C3%A9zier_3_big.gif) 12 | 四阶曲线 | 无 | ![](https://upload.wikimedia.org/wikipedia/commons/a/a4/B%C3%A9zier_4_big.gif) 13 | -------------------------------------------------------------------------------- /QuickChart/Canvas.md: -------------------------------------------------------------------------------- 1 | # Canvas常用操作速查表 2 | 3 | | 操作分类 | 相关API | 备注 | 4 | | ---------- | ---------------------------------------- | ---------------------------------------- | 5 | | 绘制颜色 | drawColor, drawRGB, drawARGB | 使用单一颜色填充整个画布
**相关链接:**
[【基础-颜色】](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Base/%5B3%5DColor.md)
[【Canvas-颜色与基本形状】](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B2%5DCanvas_BasicGraphics.md) | 6 | | 绘制基本形状 | drawPoint, drawPoints, drawLine, drawLines, drawRect, drawRoundRect, drawOval, drawCircle, drawArc | 依次为 点、线、矩形、圆角矩形、椭圆、圆、圆弧
**相关链接 :**
[【Canvas-颜色与基本形状】](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B2%5DCanvas_BasicGraphics.md) | 7 | | 绘制图片 | drawBitmap, drawPicture | 绘制位图和图片
**相关链接:**
[【Canvas-图片文字】](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B4%5DCanvas_PictureText.md) | 8 | | 绘制文本 | drawText, drawPosText, drawTextOnPath | 依次为 绘制文字、绘制文字时指定每个文字位置、根据路径绘制文字
**相关链接:**
[【Canvas-图片文字】](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B4%5DCanvas_PictureText.md) | 9 | | 绘制路径 | drawPath | 绘制路径,绘制贝塞尔曲线时也需要用到该函数
**相关链接:**
[【Path-基本操作】](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B5%5DPath_Basic.md)
[【Path-贝塞尔曲线】](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B6%5DPath_Bezier.md)
[【Path-完结篇(伪)】](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B7%5DPath_Over.md)
[【Path-Path玩出花样(PathMeasure)】](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B08%5DPath_Play.md) | 10 | | 顶点操作 | drawVertices, drawBitmapMesh | 通过对顶点操作可以使图像形变,drawVertices直接对画布作用、 drawBitmapMesh只对绘制的Bitmap作用 | 11 | | 画布剪裁 | clipPath, clipRect | 设置画布的显示区域 | 12 | | 画布快照 | save, restore, saveLayerXxx, restoreToCount, getSaveCount | 依次为 保存当前状态、 回滚到上一次保存的状态、 保存图层状态、 会滚到指定状态、 获取保存次数
**相关链接:**
[【Canvas-画布操作】](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B3%5DCanvas_Convert.md) | 13 | | 画布变换 | translate, scale, rotate, skew | 依次为 位移、缩放、 旋转、错切
**相关链接:**
[【基础-坐标系】](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Base/%5B1%5DCoordinateSystem.md)
[【基础-角度弧度】](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Base/%5B2%5DAngleAndRadian.md)
[【Canvas-画布操作】](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B3%5DCanvas_Convert.md) | 14 | | Matrix(矩阵) | getMatrix, setMatrix, concat | 实际画布的位移,缩放等操作的都是图像矩阵Matrix,只不过Matrix比较难以理解和使用,故封装了一些常用的方法。
[【Matrix-原理】](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B09%5DMatrix_Basic.md) | 15 | 16 | | 操作类型 | 相关API | 备注 | 17 | | ---------- | ---------------------------------------- | ---------------------------------------- | 18 | | 基础方法 | getDensity, getWidth, getHeight,getDrawFilter,isHardwareAccelerated(API 11),getMaximumBitmapWidth,getMaximumBitmapHeight,getDensity,quickReject,isOpaque,setBitmap,setDrawFilter | 使用单一颜色填充画布 | 19 | | 绘制颜色 | drawColor, drawRGB, drawARGB,drawPaint | 使用单一颜色填充画布 | 20 | | 绘制基本形状 | drawPoint, drawPoints, drawLine, drawLines, drawRect, drawRoundRect, drawOval, drawCircle, drawArc | 依次为 点、线、矩形、圆角矩形、椭圆、圆、圆弧 | 21 | | 绘制图片 | drawBitmap, drawPicture | 绘制位图和图片 | 22 | | 绘制文本 | drawText, drawPosText, drawTextOnPath | 依次为 绘制文字、绘制文字时指定每个文字位置、根据路径绘制文字 | 23 | | 绘制路径 | drawPath | 绘制路径,绘制贝塞尔曲线时也需要用到该函数 | 24 | | 顶点操作 | drawVertices, drawBitmapMesh | 通过对顶点操作可以使图像形变,drawVertices直接对画布作用、 drawBitmapMesh只对绘制的Bitmap作用 | 25 | | 画布剪裁 | clipPath, clipRect, clipRegion,getClipBounds | 画布剪裁相关方法 | 26 | | 画布快照 | save, restore, saveLayer, saveLayerXxx, restoreToCount, getSaveCount | 依次为 保存当前状态、 回滚到上一次保存的状态、 保存图层状态、 回滚到指定状态、 获取保存次数 | 27 | | 画布变换 | translate, scale, rotate, skew | 依次为 位移、缩放、 旋转、错切 | 28 | | Matrix(矩阵) | getMatrix, setMatrix, concat | 实际画布的位移,缩放等操作的都是图像矩阵Matrix,只不过Matrix比较难以理解和使用,故封装了一些常用的方法。 | 29 | -------------------------------------------------------------------------------- /QuickChart/Matrix.md: -------------------------------------------------------------------------------- 1 | # Matrix常用操作速查表 2 | 3 | | 方法类别 | 相关API | 摘要 | 4 | | -------- | ---------------------------------------- | -------------------------- | 5 | | 基本方法 | equals hashCode toString toShortString | 比较、 获取哈希值、 转换为字符串 | 6 | | 数值操作 | set reset setValues getValues | 设置、 重置、 设置数值、 获取数值 | 7 | | 数值计算 | mapPoints mapRadius mapRect mapVectors | 计算变换后的数值 | 8 | | 设置(set) | setConcat setRotate setScale setSkew setTranslate | 设置变换 | 9 | | 前乘(pre) | preConcat preRotate preScale preSkew preTranslate | 前乘变换 | 10 | | 后乘(post) | postConcat postRotate postScale postSkew postTranslate | 后乘变换 | 11 | | 特殊方法 | setPolyToPoly setRectToRect rectStaysRect setSinCos | 一些特殊操作 | 12 | | 矩阵相关 | invert isAffine(API21) isIdentity | 求逆矩阵、 是否为仿射矩阵、 是否为单位矩阵 ... | 13 | 14 | ## 相关文章 15 | 16 | * [安卓自定义View进阶 - Matrix原理](http://www.gcssloop.com/customview/Matrix_Basic/) 17 | * [安卓自定义View进阶 - Matrix详解](http://www.gcssloop.com/customview/Matrix_Method/) 18 | * [安卓自定义View进阶 - Matrix Camera](http://www.gcssloop.com/customview/matrix-3d-camera) 19 | 20 | -------------------------------------------------------------------------------- /QuickChart/Path.md: -------------------------------------------------------------------------------- 1 | # Path常用操作速查表 2 | 3 | > 为了兼容性(_偷懒_) 本表格中去除了在API21(即安卓版本5.0)以上才添加的方法。忍不住吐槽一下,为啥看起来有些顺手就能写的重载方法要等到API21才添加上啊。宝宝此刻内心也是崩溃的。 4 | 5 | 作用 | 相关方法 | 备注 6 | ----------------|-----------------|------------------------------------------ 7 | 移动起点 | moveTo | 移动下一次操作的起点位置 8 | 设置终点 | setLastPoint | 重置当前path中最后一个点位置,如果在绘制之前调用,效果和moveTo相同 9 | 连接直线 | lineTo | 添加上一个点到当前点之间的直线到Path 10 | 闭合路径 | close | 连接第一个点连接到最后一个点,形成一个闭合区域 11 | 添加内容 | addRect, addRoundRect, addOval, addCircle, addPath, addArc, arcTo | 添加(矩形, 圆角矩形, 椭圆, 圆, 路径, 圆弧) 到当前Path (注意addArc和arcTo的区别) 12 | 是否为空 | isEmpty | 判断Path是否为空 13 | 是否为矩形 | isRect | 判断path是否是一个矩形 14 | 替换路径 | set | 用新的路径替换到当前路径所有内容 15 | 偏移路径 | offset | 对当前路径之前的操作进行偏移(不会影响之后的操作) 16 | 贝塞尔曲线 | quadTo, cubicTo | 分别为二次和三次贝塞尔曲线的方法 17 | rXxx方法 | rMoveTo, rLineTo, rQuadTo, rCubicTo | **不带r的方法是基于原点的坐标系(偏移量), rXxx方法是基于当前点坐标系(偏移量)** 18 | 填充模式 | setFillType, getFillType, isInverseFillType, toggleInverseFillType | 设置,获取,判断和切换填充模式 19 | 提示方法 | incReserve | 提示Path还有多少个点等待加入**(这个方法貌似会让Path优化存储结构)** 20 | 布尔操作(API19) | op | 对两个Path进行布尔运算(即取交集、并集等操作) 21 | 计算边界 | computeBounds | 计算Path的边界 22 | 重置路径 | reset, rewind | 清除Path中的内容
**reset不保留内部数据结构,但会保留FillType.**
**rewind会保留内部的数据结构,但不保留FillType** 23 | 矩阵操作 | transform | 矩阵变换 24 | 25 | ## 相关文章 26 | 27 | * [Path基本操作](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B5%5DPath_Basic.md) 28 | * [贝塞尔曲线](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B6%5DPath_Bezier.md) 29 | * [Path完结篇(伪)](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B7%5DPath_Over.md) 30 | * [Path玩出花样](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B8%5DPath_Play.md) 31 | 32 | -------------------------------------------------------------------------------- /QuickChart/README.md: -------------------------------------------------------------------------------- 1 | # 速查表 2 | 3 | 4 | * [Canvas常用操作速查表](https://github.com/GcsSloop/AndroidNote/blob/master/QuickChart/Canvas.md) 5 | * [Path常用操作速查表](https://github.com/GcsSloop/AndroidNote/blob/master/QuickChart/Path.md) 6 | * [Matrix常用操作速查表](https://github.com/GcsSloop/AndroidNote/blob/master/QuickChart/Matrix.md) 7 | * [贝塞尔曲线常用操作速查表](https://github.com/GcsSloop/AndroidNote/blob/master/QuickChart/Bezier.md) 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](http://ww1.sinaimg.cn/large/005Xtdi2jw1f6307cu3krj30rs05kglz.jpg) 2 | 3 | ### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop) 4 | 5 | 我的安卓学习笔记,记录学习过程中遇到的问题,以及我的一些经验总结。如果出现链接失效等情况可以提交 Issues 提醒我修改相关内容。 6 | 7 | > #### PS:点击分类标题可以查看该分类的详细信息。 8 | 9 | ## [博客](http://www.gcssloop.com/#blog "GcsSloop的博客") 10 | 11 | 我的个人博客,在博客中可以获得更好的阅读体验,同时在博客的评论区可以更及时的向我反馈文章中的问题。 12 | 13 | 14 | 15 | ****** 16 | 17 |

18 | 19 | ## [自定义View](https://github.com/GcsSloop/AndroidNote/tree/master/CustomView/README.md) 20 | 21 | * 基础篇 22 | * [安卓自定义View基础 - 坐标系](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Base/%5B01%5DCoordinateSystem.md) 23 | * [安卓自定义View基础 - 角度弧度](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Base/%5B02%5DAngleAndRadian.md) 24 | * [安卓自定义View基础 - 颜色](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Base/%5B03%5DColor.md) 25 | * 进阶篇 26 | * [安卓自定义View进阶 - 分类和流程](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B01%5DCustomViewProcess.md) 27 | * [安卓自定义View进阶 - 绘制基本图形](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B02%5DCanvas_BasicGraphics.md) 28 | * [安卓自定义View进阶 - 画布操作](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B03%5DCanvas_Convert.md) 29 | * [安卓自定义View进阶 - 图片文字](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B04%5DCanvas_PictureText.md) 30 | * [安卓自定义View进阶 - Path基本操作](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B05%5DPath_Basic.md) 31 | * [安卓自定义View进阶 - 贝塞尔曲线](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B06%5DPath_Bezier.md) 32 | * [安卓自定义View进阶 - Path完结篇(伪)](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B07%5DPath_Over.md) 33 | * [安卓自定义View进阶 - Path玩出花样(PathMeasure)](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B08%5DPath_Play.md) 34 | * [安卓自定义View进阶 - Matrix原理](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B09%5DMatrix_Basic.md) 35 | * [安卓自定义View进阶 - Matrix详解](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B10%5DMatrix_Method.md) 36 | * [安卓自定义View进阶 - Matrix Camera](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B11%5DMatrix_3D_Camera.md) 37 | * [安卓自定义View进阶 - 事件分发机制原理](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B12%5DDispatch-TouchEvent-Theory.md) 38 | * [安卓自定义View进阶 - 事件分发机制详解](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B15%5DDispatch-TouchEvent-Source.md) 39 | * [安卓自定义View进阶 - MotionEvent详解](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B16%5DMotionEvent.md) 40 | * [安卓自定义View进阶 - 特殊形状控件事件处理方案](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B17%5Dtouch-matrix-region.md) 41 | * [安卓自定义View进阶 - 多点触控详解](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B18%5Dmulti-touch.md) 42 | * [安卓自定义View进阶 - 手势检测(GestureDetector)](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B19%5Dgesture-detector.md) 43 | * [安卓自定义View进阶 - 缩放手势检测(ScaleGestureDetector)](http://www.gcssloop.com/customview/scalegesturedetector) 44 | * [安卓自定义View进阶 - 画笔基础(Paint)](http://www.gcssloop.com/customview/paint-base) 45 | 46 | 47 | * [ViewSupport - 自定义View工具包](https://github.com/GcsSloop/ViewSupport) 48 | 49 | ****** 50 | 51 | ## 雕虫晓技 52 | 53 | * [雕虫晓技(一) 组件化](http://www.gcssloop.com/gebug/componentr) 54 | * [雕虫晓技(二) 编码](http://www.gcssloop.com/gebug/coding) 55 | * [雕虫晓技(三) 通用圆角布局全解析](http://www.gcssloop.com/gebug/rclayout) 56 | * [雕虫晓技(四) 搭建私有Maven仓库(带容灾备份)](http://www.gcssloop.com/gebug/maven-private) 57 | * [雕虫晓技(五) 网格分页布局源码解析(上) (付费)](https://xiaozhuanlan.com/topic/5841730926) 58 | * [雕虫晓技(六) 网格分页布局源码解析(下) (付费)](https://xiaozhuanlan.com/topic/1456397082) 59 | * [雕虫晓技(七) 用旧Android手机做远程摄像头](http://www.gcssloop.com/gebug/internet-ip-webcam) 60 | * [雕虫晓技(八) Android与数据流的斗争](http://www.gcssloop.com/gebug/android-stream) 61 | * [雕虫晓技(九) Netty与私有协议框架](http://www.gcssloop.com/gebug/netty-private-protocol) 62 | * [雕虫晓技(十) Android超简单气泡效果](http://www.gcssloop.com/gebug/bubble-sample) 63 | 64 | ****** 65 | 66 | ## [教程类](https://github.com/GcsSloop/AndroidNote/tree/master/Course/README.md) 67 | 68 | * [在AndroidStudio中使用PlantUML(Win)](https://github.com/GcsSloop/AndroidNote/blob/master/Course/HowToUsePlantUMLInAS.md) 69 | * [在AndroidStudio中使用PlantUML(Mac)](https://github.com/GcsSloop/AndroidNote/blob/master/Course/HowToUsePlantUMLInAS%5BMac%5D.md) 70 | * [优雅的发布Android开源库(论JitPack的优越性)](https://github.com/GcsSloop/AndroidNote/blob/master/Course/ReleaseLibraryByJitPack.md) 71 | * [用JitPack发布时附加文档和源码](https://github.com/GcsSloop/AndroidNote/blob/master/Course/jitpack-javadoc.md) 72 | 73 | ****** 74 | 75 | ## [Markdown](https://github.com/GcsSloop/AndroidNote/tree/master/Course/Markdown) 76 | 77 | * [Markdown 快速入门](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-start.md) 78 | * [Markdown 基础语法](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-grammar.md) 79 | * [Markdown 链接图片](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-link.md) 80 | * [Markdown 编辑器](https://github.com/GcsSloop/AndroidNote/blob/master/Course/Markdown/markdown-editor.md) 81 | 82 | ****** 83 | 84 | ## Tips 85 | 86 | * [ArrayList与LinkedList遍历性能比较](https://github.com/GcsSloop/AndroidNote/blob/magic-world/ChaosCrystal/List%E9%81%8D%E5%8E%86%E6%80%A7%E8%83%BD.md) 87 | * [程序员不可不知的版权协议](https://github.com/GcsSloop/AndroidNote/blob/magic-world/ChaosCrystal/%E5%BC%80%E6%BA%90%E5%85%B1%E4%BA%AB%E5%8D%8F%E8%AE%AE.md) 88 | 89 | ****** 90 | 91 | ## [速查表](https://github.com/GcsSloop/AndroidNote/tree/master/QuickChart/README.md) 92 | 93 | * [Canvas常用操作速查表](https://github.com/GcsSloop/AndroidNote/blob/master/QuickChart/Canvas.md) 94 | * [Path常用操作速查表](https://github.com/GcsSloop/AndroidNote/blob/master/QuickChart/Path.md) 95 | * [Matrix常用操作速查表](https://github.com/GcsSloop/AndroidNote/blob/master/QuickChart/Matrix.md) 96 | * [贝塞尔曲线常用操作速查表](https://github.com/GcsSloop/AndroidNote/blob/master/QuickChart/Bezier.md) 97 | 98 | ****** 99 | 100 | ## [混沌水晶](https://github.com/GcsSloop/AndroidNote/tree/master/ChaosCrystal/README.md) 101 | 102 | 混沌水晶本身并没有太大功效,但与其他物品合成之后可能产生质的变化。 103 | 104 | * [Android中dip、dp、sp、pt和px](https://github.com/GcsSloop/AndroidNote/blob/master/ChaosCrystal/Android%E4%B8%ADdip%E3%80%81dp%E3%80%81sp%E3%80%81pt%E5%92%8Cpx.md) 105 | * [AndroidStudio常用快捷键(Mac)](https://github.com/GcsSloop/AndroidNote/blob/master/ChaosCrystal/AndroidStudio%E5%B8%B8%E7%94%A8%E5%BF%AB%E6%8D%B7%E9%94%AE(Mac).md) 106 | * [在线查看Android API源码](https://github.com/GcsSloop/AndroidNote/blob/master/ChaosCrystal/HowToViewAPISourceOnline.md) 107 | * [录屏与GIF制作](https://github.com/GcsSloop/AndroidNote/blob/master/ChaosCrystal/%E5%BD%95%E5%B1%8F%E4%B8%8EGIF%E5%88%B6%E4%BD%9C.md) 108 | * [ADB常用命令](https://github.com/GcsSloop/AndroidNote/blob/master/ChaosCrystal/ADB%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4.md) 109 | 110 | ****** 111 | 112 | ## 开源库 113 | 114 | * [arc-seekbar - 弧形SeekBar](https://github.com/GcsSloop/arc-seekbar) 115 | * [encrypt - 加密工具包](https://github.com/GcsSloop/encrypt) 116 | * [rclayout - 通用圆角布局](https://github.com/GcsSloop/rclayout) 117 | * [FontsManager - 快速替换字体](https://github.com/GcsSloop/FontsManager) 118 | * [Rocker - 自定义摇杆](https://github.com/GcsSloop/Rocker) 119 | * [LeafLoading - 进度条](https://github.com/GcsSloop/LeafLoading) 120 | * [Rotate3dAnimation - 3D旋转动画(修正版)](https://github.com/GcsSloop/Rotate3dAnimation) 121 | 122 | ------ 123 | 124 | ## 源码解析 125 | 126 | - [AtomicFile 源码解析](https://github.com/GcsSloop/AndroidNote/blob/master/SourceAnalysis/AtomicFile.md) 127 | 128 | ## 传送门 129 | 130 | 通往异世界的传送门,请谨慎使用。 131 | 132 | 时空折跃准备完毕,[点击开始传送](https://github.com/GcsSloop/AndroidNote/tree/magic-world)。 133 | 134 | ***** 135 | 136 | ## 博文修复计划 137 | 138 | 由于自己的知识水平有限,书写的文章难免会出现一些问题。 139 | 140 | 随着知识增长和知识面的扩大,发现了一些之前未曾注意到的问题,故有此博文修复计划,本次修复不仅会修复之前文章中的瑕疵和纰漏,甚至会对文章的知识结构稍作调整,所有修复的文章和调整后的内容都会在微博重新发布声明,点击下面关注我的微博可以第一时间了解到相关信息,另外据说关注我的微博会变帅哦。 141 | 142 | **本次博文修复计划主要针对 [个人博客](http://www.gcssloop.com/#blog) 和 [GitHub](https://github.com/GcsSloop),由于博主精力有限以及某些平台自身限制,对于其他平台仅能修复部分内容。** 143 | 144 | ****** 145 | 146 | ## 版权声明 147 | 148 | * 所有原创文章(未进行特殊标识的均属于原创) 的著作权属于 **GcsSloop**。 149 | * 所有转载文章(标题注明`[转]`的所有文章) 的著作权属于原作者。 150 | * 所有译文文章(标题注明`[译]`的所有文章) 的原文著作权属于原作者,译文著作权属于 **GcsSloop**。 151 | 152 | #### 转载注意事项 153 | 154 | 除注明外,所有文章均采用 [Creative Commons BY-NC-ND 4.0(自由转载-保持署名-非商用-禁止演绎)](http://creativecommons.org/licenses/by-nc-nd/4.0/deed.zh)协议发布。 155 | 156 | 你可以在非商业的前提下免费转载,但同时你必须: 157 | 158 | * 保持文章原文,不作修改。 159 | * 明确署名,即至少注明 `作者:GcsSloop` 字样以及文章的原始链接,且不得使用 `rel="nofollow"` 标记。 160 | * 商业用途请点击最下面图片联系本人。 161 | * 微信公众号转载一律不授权 `原创` 标志。 162 | 163 | ## 捐赠 164 | 165 | #### 如果你觉得我的文章有帮助的话,捐赠一些晶石,鼓励我继续研究! 🐾 166 | 167 | | | | 168 | | ---------------------------------------- | ---------------------------------------- | 169 | | ![](http://www.gcssloop.com/assets/images/wechat.png) | ![](http://www.gcssloop.com/assets/images/alipay.png) | 170 | 171 | ## 交流群 172 | 173 | QQ群:612310796 174 | 微信群:加我个人微信 GcsSloop,备注加群。 175 | ![](https://ww3.sinaimg.cn/large/006tNc79gy1fl8bemmtmxj30p005k406.jpg) 176 | 177 | ### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop) 178 | 179 | 180 | 181 | [▲ 回到顶部](#top) 182 | -------------------------------------------------------------------------------- /SourceAnalysis/AtomicFile.md: -------------------------------------------------------------------------------- 1 | # AtomicFile 源码解析 2 | 3 | ![](http://ww4.sinaimg.cn/large/005Xtdi2jw1faa9dj0nf7j30rs05kaaf.jpg) 4 | 5 | ## 什么是 AtomicFile ? 6 | 7 | AtomicFile 在 `android.support.v4.util` 包下,**是一个与文件相关的工具类,其作用是保证文件读写的原子性。** 即文件读写的时候全部成功才会更新文件,如果失败则不会影响文件内容。 8 | 9 | 看官方对其说明: 10 | 11 | > Static library support version of the framework's `AtomicFile`, a helper class for performing atomic operations on a file by creating a backup file until a write has successfully completed. 12 | > 13 | > 静态支持库版本的AtomicFile,一个帮助类,用于通过创建备份文件对文件执行原子操作,直到写入成功完成。 14 | > 15 | > Atomic file guarantees file integrity by ensuring that a file has been completely written and sync'd to disk before removing its backup. As long as the backup file exists, the original file is considered to be invalid (left over from a previous attempt to write the file). 16 | > 17 | > 原子文件通过确保文件在删除其备份之前已经完全写入并同步到磁盘,从而保证文件的完整性。只要备份文件存在,原始文件将被视为无效(会尝试写入备份文件中)。 18 | > 19 | > Atomic file does not confer any file locking semantics. Do not use this class when the file may be accessed or modified concurrently by multiple threads or processes. The caller is responsible for ensuring appropriate mutual exclusion invariants whenever it accesses the file. 20 | > 21 | > 原子文件不提供任何文件锁定语义。当文件可能被多个线程或进程并发访问或修改时,不要使用此类。每当访问文件时,调用者都负责确保适当的互斥变量。 22 | 23 | ## 主要方法 24 | 25 | | 返回值 | 方法名称和简介 | 26 | | :------------------: | ---------------------------------------- | 27 | | | **AtomicFile(File baseName)**
构造方法,通过File创建AtomicFile | 28 | | **FileOutputStream** | **startWrite()**
准备写文件,获得一个向文件写入的字节流(FileOutputStream)。 | 29 | | **void** | **finishWrite(FileOutputStream str)**
写入完毕时调用。 | 30 | | **void** | **failWrite(FileOutputStream str)**
写入失败时调用。 | 31 | | **FileInputStream** | **openRead()**
准备读取文件,获得一个从文件读取的字节流(FileInputStream)。 | 32 | | **byte[]** | **readFully()**
将文件中所有内容读取出来用 byte 数组返回,相当于更方便的 openRead 方法。 | 33 | | **File** | **getBaseFile()**
获得原文件地址。 | 34 | | **void** | **delete()**
删除 AtomicFile,包括原文件和备份文件。 | 35 | 36 | ## AtomicFile 原理 37 | 38 | AtomicFile 原理其实非常简单,下面用一张图简单展示一下其原理: 39 | 40 | ![](http://ww3.sinaimg.cn/large/005Xtdi2jw1faaeg4kqo5j30lu0tegp5.jpg) 41 | 42 | ## AtomicFile 源码解析 43 | 44 | 源码解析非常简单,根据上面表格中的内容按顺序一个一个看吧。 45 | 46 | 47 | 48 | #### 构造函数: 49 | 50 | 根据传进来的文件创建 AtomicFile 类,同时创建辅助备份文件(空文件)。 51 | 52 | ```java 53 | private final File mBaseName; 54 | private final File mBackupName; 55 | 56 | public AtomicFile(File baseName) { 57 | mBaseName = baseName; // 记录原始文件 58 | mBackupName = new File(baseName.getPath() + ".bak"); // 创建备份文件 59 | } 60 | ``` 61 | 62 | 63 | 64 | #### 开始写入(startWrite) 65 | 66 | 在获取向文件写入的输出流之前,它会对原文件进行备份,同时清除原文件内容。 67 | 68 | 同时根据其原理可知,它并不是线程安全的,如果需要多线程操作等,最好自己加锁,如果一个线程未写完,直接开启了另一个线程进行写入可能会导致文件内容丢失。 69 | 70 | **另外使用这种方法获得到的 FileOutputStream 不要直接关闭,写入完成的时候需要调用 `finishWrite` 或者`failWrite` 进行关闭,否则下次读取的时候会因为备份文件存在而使本次写入失效。** 71 | 72 | ```java 73 | public FileOutputStream startWrite() throws IOException { 74 | // 当原文件存在,备份文件不存在的时候,原文件更名为备份文件 75 | if (mBaseName.exists()) { 76 | if (!mBackupName.exists()) { 77 | // 如果原文件存在且备份文件不存在,直接将原文件重命名为备份文件 78 | if (!mBaseName.renameTo(mBackupName)) { 79 | Log.w("AtomicFile", "Couldn't rename file " + mBaseName 80 | + " to backup file " + mBackupName); 81 | } 82 | } else { 83 | mBaseName.delete(); // 删除原文件 84 | } 85 | } 86 | // 保证原文件存在,并根据原文件创建一个输出流。 87 | FileOutputStream str = null; 88 | try { 89 | str = new FileOutputStream(mBaseName); 90 | } catch (FileNotFoundException e) { 91 | File parent = mBaseName.getParentFile(); 92 | if (!parent.mkdirs()) { 93 | throw new IOException("Couldn't create directory " + mBaseName); 94 | } 95 | FileUtils.setPermissions( 96 | parent.getPath(), 97 | FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH, 98 | -1, -1); 99 | try { 100 | str = new FileOutputStream(mBaseName); 101 | } catch (FileNotFoundException e2) { 102 | throw new IOException("Couldn't create " + mBaseName); 103 | } 104 | } 105 | return str; // 返回输出流。 106 | } 107 | ``` 108 | 109 | 110 | 111 | #### 写入完成(finishWrite) 112 | 113 | 写入完成的时候需要用户调用该方法,该方法会关闭 FileOutputStream ,并且会删除备份文件以保证文件的唯一性。 114 | 115 | ```java 116 | public void finishWrite(FileOutputStream str) { 117 | if (str != null) { 118 | FileUtils.sync(str); 119 | try { 120 | str.close(); 121 | mBackupName.delete(); 122 | } catch (IOException e) { 123 | Log.w("AtomicFile", "finishWrite: Got exception:", e); 124 | } 125 | } 126 | } 127 | ``` 128 | 129 | 130 | 131 | #### 写入失败(failWrite) 132 | 133 | 写入失败时会调用该方法,该方法会关闭 FileOutputStream,并且从备份文件恢复文件内容。 134 | 135 | ```java 136 | public void failWrite(FileOutputStream str) { 137 | if (str != null) { 138 | FileUtils.sync(str); 139 | try { 140 | str.close(); 141 | mBaseName.delete(); 142 | mBackupName.renameTo(mBaseName); 143 | } catch (IOException e) { 144 | Log.w("AtomicFile", "failWrite: Got exception:", e); 145 | } 146 | } 147 | } 148 | ``` 149 | 150 | 151 | 152 | #### 准备读取(openRead) 153 | 154 | 该方法会获取到一个读取文件的输入流(FileInputStream),并且在备份文件存在时,会简单粗暴的删除原文件,并从备份文件恢复内容。 155 | 156 | ```java 157 | public FileInputStream openRead() throws FileNotFoundException { 158 | if (mBackupName.exists()) { 159 | mBaseName.delete(); 160 | mBackupName.renameTo(mBaseName); 161 | } 162 | return new FileInputStream(mBaseName); 163 | } 164 | ``` 165 | 166 | 167 | 168 | #### 读取全部(readFully) 169 | 170 | 该方法会将文件中的所有内容转化为一个 byte 数组并且返回,所以不建议使用该方法读取大文件。 171 | 172 | ```java 173 | public byte[] readFully() throws IOException { 174 | FileInputStream stream = openRead(); 175 | try { 176 | int pos = 0; 177 | int avail = stream.available(); 178 | byte[] data = new byte[avail]; 179 | // 通过循环读取的方式将文件所有内容都装入 byte 数组中。 180 | while (true) { 181 | int amt = stream.read(data, pos, data.length-pos); 182 | //Log.i("foo", "Read " + amt + " bytes at " + pos 183 | // + " of avail " + data.length); 184 | if (amt <= 0) { 185 | //Log.i("foo", "**** FINISHED READING: pos=" + pos 186 | // + " len=" + data.length); 187 | return data; 188 | } 189 | pos += amt; 190 | avail = stream.available(); 191 | if (avail > data.length-pos) { 192 | byte[] newData = new byte[pos+avail]; 193 | System.arraycopy(data, 0, newData, 0, pos); 194 | data = newData; 195 | } 196 | } 197 | } finally { 198 | stream.close(); 199 | } 200 | } 201 | ``` 202 | 203 | 204 | 205 | #### 获取原文件(getBaseFile) 206 | 207 | 该方法用于获取原文件路径,但通常情况下并不推荐使用,因为获取到的原文件可能是损坏的或者无效的。 208 | 209 | ```java 210 | public File getBaseFile() { 211 | return mBaseName; 212 | } 213 | ``` 214 | 215 | 216 | 217 | #### 删除(delete) 218 | 219 | 该方法会直接删除原文件和备份文件。 220 | 221 | ```java 222 | public void delete() { 223 | mBaseName.delete(); 224 | mBackupName.delete(); 225 | } 226 | ``` 227 | 228 | 229 | 230 | ## 简单示例 231 | 232 | 233 | 234 | ## FAQ 235 | 236 | **Q:AtmoicFile 适用于哪些文件?** 237 | 238 | > A:**AtomicFile 适用一次性写入的文件**。根据 AtomicFile 原理可知,每一次获取写入文件的输出流的时候都会清空原文件的内容,所以是无法给文件追加内容的。 239 | 240 | **Q:AtmoicFile 是否是线程安全的?** 241 | 242 | > A:根据其原理就可知它没有带线程锁,所以**AtomicFile并不能保证线程安全**。 243 | 244 | **Q:文件写入完毕后可以直接关闭输出字节流(FileOutputStream)么?** 245 | 246 | > A:AtomicFile写入完毕后是不允许直接关闭字节流的,因为直接关闭字节流会导致备份文件没有删除,因而下次读取的时候会导致读取到的是原文件,而不是更新后的文件。 247 | > **写入完毕后应调用 finishWrite(正常写入完成) 或者 failWrite(写入失败)。** 248 | 249 | **Q:文件读取完毕后可以直接关闭输入字节流(FileInputStream)么?** 250 | 251 | > A:可以。 252 | 253 | 254 | 255 | ## 结语 256 | 257 | AtomicFile 作为一个工具类,有其方便之处,同时也有一些需要注意的地方,例如不能追加内容,另外,AtomicFile 除了上述方法之外,还有另外几个方法没有在本文中说明,这几个方法由于不安全,已经被标注删除和隐藏,故本文就不在赘述了。 258 | 259 | 事实上 AtomicFile 逻辑非常简单,应用场景也有限,如果需要的话自己徒手写一个也是可以的,个人觉得它的作用并不太大,如果以后大家在源码或者其他地方见到了这个类,知道它是干什么的就行了。 260 | 261 | **最后,任何工具都是双刃剑,用好了伤人,用不好伤己,希望大家用之前好好了解一下其利弊,权衡之后再做决定。** 262 | 263 | 264 | 265 | ## 参考资料 266 | 267 | [Guide - AtomicFile](https://developer.android.com/reference/android/support/v4/util/AtomicFile.html) 268 | [源码 - AtomicFile](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/util/AtomicFile.java) 269 | 270 | 271 | 272 | ## About Me 273 | 274 | ### 作者微博: @GcsSloop 275 | 276 | -------------------------------------------------------------------------------- /SourceAnalysis/CircularArray.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GcsSloop/AndroidNote/21ba33e3d40c7f462d3c7c90322f4318060fe3ff/SourceAnalysis/CircularArray.md -------------------------------------------------------------------------------- /SourceAnalysis/README.md: -------------------------------------------------------------------------------- 1 | # 源码解析 2 | 3 | * [AtomicFile 源码解析](https://github.com/GcsSloop/AndroidNote/blob/master/SourceAnalysis/AtomicFile.md) 4 | --------------------------------------------------------------------------------