Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all 5930 articles
Browse latest View live

流媒体开发 HLS草案

$
0
0
目录
1 简介 2
2 概述 2
3 播放列表文件 3
3.1 介绍 3
3.2新标签 4
3.2.1 EXT-X-TARGETDURATION 4
3.2.2 EXT-X-MEDIA-SEQUENCE 4
3.2.3 EXT-X-KEY 4
3.2.4 EXT-X-PROGRAM-DATE-TIME 5
3.2.5 EXT-X-ALLOW-CATCH 5
3.2.6 EXT-X-ENDLIST 5
3.2.7 EXT-X-STREAM-INF 5
3.2.8 EXT-X-DISCONTINUITY 6
3.2.9 EXT-X-VERSION 6
4 多媒体文件 7
5 密钥文件 7
5.1 介绍 7
5.2  IV FOR AES-128 7
6 客户端/服务器行为 8
6.1 介绍 8
6.2 服务器进程 8
6.2.1介绍 8
6.2.2 滑动窗口播放列表 9
6.2.3 加密媒体文件 9
6.2.4 提供变种数据流 10
6.3 客户端进程 10
6.2.1 介绍 10
6.2.2 加载播放列表文件 11
6.2.3播放播放列表文件 11
6.2.4重新载入播放列表文件 11
6.2.5 确定下一个要加载的文件 12
6.2.6 解密经加密的媒体文件 12
7 协议版本的兼容性 12
8 例子 12
8.1 简单的播放列表文件 12
8.2 滑动窗口播放列表,使用https 13
8.3 加密的媒体文件与播放列表文件 13
8.4 变种的播放列表文件 13
 
 
1 简介
本文档介绍了通过HTTP传输极大的多媒体数据流的协议[RFC2616]。该协议支持媒体数据的加密,并提供流的备用版本(如比特率)。媒体数据可以在创建后被很快地传输,允许它在近实时被接收。
在第11章中列出了,如HTTP的,描述相关标准的外部引用。
 
2 概述
多媒体演示文稿是由播放列表文件中的URI指定的,播放列表是一个由uri和信息标签组成的有序列表。每一个URI都关联了一个媒体文件,该媒体文件是一个连续数据流的一个分片。
为了播放数据流,客户端首先获取播放列表文件,然后获取并播放列表中的每一个媒体文件。正如本文档所描述的那样,它通过重载播放列表文件来发现其他新增的分片。
文档中的关键词“必须”“不准”,“需要”“应该”“不应该”“推荐”“可以”“可选”等见RFC2119。
 
3 播放列表文件
3.1介绍
播放列表必须是扩展的M3U文件,该文档通过定义新的标签扩展了m3u文件的格式。M3U播放列表是一个文本文件,它包含了各自独立的行,行以一个LF字符或者LF字符紧跟一个CR字符来结束。行可以是一个URI,空行,或者以字符#开头。空行将会被忽略。空格只能作为一行中不同元素间的分隔。
一个URI 表示一个媒体文件或是变种播放列表文件(见3.2.7)
    URI可以是相对的,一个相对的URI必须被包含该URI的播放列表文件中的URI所解析。
以注释字符#开头的行可能是注释或者标签,标签以#EXT开头,其他所有行都应该被忽略。播放列表文件的持续时间是他所指向的媒体文件的时长的总和。
以.M3U8作为文件名后缀或者HTTPContent-Type(RFC2616)为“Application/vnd.apple.mpegurl”的M3U播放列表文件使用UTF-8(RFC3629)编码。以.M3U作为文件名后缀或者HTTPContent-Type为“audio/mpegurl”的M3U播放列表文件使用US-ASCII编码。
播放列表文件名必须以.M3U8为后缀、HTTPContent-Type为“Application/vnd.apple.mpegurl”(如果使用http传输)或者以.M3U为后缀、HTTPContent-Type为“audio/mpegurl”。
扩展的M3U文件格式定义了两种标签:EXTM3U和EXTINF。区分扩展的M3U文件与普通M3U文件的关键在于前者的首行为#EXTM3U。
EXTINF是一个记录标记,该标记描述了后边URI所指定的媒体文件。每个媒体文件URI前边必须有EXTINF标签。格式如下:
#EXTINF: <DURATION>,<TITLE>
DURATION是一个整数,它指定了媒体文件以秒为单位的持续时间,时间应四舍五入到最接近的整数。行内逗号后边的剩余部分是媒体文件的名字,该名字是媒体分片的人眼可读的信息标题。
该文档定义了如下的新标签:EXT-X-TARGETDURATION,EXT-X-MEDIA-SEQUENCE,EXT-X-KEY,EXT-X-PROGRAM-DATE-TIME,EXT-X-ALLOW-CATCH,EXT-X-ENDLIST,EXT-X-STREAM-INF,EXT-X-DISCONTINUITY,EXT-X-VERSION
 
3.2新标签
3.2.1      EXT-X-TARGETDURATION
该标签指定了媒体文件持续时间的最大值,播放文件列表中的媒体文件在EXTINF标签中定义的持续时间必须小于或者等于该标签指定的持续时间。该标签在播放列表文件中必须出现一次,其格式为:
# EXT-X-TARGETDURATION:<s>
S是一个以秒为单位的整数。
3.2.2      EXT-X-MEDIA-SEQUENCE
播放列表文件中每个媒体文件的URI都有一个唯一的序列号。URI的序列号等于它之前那个RUI的序列号加一。EXT-X-MEDIA-SEQUENCE指明了出现在播放列表文件中的第一个URI的序列号。其格式如下:
#EXT-X-MEDIA-SEQUENCE:<Number>
播放列表文件中的EXT-X-MEDIA-SEQUENCE标签不能多于一个。如果播放列表文件中没有EXT-X-MEDIA-SEQUENCE标签,那么将会把播放列表中第一个URI的序列号当成0。
媒体文件的序列号码不是必须出现在它的URI中的。见6.3.2和6.3.5。
3.2.3      EXT-X-KEY
媒体文件可能是被加密的,EXT-X-KEY提供了解密媒体文件的必要信息,它的格式如下:
#EXT-X-KEY:METHOD=<method> [,URI = “<uri>”] [,IV = <iv>]
Method属性指定了加密方法,定义了两种加密方法:NONE和AES-128。
加密方法NONE表示媒体文件不被加密,如果加密方法是NONE,那么URI和IV属性不允许存在。
加密方法AES-128表示媒体文件使用高级加密标准128位密钥和PKCS7 padding加密。如果加密方法是AES-128,那么对于URI属性,如果存在,则指定获取密钥的方法;对于IV属性,如果存在,则指定使用密钥的初始化向量。
IV属性出现在协议版本2中,新的EXT-X-KEY将会取代任何一个先前的EXT-X-KEY。
如果播放列表文件没有包含EXT-X-KEY标签,那么媒体文件将不会被加密。
密钥文件的格式见第五章,媒体文件加密信息见5.2、6.2.3、6.3.6。
3.2.4      EXT-X-PROGRAM-DATE-TIME
EXT-X-PROGRAM-DATE-TIME标签将下一个媒体文件的开头和绝对日期关联起来。日期/时间的表示基于ISO/IEC,并且要指明时区。例如:
#EXT-X-PROGRAM-DATE-TIME:<YYYY–MM–DDThh:mm:ssZ>
详见6.2.1和6.3.3
3.2.5      EXT-X-ALLOW-CATCH
EXT-X-ALLOW-CATCH标签指定客户端可以或者不准缓存下载的媒体文件用来重播。它可能会出现在播放列表文件的任何地方,但是不能出现两次或以上。该标签适用于播放列表中的所有分片。其格式如下:
#EXT-X-ALLOW-CACHE:<YES|NO>
详见6.3.3
3.2.6      EXT-X-ENDLIST
    EXT-X-ENDLIST标签标示没有更多媒体文件将会加入到播放列表中,它可能会出现在播放列表文件的任何地方,但是不能出现两次或以上。其格式如下:
#EXT-X-ENDLIST
3.2.7      EXT-X-STREAM-INF
     EXT-X-STREAM-INF标签表示在播放列表中的下一个URI标识另一个播放列表文件。格式如下:
#EXT-X-STREAM-INF:[attribute=value][,attribute=value]* <URI>
在一个EXT-X-STREAM-INF标签中attribute不能出现两次或以上。其它属性定义:
BANDWIDTH = <n>
n为每秒比特数,它必须是每个媒体文件比特速率的上限,必须经过计算包含那些在播放列表中出现的或者将要出现的容器开销。
PROGRAM-ID=<i>
i是一个数字,在播放列表文件的范围内唯一的标识了一个特定的演示文稿。
    一个播放列表文件可能包含多个具有相同PROGRAM-ID 的EXT-X-STREAM-INF标签来标识某个演示文稿的不同编码。这些变种的的播放列表可能包含额外的EXT-X-STREAM-INF标签。
 
CODECS="[format][,format]*"
 
每一种格式都指定了存在于媒体文件中的媒体类型。合法的格式标示符都是那些在ISO文件格式名称空间被RFC4281定义的格式。
RESOLUTION=<N>x<M>
 
N是流中视频水平编码分辨率的近似,以像素数表示,M是编码垂直分辨率的近似。
3.2.8      EXT-X-DISCONTINUITY
     EXT-X-DISCONTINUITY标签表示该标签后边的媒体文件和之前的媒体文件之间的编码间断。特性可能改变的一组是:
file format
number and type of tracks
encoding parameters
encoding sequence
详见第四章,6.2.1、6.3.3。
 
 
3.2.9      EXT-X-VERSION
EXT-X-VERSION标签指出了播放列表版本的适应性。播放列表文件、其关联的媒体和服务器必须遵守最新版本的所有规定。
 
 
4 多媒体文件
每一个媒体文件资源定位符都必须标识一个媒体文件,该文件是整体数据的一个分片。每个媒体文件必须按照MPEG-2的传输流和MPEG-2音频流的格式。[ISO13818]
传输流文件必须包含一个MPEG-2节目。在每个文件的开始应该有一个节目关联表和一个节目映射表。包含视频的文件应该有至少一个密钥帧和足够的信息来完全初始化一个视频解码器。
播放列表中的媒体文件必须是编码流中媒体文件的末尾与先前的序列号的延续,除非它是播放列表中出现的第一个媒体文件,或者它前边有EXT-X-DISCONTINUITY标签。
客户端应该准备好处理一个特定类型(音频或视频等)的多个轨道。一个没有优先级的客户端应该选择它能播放的具有最小数字编号的音轨。
客户端应该忽略那些传输流的内部不能识别的流。
媒体文件内样本流和相应的多媒体流的编码参数应保持一致。然而客户端应该解决编码的变化问题,例如缩放视频内容以适应分辨率改变。
5 密钥文件
5.1介绍
    URI属性中EXT-X-KEY标签标识一个密钥文件。密钥文件包含解密播放列表中媒体文件的密钥。AES-128加密算法使用16字节的密钥。密钥文件的格式为16字节的二进制数数组。
5.2  IV FOR AES-128
128位AES在加密和解密的时候需要提供一个相同的16字节的初始化向量(IV),变换IV可以提高密钥的健壮性。
如果EXT-X-KEY标签有IV属性,在使用密钥加密或者解密的时候必须使用此属性值作为IV。这个值必须被解释为128位的16进制数,而且必须有前缀0x。
    如果EXT-X-KEY标签没有IV属性,在加密或者解密媒体文件的时候必须使用序列号作为IV值。大端二进制表示的序列号应该放置在16字节的缓冲区中且左边补0。
6 客户端/服务器行为
6.1介绍
本章介绍服务器怎样产生播放列表和媒体文件以及客户端怎样下载并播放。
6.2服务器进程
6.2.1介绍
MPEG-2数据流的产生超过了本文档的范围,本文档仅仅假设有一个数据流连续的源。
服务器必须将数据流分割成持续时间大致相等的媒体文件,服务器应该尝试点分割流来支持对个别媒体文件的有效解码,例如包和关键帧的边界。
服务器必须为媒体文件创建URI,允许它的客户端能够获取到文件。
服务器必须创建播放列表。播放列表必须符合第三章描述的格式。服务器要提供的媒体文件的URI必须按顺序出现在播放列表中。如果URI出现在了播放列表中,那么这个媒体文件对于客户端必须是可用的。
播放列表文件必须包含一个EXT-X-TARGRTDURATION标签,它必须指明添加到播放列表中媒体文件的最大EXTINF值。整个演示文稿期间,这个值必须保持不变。典型持续时间为10s。
播放列表文件应该包含EXT-X-VERSION标签来说明流对于版本的兼容性。它的值应该是服务器、播放列表文件和其所关联的媒体文件都能执行的最低协议版本。
如果播放列表文件通过HTTP传输,那么服务器应该支持客户端请求使用gzip内容编码。
从客户端的角度来看,播放列表文件的变更必须是自动的。
服务器不可以改变EXT-X-ALLOW-CATCH的值。
播放列表中每个媒体文件的URI必须以EXTINF作为前缀来说明媒体文件的持续时间。
服务器可以将媒体文件和绝对的日期和时间关联起来,只要在它的URI前缀上一个EXT-X-PROGRAM-DATE-TIME标签。日期和时间的值提供了一个媒体时间表到挂钟时间的信息映射,该挂钟时间可以作为搜索、显示或其他目的的基准。
如果服务器提供了这个映射,那么它应该在每个EXT-X-DISCONTINUITY标签的后边加一个EXT-X-PROGRAM-DATE-TIME标签。
如果播放列表文件包含演示文稿的最后一个分片,那么应该加一个EXT-X-ENDLIST标签。
如果播放列表文件没有包含EXT-X-ENDLIST标签,那么服务器应该使一个新版本的播放列表文件可用,并至少包含一个媒体文件的URI。新的播放列表文件必须与前一个播放列表文件在相对的时间内有效:从上一个播放列表文件开始有效的时间算起,不早于0.5倍持续时间,不晚于1.5倍持续时间。//不太清楚可用是什么意思?
如果服务器期望移除演示文稿,它必须使播放列表文件对于客户端不可用,在播放列表被清除时,它应该确保播放列表文件中的所有媒体文件对于客户端来说至少在一个播放列表文件持续时间内是可用的。
6.2.2滑动窗口播放列表
服务器可以限制最近一段时间添加到播放列表文件中的媒体文件的可用性,为了达到这个目的,播放列表文件必须包含准确的EXT-X-MEDIA-SEQUENCE标签。标签的值是按照从播放列表中移除的媒体文件的URI递增的。
媒体文件的URI必须按照其加入的顺序移除。当服务器从播放列表移除URI时,媒体文件在一段时间内必须保持可用,该时间等于媒体文件的时间加上包含该媒体文件的最长播放列表文件的时间。
当媒体文件通过http传输给客户端后,如果服务器打算移除该文件,那么它应该确保http响应头包含反应生存时间的过期头。
那些不包含EXT-X-ENDLIST标签的播放列表文件的持续时间必须至少三倍于targrtdutration。//为什么是三倍?
6.2.3加密媒体文件
如果媒体文件需要被加密,那么服务器必须定义一个URI来允许被授权的客户端获取包含解密密钥的密钥文件。密钥文件必须符合第五章描述的格式。服务器可以在密钥响应中设置超时头来表名密钥可以被缓存。
如果采用AES-128加密算法,那么AES-128 CBC加密模式应该适应于每一个媒体文件。整个文件必须是加密的。密码块的连接不能用于跨媒体文件。用于解密的初始化向量必须是媒体文件的序列号或者EXT-X-KEY标签的IV属性的值。服务器必须使用这种加密算法和其他由紧随在播放列表文件中URI后边的EXT-X-KEY标签所指定的属性来加密播放列表文件中的每一个媒体文件。EXT-X-KEY标签中方法为none或者没有EXT-X-KEY标签的媒体文件不能被加密。
    如果播放列表文件包含了一个经过加密的媒体文件的URI,那么服务器不可以将EXT-X-KEY标签从播放列表文件中移除。
6.2.4提供变种数据流
服务器可以提供多个播放列表文件来支持对同一个演示文稿的不同编码。提供变种播放列表文件列出每一个变种流,从而使得客户端可以在不同编码之间动态切换。
变种播放列表文件必须为每一个变种流包含一个EXT-X-STREAM-INF标签。同一演示文稿的每个EXT-X-STREAM-INF都必须有相同的programid。每个演示文稿的programid在变种播放列表内必须是唯一的。
如果EXT-X-STREAM-INF标签包含CODECS属性,则属性值必须包含RFC4281定义的所有格式,
 
服务器在生成变种流的时候必须遵守以下规则:
1)每一个变种流必须呈现相同的内容,包括流的间断性。
2)每个变种播放列表文件必须有相同的target duration。
3)只在个别变种播放列表文件中出现的内容必须放在列表文件的头或者尾,且不能超过target duration。
4)变种流内匹配内容,必须有匹配时间戳。这可以使客户端同步流。
5)基本音频流文件必须在文件中第一个样本的采样信号的时间戳前预先准备一个ID3 PRIV标签,标签的所有者标示符为“com.apple.streaming.transportStreamTimestamp”。二进制数据必须是33位的基本时间戳,用8字节的数字表示。
 
另外,所有的变种流都应该包含相同编码的音频二进制流。这使得客户端在不同的流之间切换时没有毛刺声音。//什么事毛刺声音?
6.3客户端进程
6.3.1介绍
客户端怎样获取播放列表中的URI不在本文档的范围之内,我们假设已经获取到了URI。
6.3.2加载播放列表文件
每一次加载或者重载播放列表文件时:
客户端必须保证播放列表文件以EXTM3U标签开头,并且如果协议版本号存在,客户端必须支持该版本。否则,客户端不可以试图使用该列表文件。
客户端可以忽略它不能识别的标签和属性。
如果播放列表文件包含了EXT-X-MEDIA-SEQUENCE标签,那么客户端会假设在播放列表被加载的时间内以及播放列表的持续时间内媒体文件将变得不可用。播放列表的持续时间等于其中包含的媒体文件时长的总和。//为啥假设不可用?
6.3.3播放播放列表文件
当开始播放的时候,客户端首先从播放列表中选择要播放的媒体文件。如果不存在EXT-X-ENDLIST标签,并且客户端想正常播放媒体(按顺序以标准速率播放),那么客户端就不应该从播放列表文件尾部选择少于三个target duration的媒体文件。
为了达到正常播放的目的,媒体文件必须按照他们在播放列表中的顺序播放。客户端还可以用其他任何方式播放,比如顺序播放,随机播放,特效播放等。
对于存在EXT-X-DISCONTINUITY标签的媒体文件,在播放之前客户端必须准备好重置分析和解码器。
为了不间断播放,应该提前载入媒体文件,以补偿延时和吞吐量的变化。
如果播放列表文件包含了EXT-X-ALLOW-CATCH标签,并且它的值为NO,那么客户端在播放以后不可以缓存媒体文件。否则允许缓存用来以后重播。
客户端可以使用EXT-X-PROGRAM-DATE-TIME标签来为用户显示节目的起始时间。如果这个值包含了时区信息,那么客户端应该考虑到这点;如果不包含,那么客户端不可以推测时区。
客户端不能依靠EXT-X-ALLOW-CATCH标签值的正确性和一致性。
6.3.4重新载入播放列表文件
客户端必须阶段性的重新载入播放列表文件,除非文件包含了EXT-X-ENDLIST标签。然而也不能过于频繁的载入。
当客户端第一次载入播放列表文件或者已经载入但是发现文件与上次载入的时候有了变化,客户端都必须等待一段时间在可以再次载入。这段时间被称为原始最小重载延迟,它是从客户端开始载入一个播放列表文件开始计算的。
原始最小重载延迟是播放列表文件中最后一个媒体文件的持续时间。媒体文件的持续时间由EXTINF标签来指定。
如果客户端重载了一个播放列表文件,但是发现文件并没有变化,那么它在重试之前必须等一段时间。最小延迟是target duration的倍数。第一次是0.5倍,第二次1.5倍,3倍。。。
6.3.5确定下一个要加载的文件
当播放列表文件被载入或者重载以后,客户端必须检查播放列表来确定要载入的媒体文件。要载入的第一个文件必须是客户端要播放的第一个文件,见6.3.3。
    如果要播放的文件已经被载入,并且播放列表文件不包含EXT-X-MEDIA-SEQUENCE标签,那么客户端必须确认播放列表文件包含了最后一个被载入的媒体文件的URI,如果不包含,则暂停播放。要载入的下一个媒体文件必须是上一次载入的媒体文件URI之后的第一个媒体文件的URI。
    如果要播放的文件已经被载入,并且播放列表文件包含EXT-X-MEDIA-SEQUENCE标签,那么要载入的下一个媒体文件就是比上一次载入的文件的序列号大的媒体文件中的序列号最小者。
6.3.6解密经加密的媒体文件
如果播放列表文件包含了一个指定密钥文件URI的EXT-X-KEY标签,客户端必须获取密钥文件,并使用其中的密钥来解密KEY标签之后的所有媒体文件,直到遇到另一个EXT-X-KEY标签为止。
7 协议版本的兼容性
客户端和服务器必须使用版本2以及更高版本。
 
 
8 例子
8.1简单的播放列表文件
#EXTM3U
#EXT-X-TARGETDURATION:5220
#EXTINF:5220,
http://media.example.com/entire.ts
#EXT-X-ENDLIST
 
8.2滑动窗口播放列表,使用https
#EXTM3U
#EXT-X-TARGETDURATION:8
#EXT-X-MEDIA-SEQUENCE:2680
#EXTINF:8,
https://priv.example.com/fileSequence2680.ts
#EXTINF:8,
https://priv.example.com/fileSequence2681.ts
#EXTINF:8,
https://priv.example.com/fileSequence2682.ts
 
8.3加密的媒体文件与播放列表文件
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:7794
#EXT-X-TARGETDURATION:15
#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"
#EXTINF:15,
http://media.example.com/fileSequence52-1.ts
#EXTINF:15,
http://media.example.com/fileSequence52-2.ts
#EXTINF:15,
http://media.example.com/fileSequence52-3.ts
#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=53"
#EXTINF:15,
http://media.example.com/fileSequence53-1.ts
变种的播放列表文件
#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1280000
http://example.com/low.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2560000
http://example.com/mid.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=7680000
http://example.com/hi.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=65000,CODECS="mp4a.40.5"
http://example.com/audio-only.m3u8
作者:yeming1108 发表于2016/8/17 15:56:46 原文链接
阅读:19 评论:0 查看评论

Android百度地图、高德地图、腾讯地图导航路线规划问题

$
0
0
      Android百度地图、高德地图、腾讯地图导航路线规划问题
      在最近的项目中,需求是用户选择某个地址需要进行导航时,弹出百度地图、高德地图和腾讯地图让用户选择。如果该用户手机中已安装对应的地图App,则启动对应软件进行导航,否则跳转到网页版的地图进行导航。

    
如下为各个地图效果:
       
百度地图高德地图

腾讯地图

   一开始做时,这不就简单吗,坐标嘛,都是一样的,去各个地图平台找到对应的URL api,传入当前的坐标和目的地址的坐标(当前使用的是百度地图的SDK)。利用Intent跳转不就行了嘛。完成之后才发现,too young too simple,原谅我对国家的安全知识了解有点少。坐标各地图是不一定相同的,地址是有偏移的。
    下面是我找弄度娘来的(为什么不用google呢?):       
  我们平时用到的地球坐标系统,叫做WGS-84坐标,这个是国际通用的“准确”的坐标系统。国家保密插件,其实就是对真实坐标系统进行人为的加偏处理,即为GCJ-02坐标,戏称“火星坐标”。于是,我们有了下面的东西:
    - 地球坐标:指WGS84坐标系统
    - 火星坐标:指使用国家保密插件人为偏移后的坐标
    - 地球地图:指与地球坐标对应的客观真实的地图
    - 火星地图:指经过加密偏移后的,与火星坐标对应的地图

国内出版的各种地图系统(包括电子形式),必须至少采用GCJ-02对地理位置进行首次加密。于是,

    - 谷歌地图的大陆地图、高德国内地图采用GCJ-02对地图进行加偏。
    - 百度地图更是进一步发挥了天朝特色,除了GCJ-02加偏,自己又在此基础上继续进行加偏,相应的坐标称为BD-09坐标。
各地图厂商使用的坐标系:
        - 火星坐标  GCJ02坐标系
            - iOS 地图
            - Gogole地图
            - 搜搜(腾讯)、阿里云、高德地图
        - 地球坐标  WGS84坐标系
            - Google 卫星地图(国外地图应该都是……)
        - 百度坐标  BD09坐标系
            - 百度地图 

   好了,弄懂了之后。也很简单嘛,用的是百度地图SDK,那手上的坐标当然也是百度的。那调用高德和腾讯地图时,
只需把百度地图的坐标转换为火星坐标就行啊。
下面继续是度来的一个转换算法,原创地址:http://blog.csdn.net/myfmyfmyfmyf/article/details/45717797
public class MapTranslateUtils {


    /**
     * 坐标转换,腾讯地图(火星坐标)转换成百度地图坐标
     * @param lat 腾讯纬度
     * @param lon 腾讯经度
     * @return 返回结果:经度,纬度
     */
    public static  double[] map_hx2bd(double lat, double lon){
        double bd_lat;
        double bd_lon;
        double x_pi=3.14159265358979324;
        double x = lon, y = lat;
        double z = Math.sqrt(x * x + y * y) + 0.00002 * Math.sin(y * x_pi);
        double theta = Math.atan2(y, x) + 0.000003 * Math.cos(x * x_pi);
        bd_lon = z * Math.cos(theta) + 0.0065;
        bd_lat = z * Math.sin(theta) + 0.006;
        double[] doubles = new double[]{bd_lat,bd_lon};
        System.out.println("bd_lat:"+bd_lat);
        System.out.println("bd_lon:"+bd_lon);
        return  doubles;
    }



    /**
     * 坐标转换,百度地图坐标转换成腾讯地图坐标
     * @param lat  百度坐标纬度
     * @param lon  百度坐标经度
     * @return 返回结果:纬度,经度
     */
    public static  double[] map_bd2hx(double lat, double lon){
        double tx_lat;
        double tx_lon;
        double x_pi=3.14159265358979324;
        double x = lon - 0.0065, y = lat - 0.006;
        double z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * x_pi);
        double theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * x_pi);
        tx_lon = z * Math.cos(theta);
        tx_lat = z * Math.sin(theta);

        double[] doubles = new double[]{tx_lat,tx_lon};
        return doubles;
    }

}
<span style="font-family: 微软雅黑; background-color: rgb(255, 255, 255);">    ok,现在百度地图坐标系,火星坐标系都在手了。开干。</span>
        那首先要创建  一个webView页面:     
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
android:orientation="vertical">

<com.zk.common.TitleBarView
android:id="@+id/titleBar"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@color/title_bg"/>

<WebView
android:id="@+id/web_wv"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>

</LinearLayout>

WebView的Activity:
public class WebViewActivity extends AppCompatActivity {

    private WebView detailsWebView;
    private String mUrl;
    private Handler handler;
    private ProgressDialog pd;
    private String mTitle;


    public static void launch(Activity activity, String url, String title) {

        Intent intent = new Intent(activity, WebViewActivity.class);
        intent.putExtra("url", url);
        intent.putExtra("title", title);
        activity.startActivity(intent);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_web_view);

        mUrl = this.getIntent().getStringExtra("url");
        mTitle = this.getIntent().getStringExtra("title");
        detailsWebView = (WebView) findViewById(R.id.web_wv);
        detailsWebView.getSettings().setJavaScriptEnabled(true);
        detailsWebView.getSettings().setDomStorageEnabled(true);
        detailsWebView.requestFocus(View.FOCUS_DOWN);
        detailsWebView.getSettings().setUserAgentString("User-Agent");

        detailsWebView.setWebViewClient(new WebViewClient() {
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                view.loadUrl(url);
                return false;
            }
        });
        // 设置web视图客户端
        detailsWebView.setWebChromeClient(new WebChromeClient() {
            @Override
            public void onProgressChanged(WebView view, int progress) {// 载入进度改变而触发

                if (progress == 100) {
                    handler.sendEmptyMessage(1);// 如果全部载入,隐藏进度对话框 }
                    detailsWebView.setVisibility(View.VISIBLE);
                }
                super.onProgressChanged(view, progress);
            }

            @Override
            public void onReceivedTitle(WebView view, String title) {
                super.onReceivedTitle(view, title);

            }
        });

        detailsWebView.setVisibility(View.GONE);
        pd = new ProgressDialog(this);
        pd.setProgressStyle(ProgressDialog.STYLE_SPINNER);
        pd.setMessage("数据载入中,请稍候!");


        handler = new Handler() {

            public void handleMessage(Message msg) {// 定义一个Handler,用于处理下载线程与UI间通讯
                super.handleMessage(msg);
                if (!Thread.currentThread().isInterrupted()) {
                    switch (msg.what) {
                        case 0:
                            pd.show();// 显示进度对话框
                            break;
                        case 1:
                            pd.hide();// 隐藏进度对话框,不可使用dismiss()cancel(),否则再次调用show()时,显示的对话框小圆圈不会动。
                            break;
                    }
                }

            }

        };
        loadurl(detailsWebView, mUrl);
    }

    public void loadurl(final WebView view, final String url) {
        handler.post(new Runnable() {

            @Override
            public void run() {
                handler.sendEmptyMessage(0);
                view.loadUrl(url);// 载入网页
            }
        });
    }
}
首先,选择判断是否安装对应地图App时,有则启动,没有则转网页地图。


private void selectBaidu() {
    this.dismiss();
    try {
        //调起App
        if (isInstallByread("com.baidu.BaiduMap")) {
            Intent intent = Intent.getIntent("intent://map/direction?origin=latlng:"
                    + nowLat + "," + nowLng
                    + "|name:&destination=" + desAddress + "&mode=driving®ion=" + "我的位置"
                    + "&referer=Autohome|GasStation#Intent;scheme=bdapp;package=com.baidu.BaiduMap;end");
            mActivity.startActivity(intent);
        } else {
            /*String url = "http://api.map.baidu.com/direction?origin=latlng:" + mLatitude + ","
                            + mLongitude + "|name:&destination=" + mDestination
                            + "&mode=driving&output=html&src=天工项目共建";
                    WebViewActivity.launch(getActivity(), url, "网页版地图导航");*/
            Toast.makeText(mActivity, "如果您没有安装百度地图APP" +
                    "可能无法正常使用导航,建议选择其他地图", Toast.LENGTH_SHORT).show();
        }
    } catch (URISyntaxException e) {
        e.printStackTrace();
    }

}
 2、高德地图 :   开发平台URL地址: http://lbs.amap.com/api/uri-api/android-uri-explain/
private void selectGaode() {
    this.dismiss();
    double[] txDesLatLng = MapTranslateUtils.map_bd2hx(desLat, desLng);
    double[] txNowLatLng = MapTranslateUtils.map_bd2hx(nowLat, nowLng);
    if (isInstallByread("com.autonavi.minimap")) {
        try {
            Intent intentOther = new Intent("android.intent.action.VIEW",
                    Uri.parse("androidamap://navi?sourceApplication=amap&lat="
                            + desLat + "&lon=" + desLng + "&dev=1&stype=0"));
            intentOther.setPackage("com.autonavi.minimap");
            intentOther.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
            mActivity.startActivity(intentOther);
        } catch (Exception e) {
            String url = "http://m.amap.com/?from="
                    + nowLat + "," + nowLng
                    + "(from)&to=" + desLat + "," + desLng + "(to)&type=0&opt=1&dev=0";
            WebViewActivity.launch(mActivity, url, "网页版地图导航");
        }

    } else {
        String url = "http://m.amap.com/?from="
                + txNowLatLng[0] + "," + txNowLatLng[1]
                + "(from)&to=" + txDesLatLng[0] + "," + txDesLatLng[1] + "(to)&type=0&opt=1&dev=0";
        WebViewActivity.launch(mActivity, url, "网页版地图导航");

    }

}

   需要注意的是上面的dev参数,如果你传入的已经是火星坐标,参数为0就行,如果你传入的是高德坐标,则传入1。这里是个坑,要注意一下。


3、腾讯地图:    开发平台URL地址: http://lbs.qq.com/uri_v1/guide-route.html
        看官方介绍目前腾讯还不支持直接启动腾讯地图App。

    所以只能调用web的地图:

private void selectTencent() {
    this.dismiss();
    double[] txDesLatLng = MapTranslateUtils.map_bd2hx(desLat, desLng);
    double[] txNowLatLng = MapTranslateUtils.map_bd2hx(nowLat, nowLng);
    String url = "http://apis.map.qq.com/uri/v1/routeplan?type=drive&from=&fromcoord="
            + txNowLatLng[0] + "," + txNowLatLng[1]
            + "&to=&tocoord=" + txDesLatLng[0] + "," + txDesLatLng[1] + "&policy=0&referer=myapp";
    WebViewActivity.launch(mActivity, url, "网页版地图导航");
}
另外此处是高德地图对其他坐标进行转换的api:

   Demo代码地址:









作者:zengke1993 发表于2016/8/17 16:02:19 原文链接
阅读:15 评论:0 查看评论

MotionEvent 概述

$
0
0

MotionEvent

2016/8/17 11:01:49

第一篇

  Object used to report movement (mouse, pen, finger, trackball) events.
  Motion events may hold either absolute or relative movements and other data,
  depending on the type of device.

MotionEvent用于呈现移动事件(鼠标、手写笔、手指、追踪器)事件。Motion events 可根据不同的设备类型,控制移动事件和其他数据。

Overview

简介

 Motion events describe movements in terms of an action code and a set of axis values.
 The action code specifies the state change that occurred such as a pointer going
 down or up.  The axis values describe the position and other movement properties.

Motion Events 描述移动依据一个动作和一组坐标轴值。这个动作指示发生状态改变比如指示器将要向下个或者上移动。这个轴值描述的是位置和其他移动属性。

 For example, when the user first touches the screen, the system delivers a touch
 event to the appropriate {@link View} with the action code {@link #ACTION_DOWN}
 and a set of axis values that include the X and Y coordinates of the touch and
 information about the pressure, size and orientation of the contact area.

例如:当用户第一次碰触屏幕,系统是分派一个触碰事件的操作码给相应的控件,操作码包含一组坐标轴值包含有X轴和Y轴触碰和相关按压的作用区域的尺寸和方向相关信息。

Some devices can report multiple movement traces at the same time.  Multi-touch
screens emit one movement trace for each finger.  The individual fingers or
other objects that generate movement traces are referred to as <em>pointers</em>.
Motion events contain information about all of the pointers that are currently active
even if some of them have not moved since the last event was delivered.

一些设备可以同时呈现多个移动轨迹,多个手指触摸屏幕事件运动轨迹发出给每一根手指。每个独立手指或其他对象被分配生成移动轨迹都引用指针事件包含所有相关信息,直到当前活动事件不移动为止。

The number of pointers only ever changes by one as individual pointers go up and down,
except when the gesture is canceled

多个指示器仅由单个手指改变向上和下,除非当手势取消掉。

 Each pointer has a unique id that is assigned when it first goes down
 (indicated by {@link #ACTION_DOWN} or {@link #ACTION_POINTER_DOWN}).  A pointer id
 remains valid until the pointer eventually goes up (indicated by {@link #ACTION_UP}
 or {@link #ACTION_POINTER_UP}) or when the gesture is canceled (indicated by
 {@link #ACTION_CANCEL}).

每一个指示器触发ACTION_DOWN会分配一个唯一的id,这个指示器一直有效直到ACTION_UP触发或取消掉改手势行为。

 The MotionEvent class provides many methods to query the position and other properties of
 pointers, such as {@link #getX(int)}, {@link #getY(int)}, {@link #getAxisValue},
 {@link #getPointerId(int)}, {@link #getToolType(int)}, and many others.  Most of these
 methods accept the pointer index as a parameter rather than the pointer id.
 The pointer index of each pointer in the event ranges from 0 to one less than the value
 returned by {@link #getPointerCount()}.

MotionEvent这个类提供很多方法去查询位置和指示器的其他属性,比如获取X轴坐标值、Y轴坐标值、坐标轴值、指示器Id、工具类型、和诸多其他方法。这些大多数的方法接收指示器下标而不是指示器id.每个指示器索引通过从getPointerCount()方法获取,范围从0开始或一个也没有。

 The order in which individual pointers appear within a motion event is undefined.
 Thus the pointer index of a pointer can change from one event to the next but
 the pointer id of a pointer is guaranteed to remain constant as long as the pointer
 remains active.  Use the {@link #getPointerId(int)} method to obtain the
 pointer id of a pointer to track it across all subsequent motion events in a gesture.
 Then for successive motion events, use the {@link #findPointerIndex(int)} method
 to obtain the pointer index for a given pointer id in that motion event.

哪一个独立的指示器出现的移动事件顺序是不明确的。因此一个指示器的索引可以从一个事件到下一个事件改变但是指示器的id尽可能的长时间的保留活动的常量。使用getPointerId方法获得指示器id在所有事件队列中的手势中追踪。对于连续的事件,使用findPotinterIndex方法获得给定移动事件的指示器索引。

 Mouse and stylus buttons can be retrieved using {@link #getButtonState()}.  It is a
 good idea to check the button state while handling {@link #ACTION_DOWN} as part
 of a touch event.  The application may choose to perform some different action
 if the touch event starts due to a secondary button click, such as presenting a
 context menu.

鼠标和手写笔可以通过使用getButtonState检索。这是一个很好的想法去检查按钮状态并处理触碰事件的一部分。应用可以选择完成不同的行为从事件开始到第二个按钮点击,例如按下菜单键。

Batching

批处理

 For efficiency, motion events with {@link #ACTION_MOVE} may batch together
 multiple movement samples within a single object.  The most current
 pointer coordinates are available using {@link #getX(int)} and {@link #getY(int)}.
 Earlier coordinates within the batch are accessed using {@link #getHistoricalX(int, int)}
 and {@link #getHistoricalY(int, int)}.  The coordinates are "historical" only
 insofar as they are older than the current coordinates in the batch; however,
 they are still distinct from any other coordinates reported in prior motion events.
 To process all coordinates in the batch in time order, first consume the historical
 coordinates then consume the current coordinates.

为了高效,ACTION_DOWN移动事件可以同时和多个移动事件在单一的对象中处理。大多数情况获得当前坐标可使用getX()和getY()方法。更早的坐标处理访问getHistoricalX()和getHistoricalY。历史坐标仅在当前坐标之前;然而,他们仍然和其他呈现的原始移动事件不同。一次处理所有的坐标,第一消耗历史坐标会消耗当前坐标。

Example: Consuming all samples for all pointers in a motion event in time order.

例如:同时有序消耗所有单个指示器的移动事件

 
void printSamples(MotionEvent ev) {
final int historySize = ev.getHistorySize();
final int pointerCount = ev.getPointerCount();
for (int h = 0; h < historySize; h++) {
System.out.printf("At time %d:", ev.getHistoricalEventTime(h));
for (int p = 0; p < pointerCount; p++) {
System.out.printf(" pointer %d: (%f,%f)",
ev.getPointerId(p), ev.getHistoricalX(p, h), ev.getHistoricalY(p, h));
}
}
System.out.printf("At time %d:", ev.getEventTime());
for (int p = 0; p < pointerCount; p++) {
System.out.printf(" pointer %d: (%f,%f)",
ev.getPointerId(p), ev.getX(p), ev.getY(p));
}
}

Device Types

设备类型

 The interpretation of the contents of a MotionEvent varies significantly depending
 on the source class of the device.

解析事件变量的意义依赖于设备的来源类型。

 On pointing devices with source class {@link InputDevice#SOURCE_CLASS_POINTER}
 such as touch screens, the pointer coordinates specify absolute
 positions such as view X/Y coordinates.  Each complete gesture is represented
 by a sequence of motion events with actions that describe pointer state transitions
 and movements.  A gesture starts with a motion event with {@link #ACTION_DOWN}
 that provides the location of the first pointer down.  As each additional
 pointer that goes down or up, the framework will generate a motion event with
 {@link #ACTION_POINTER_DOWN} or {@link #ACTION_POINTER_UP} accordingly.
 Pointer movements are described by motion events with {@link #ACTION_MOVE}.
 Finally, a gesture end either when the final pointer goes up as represented
 by a motion event with {@link #ACTION_UP} or when gesture is canceled
 with {@link #ACTION_CANCEL}.

指示设备来源类型参考InputDevice.SOURCE_CLASS_POINTER,比如触摸屏幕,指示器坐标指定X/Y坐标相对位置。每一个完成的手势被呈现在完成移动事件的行为队列中,这个队列描述这个指示器移动和过度。一个手势开始于事件ACTION_DOWN提供了第一个指示器位置被按下。一个附加的指示器可以按下或释放,框架层将生成一个ACTION_POINTER_DOWN或ACIONT_POINTER_UP的移动事件。指示移动被描述为ACTION_MOVE的行为。最后,一个手势结束当指示器释放通过ACTION_UP事件或ACTION_CANCEL.

 Some pointing devices such as mice may support vertical and/or horizontal scrolling.
 A scroll event is reported as a generic motion event with {@link #ACTION_SCROLL} that
 includes the relative scroll offset in the {@link #AXIS_VSCROLL} and
 {@link #AXIS_HSCROLL} axes.  See {@link #getAxisValue(int)} for information
 about retrieving these additional axes.

一些指示设备例如鼠标可以支持垂直或水平的滚动。滚动事件通过ACTION_SCROLL来呈现包括相关的滚动偏移AXIS_VSCROLL和AXIS_HSCROLL坐标轴。通过getAxisValue检索获取更多附加坐标轴信息。

 On trackball devices with source class {@link InputDevice#SOURCE_CLASS_TRACKBALL},
 the pointer coordinates specify relative movements as X/Y deltas.
 A trackball gesture consists of a sequence of movements described by motion
 events with {@link #ACTION_MOVE} interspersed with occasional {@link #ACTION_DOWN}
 or {@link #ACTION_UP} motion events when the trackball button is pressed or released.

追踪球设备来源通过InputDevice.SOURCE_CLASS_TRACKBALL获得,指示器坐标指定相关移动的X/Y增量。追踪球手势由移动事件队列组成其描述为ACTION_MOVE其中点缀着当追踪球的按下ACTION_DOWN或ACTION_UP释放事件。

 On joystick devices with source class {@link InputDevice#SOURCE_CLASS_JOYSTICK},
 the pointer coordinates specify the absolute position of the joystick axes.
 The joystick axis values are normalized to a range of -1.0 to 1.0 where 0.0 corresponds
 to the center position.  More information about the set of available axes and the
 range of motion can be obtained using {@link InputDevice#getMotionRange}.
 Some common joystick axes are {@link #AXIS_X}, {@link #AXIS_Y},
 {@link #AXIS_HAT_X}, {@link #AXIS_HAT_Y}, {@link #AXIS_Z} and {@link #AXIS_RZ}.

操纵杆设备通过InputDevice.SOURCE_CLASS_JOYSTICK方式获取,指示器指示操纵杆的绝对位置。操纵杆坐标值通常冉伟在-1.0到1.0之间0.0坐标是中心位置。更多信息相关一组可用的坐标轴范围和时间可以通过InputDevice.getMotionRange获得。一些通用操纵杆坐标参见AXIS_X,AXIS_HAT_X,AXIS_HAT_Y,AXIS_Z和AXIS_RZ.

Refer to {@link InputDevice} for more information about how different kinds of 
input devices and sources represent pointer coordinates.

参考InputDevices获取更多不同种类的输入设备和资源指示器坐标信息。

Consistency Guarantees

一致性保证

 Motion events are always delivered to views as a consistent stream of events.
 What constitutes a consistent stream varies depending on the type of device.
 For touch events, consistency implies that pointers go down one at a time,
 move around as a group and then go up one at a time or are canceled.

移动事件总是分配给控件保持一致的流。一致流的变动组成取决于设备的类型。对于触碰事件,一致性意味着指示器按下在一起,移动和释放取消都和view同时进行的。

 While the framework tries to deliver consistent streams of motion events to
 views, it cannot guarantee it.  Some events may be dropped or modified by
 containing views in the application before they are delivered thereby making
 the stream of events inconsistent.  Views should always be prepared to
 handle {@link #ACTION_CANCEL} and should tolerate anomalous
 situations such as receiving a new {@link #ACTION_DOWN} without first having
 received an {@link #ACTION_UP} for the prior gesture.

然而框架尝试分配给View一致性的移动事件,但它并不能保证。一些事件可能被抛弃或者修改在应用之前造成事件的不一致。Views应该总是应该处理ACTION_CANCEL和忍受异常的情形例如接收一个新的ACTION_DOWN按下而没有收到在之前ACTION_UP的手势。

作者:zhfycostar 发表于2016/8/17 16:03:02 原文链接
阅读:19 评论:0 查看评论

Android Wifi开发

$
0
0

WIFI简介

WifiManager

public class WifiManager

extends Object

的java.lang.Object

↳ android.net.wifi.WifiManager

这个类提供了管理Wi-Fi连接的所有方面的主要API。通过调用得到这个类的一个实例 Context.getSystemService(Context.WIFI_SERVICE)。它涉及几类产品:

  • 配置网络列表。该列表可以查看和更新​​,单个条目的属性进行修改。
  • 当前活动的Wi-Fi网络,如果有的话。连接可以建立或拆除,并且有关网络的状态的动态信息可以查询。
  • 接入点的扫描的结果,包含足够的信息来什么接入点连接到决策。
  • 它定义了在任何类型的Wi-Fi状态变化的各种转播意向动作的名称。

这是表演的Wi-Fi具体操作时要使用的API。要执行一些与在抽象的层面与网络连接操作,使用ConnectivityManager

常用公共方法

1.获取WifiManger

        //取得WifiManager对象
        mWifiManager = (android.net.wifi.WifiManager) context
                .getSystemService(Context.WIFI_SERVICE);
        // 取得WifiInfo对象
        mWifiInfo = mWifiManager.getConnectionInfo();

2.打开wifi

    /** 打开WIFI*/
    public void openWifi() {
        if (!mWifiManager.isWifiEnabled()) {
            mWifiManager.setWifiEnabled(true);
        }
    }

3.关闭wifi

    /**关闭WIFI*/
    public void closeWifi() {
        if (mWifiManager.isWifiEnabled()) {
            mWifiManager.setWifiEnabled(false);
        }
    }

4.连接指定的wifi

    /**添加一个网络并连接*/
    public void addNetworkWPA(String wifiName ,String wifiPassword) {
        WifiConfiguration wifiConfiguration = CreateWifiInfo(wifiName, wifiPassword, 3);
        int wcgID = mWifiManager.addNetwork(wifiConfiguration);
        boolean b =  mWifiManager.enableNetwork(wcgID, true);
        Log.d(TAG,"wcgID="+wcgID);
        Log.d(TAG,"b="+b);
    }

5.断开当前连接的wifi

    /**断开指定ID的网络*/
    public void disconnectWifi(int netId) {
        mWifiManager.disableNetwork(netId);
        mWifiManager.disconnect();
    }

6.收索wifi

    /**搜索附近的wifi*/
    List<ScanResult> scanResults = wifiManager.startScanWifi();

7.完整的wifi工具类

    /**
     * Description :
     * Author : liujun
     * Email  : liujin2son@163.com
     * Date   : 2016/8/12 0012
     */
    public class WifiHelper {

        public static final String TAG="WifiHelper";
        //管理wifi
        private android.net.wifi.WifiManager mWifiManager;
        // WifiInfo对象
        private WifiInfo mWifiInfo;
        // 扫描出的
        private List<ScanResult> mWifiList;
        // 网络连接列表
        private List<WifiConfiguration> mWifiConfigurations;
        // 定义一个WifiLock
        private android.net.wifi.WifiManager.WifiLock mWifiLock;
        //管理网络连接
        private ConnectivityManager connectManager;
        //网络连接
        private NetworkInfo netInfo;
        //动态主机配置协议信息的对象,获得IP等网关信息
        private DhcpInfo dhcpInfo;

        /**
         * 构造器
         * @param context
         */
        public WifiHelper(Context context) {
            //取得WifiManager对象
            mWifiManager = (android.net.wifi.WifiManager) context
                    .getSystemService(Context.WIFI_SERVICE);
            // 取得WifiInfo对象
            mWifiInfo = mWifiManager.getConnectionInfo();
             //获取管理网络连接对象
    //        connectManager = (ConnectivityManager) context.getSystemService(CONNECTIVITY_SERVICE);
              //获取网络连接对象
    //        netInfo = connectManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
              //获取动态主机配置协议信息的对象
    //        dhcpInfo = mWifiManager.getDhcpInfo();
        }

        /**再次获取wifi信息*/
        public void reSetWifiInfo(){
            // 取得WifiInfo对象
            mWifiInfo = mWifiManager.getConnectionInfo();
        }

        /** 打开WIFI*/
        public void openWifi() {
            if (!mWifiManager.isWifiEnabled()) {
                mWifiManager.setWifiEnabled(true);
            }
        }

        /**关闭WIFI*/
        public void closeWifi() {
            if (mWifiManager.isWifiEnabled()) {
                mWifiManager.setWifiEnabled(false);
            }
        }

        /**
         * 检查当前WIFI状态
         * 1.WifiManager.WIFI_STATE_DISABLING: Wi-Fi已被禁用
         * 2.WifiManager.WIFI_STATE_DISABLING: 无线网络目前正在禁用
         * 3.WifiManager.WIFI_STATE_ENABLED: Wi-Fi已启用
         * 4.WifiManager.WIFI_STATE_ENABLING:目前正在支持Wi-Fi
         * 5.WifiManager.WIFI_STATE_UNKNOWN:无线网络处于未知状态
          */
        public int getWifiState() {
            return mWifiManager.getWifiState();
        }

        /**
         * 返回最新的接入点(wifi)的结果
         * ScanResult包含:
         * ScanResult.level : wifi信号强度,值越大信号越强
         * scanResult.frequency:wifi的频率
         * scanResult.SSID: wifi名称
         * scanResult.BSSID:wifi的BSSID
         * ......
         * @return
         */
        public List<ScanResult>  startScanWifi() {
            mWifiManager.startScan();
            // 得到扫描结果
            mWifiList = mWifiManager.getScanResults();
            return mWifiList;
        }

        /**返回请求者配置的所有网络的列表。*/
        public List<WifiConfiguration> getConfiguredNetworks(){
            // 得到配置好的网络连接
            mWifiConfigurations = mWifiManager.getConfiguredNetworks();
            return mWifiConfigurations;
        }
        /** 得到MAC地址*/
        public String getMacAddress() {
            return (mWifiInfo == null) ? "NULL" : mWifiInfo.getMacAddress();
        }

        /**得到接入点的BSSID*/
        public String getBSSID() {
            return (mWifiInfo == null) ? "NULL" : mWifiInfo.getBSSID();
        }

        /**得到IP地址*/
        public int getIPAddress() {
            return (mWifiInfo == null) ? 0 : mWifiInfo.getIpAddress();
        }
        /**得到连接的ID*/
        public int getNetworkId() {
            return (mWifiInfo == null) ? 0 : mWifiInfo.getNetworkId();
        }

        /**得到WifiInfo的所有信息包*/
        public String getWifiInfo() {
            return (mWifiInfo == null) ? "NULL" : mWifiInfo.toString();
        }

        /**添加一个网络并连接*/
        public void addNetworkWPA(String wifiName ,String wifiPassword) {
            WifiConfiguration wifiConfiguration = CreateWifiInfo(wifiName, wifiPassword, 3);
            int wcgID = mWifiManager.addNetwork(wifiConfiguration);
            boolean b =  mWifiManager.enableNetwork(wcgID, true);
            Log.d(TAG,"wcgID="+wcgID);
            Log.d(TAG,"b="+b);
        }

        /**断开指定ID的网络*/
        public void disconnectWifi(int netId) {
            mWifiManager.disableNetwork(netId);
            mWifiManager.disconnect();
        }

        /**允许之前配置的网络与关联*/
        public void enableNetwork(int netId){
            mWifiManager.enableNetwork(netId,false);
        }

        /**创建一个wifi配置信息*/
        private WifiConfiguration CreateWifiInfo(String SSID, String Password, int Type)
        {
            WifiConfiguration config = new WifiConfiguration();
            config.allowedAuthAlgorithms.clear();
            config.allowedGroupCiphers.clear();
            config.allowedKeyManagement.clear();
            config.allowedPairwiseCiphers.clear();
            config.allowedProtocols.clear();
            config.SSID = "\"" + SSID + "\"";

            WifiConfiguration tempConfig = this.IsExsits(SSID);
            if(tempConfig != null) {
                mWifiManager.removeNetwork(tempConfig.networkId);
            }
            /**连接不需要密码的wifi*/
            if(Type == 1) //WIFICIPHER_NOPASS
            {
                config.wepKeys[0] = "\"\"";
                config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
                config.wepTxKeyIndex = 0;
            }
            /**连接wep格式加密wifi*/
            if(Type == 2) //WIFICIPHER_WEP
            {
                config.hiddenSSID = true;
                config.wepKeys[0]= "\""+Password+"\"";
                config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.SHARED);
                config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);
                config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);
                config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40);
                config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104);
                config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
                config.wepTxKeyIndex = 0;
            }
            /**连接WPA格式加密wifi(就是我们平时使用的加密方法)*/
            if(Type == 3) //WIFICIPHER_WPA
            {
                config.preSharedKey = "\""+Password+"\"";
                config.hiddenSSID = true;
                config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN);
                config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);
                config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK);
                config.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP);
                //config.allowedProtocols.set(WifiConfiguration.Protocol.WPA);
                config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);
                config.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP);
                config.status = WifiConfiguration.Status.ENABLED;
            }
            return config;
        }

        private WifiConfiguration IsExsits(String str){
            List<WifiConfiguration> existingConfigs = mWifiManager.getConfiguredNetworks();
            for (WifiConfiguration existingConfig : existingConfigs){
                if (existingConfig.SSID.equals(str.trim())){
                    return existingConfig;
                }
            }
            return null;
        }

    }

8.完整的项目下载地址github

作者:u012987546 发表于2016/8/17 16:06:12 原文链接
阅读:56 评论:0 查看评论

Android编码规范之命名规则

$
0
0

命名规则

包名

包名 此包中包含
com.xx.应用名称缩写.activity 页面用到的Activity类 (activitie层级名用户界面层)
com.xx.应用名称缩写.fragment 页面用到的fragment
com.xx.应用名称缩写.base 基础共享的类
com.xx.应用名称缩写.config 通用的配置
com.xx.应用名称缩写.global 全局的监听实现类等
com.xx.应用名称缩写.adapter 页面用到的Adapter类 (适配器的类)
com.xx.应用名称缩写.util 此包中包含:公共工具方法类(util模块名)命名与第三方的utils作区分
com.xx.应用名称缩写.bean 下面可分:vo(本地类)、bo(远程类)、dto(传递引用类)
com.xx.应用名称缩写.model 此包中包含:模型类
com.xx.应用名称缩写.db 数据库操作类
com.xx.应用名称缩写.view 自定义的View类等,可通用,与app业务无关的
com.xx.应用名称缩写.widget 自定义的View类等,与app业务相关的
com.xx.应用名称缩写.service Service服务,后台服务
com.xx.应用名称缩写.receiver BroadcastReceiver服务,接收通知

类名

描述 例如
Activity 类 Activity为后缀标识 欢迎页面类WelcomeActivity
Adapter类 Adapter 为后缀标识 新闻详情适配器 NewDetailAdapter
解析类 Parser为后缀标识 首页解析类HomePosterParser
工具方法类 Util或Manager为后缀标识(与系统或第三方的Utils区分)或功能+Util 日志工具类: LogUtil(Logger也可)打印工具类:PrinterUtil
管理类 Manager为后缀标识 ThreadPoolManager
数据库类 以DBHelper后缀标识 新闻数据库:NewDBHelper
Service类 以Service为后缀标识 时间服务TimeServiceBroadcast
Receiver类 以Receiver为后缀标识 推送接收JPushReceiver
ContentProvider 以Provider为后缀标识
自定义基类 以Base开头 BaseActivity,BaseFragment
实体类 模块 + 类型 + 后缀(BO/VO/DTO) 例如:订单详情实体类及订单详情内的商品实体类 OrderDetailBO、OrderGoodsBO;例如:订单列表实体类及订单列表内的商品实体类 OrderBO/OrderItemBO、OrderGoodsBO/OrderItemGoodsBO;当然,如果订单详情和订单列表内的OrderGoodsBO一致,则可以单独提出来给2个实体类通用
自定义view 功能描述 + View 例如:可以展开收缩的view,CollapsibleView
自定义widget 模块名 + 类型(List/Info) + Widget 例如:订单详情内的商品列表,OrderDetailGoodsListWidget; 或者:在widget内新增order目录,放order目录下的GoodsListWidget

接口名

  • 回调事件监听接口 On + 对象 + Click/Select,例如:OnItemClick/OnMenuSelect
  • 其他接口 功能描述 + Impl

方法名

方法 说明
initXX() 初始化相关方法,使用init为前缀标识,如初始化布局initView()
isXX() checkXX() 方法返回值为boolean型的请使用is或check为前缀标识
getXX() 返回某个值的方法,使用get为前缀标识
handleXX() 对数据进行处理的方法,尽量使用handle为前缀标识
displayXX()/showXX() 弹出提示框和提示信息,使用display/show为前缀标识
saveXX() 与保存数据相关的,使用save为前缀标识
resetXX() 对数据重组的,使用reset前缀标识
clearXX() 清除数据相关的
removeXXX() 清除数据相关的
drawXXX() 绘制数据或效果相关的,使用draw前缀标识

类的属性命名

1.常量名
  • 常量名命名模式为CONSTANT_CASE,全部字母大写,用下划线分隔单词

  • 常量一般统一放置在config目录下的AppConfig文件内

  • 同类型的常量,通常放置在同一个内部类中
  • 示例参考 com.xiaoq.sample.mylib.code包下面的Appconfig
2.非常量字段名
  • 非公有,非静态字段命名以m开头。
    int mPackagePrivate;
    private int mPrivate;
    protected int mProtected;

  • 静态字段命名以s开头。
    private static MyClass sSingleton;

  • 公有非静态字段命名以p开头。
    public int pField;

  • 公有静态字段(全局变量)命名以g开头。
    public static int gField;

  • UI控件变量命名参考 附录的 UI控件缩写表

3.量词变量后缀
  • First 一组变量中的第一个
  • Last 一组变量中的最后一个
  • Next 一组变量中的下一个变量
  • Prev 一组变量中的上一个
  • Cur 一组变量中的当前变量。
  • 例如:mCustomerStrFirst mCustomerStrLast
4.集合添加如下后缀:List、Map、Set
5.数组添加如下后缀:Arr
6.临时变量
  • 临时变量通常被取名为i,j,k,m和n,它们一般用于整型;
  • c,d,e,它们一般用于字符型;

资源文件命名规范

1.资源布局文件(XML文件(layout布局文件)):
  • activity_main.xml
  • frament_main.xml
  • Dialog命名:dialog_描述.xml,例如:dialog_hint.xml
  • PopupWindow命名:ppw_描述.xml,例如:ppw_info.xml
  • 列表项命名:item_描述.xml
    • 通用列表项:item_city.xml
    • listview:list_item_city.xml
    • gridview:grid_item_city.xml
    • recyclerview:recycler_item_city.xml
    • 自定义类似listview:TabLayout:tab_item_city.xml
  • 包含项命名:模块_(位置).xml
    • 位置:top、 btm、 left、 right
    • 例如:activity_main_btm.xml、fragment_main_top.xml
    • 通用的包含项命名采用:base_(位置)_项目名称缩写_描述.xml
    • 描述:title、 content、 header、 footer
    • 例如:base_top_xxxx_title.xml、base_btm_xxxx_header.xml
    • 例如:全项目通用 base_top.xml base_btm.xml
  • 自定义Widget(布局命名)
    • widget_ + View功能描述/模块 + 类型(list/info) + .xml
    • 例如:订单详情内的商品列表 widget_order_detail_goods_list.xml
  • 特殊自定义组件/控件
    • 例如: com.xiaoq.widget.tips.TipsManager的loading布局/empty布局/重试布局
    • 通用:base_tips_loading.xml、base_tips_empty、base_tips_retry
    • 各个activity内:activity_描述_tips_loading.xml、activity_main_tips_loading.xml
    • fragment:fragment_main_tips_loading.xml
    • view:activity_main_xxx_view_tips_loading.xml、fragment_main_xxx_view_tips_loading.xml,xxx 为view的描述

2.资源文件(图片mipmap及drawable文件夹下):
  • 全部小写,采用下划线命名法,加前缀区分
  • 通用规则
名称 功能
bg_head 背景图片使用bg_功能_说明
def_search_cell 默认图片使用def_功能_说明
ic_more_help 图标图片使用ic_功能_说明
seg_list_line 具有分隔特征的图片使用seg_功能_说明 (false)不可用效果
sel_ok 选择图标使用sel_功能_说明

- 命名模式:可加后缀 _small 表示小图, _big 表示大图,逻辑名称可由多个单词加下划线组成
- 采用以下规则:
- 用途_模块名_逻辑名称
- 用途_模块名_颜色
- 用途_逻辑名称
- 用途_颜色
- 说明:用途也指控件类型(具体见UI控件缩写表)
- 例如:
- btn_main_home.png 按键
- divider_maket_white.png 分割线
- btn_red.png 红色按键
- btn_red_big.png 红色大按键
- divider_white.png 白色分割线
- 如果有多种形态如按钮等除外如 btn_xx.xml(selector)

  • normal、pressed、disbled、checked 较常用
名称 功能
btn_xx 按钮图片使用btn_整体效果(selector)
btn_xx_normal 按钮图片使用btn_正常情况效果
btn_xx_pressed 按钮图片使用btn_点击时候效果
btn_xx_focused state_focused聚焦效果
btn_xx_disabled state_enabled (false)不可用效果
btn_xx_checked state_checked选中效果
btn_xx_selected state_selected选中效果
btn_xx_hovered state_hovered悬停效果
btn_xx_checkable state_checkable可选效果
btn_xx_activated state_activated激活的
btn_xx_windowfocused state_window_focused

3.动画文件(anim文件夹下):
  • 全部小写,采用下划线命名法,加前缀区分。
  • 具体动画采用以下
    • 规则:模块名_逻辑名称
    • 例如:
      • refresh_progress.xml
      • market_cart_add.xml
      • market_cart_remove.xml
  • 普通的tween动画采用如下表格中的命名方式,前面为动画的类型,后面为方向
动画命名例子 规范写法
fade_in 淡入
fade_out 淡出
push_down_in 从下方推入
push_down_out 从下方推出
push_left 推向左方
slide_in_from_top 从头部滑动进入
zoom_enter 变形进入
slide_in 滑动进入
shrink_to_middle 中间缩小

4.styles.xml文件的命名
  • 规则:模块名+逻辑名称
    • main_tabBottom
    • order_detail_goodsItem
    • order_list_goodsImg

5. layout中的id命名
  • 命名模式为:View缩写_view的逻辑名称,可以使用AndroidStudio的插件Android Studio Prettify 自动生成findViewById
  • 例如:tv_Sender_Mobile(展示联系人手机的TextView)
  • 可参考 com.xiaoq.sample.mylib.code.CodeActivity
  • 当然,全部小写的命名是最标准的,不过为了要适应Prettify插件自动生成满足要求的局部变量,调整为小驼峰命名法,并用下划线分割

附录

注意

其他未在文档内标准的规范则以AndroidStudio自动生成的为准

UI控件缩写表
控件 缩写 例子
LinearLayout ll llFriend或者mFriendLL
RelativeLayout rl rlMessage或mMessageRL
FrameLayout fl flCart或mCartFL
TableLayout tl tlTab或mTabTL
Button btn btnHome或mHomeBtn
ImageButton ibtn btnPlay或mPlayIBtn
TextView tv tvName或mNameTV
EditText et etName或mNameET
ListView lv lvCart或mCartLV
ImageView iv ivHead或mHeadIV
GridView gv gvPhoto或mPhotoGV
常见英文单词缩写
名称 缩写
icon ic (主要用在app的图标)
color cl(主要用于颜色值)
divider di(主要用于分隔线,不仅包括Listview中的divider,还包括普通布局中的线)
selector sl(主要用于某一view多种状态,不仅包括Listview中的selector,还包括按钮的selector)
average avg
background bg(主要用于布局和子布局的背景)
buffer buf 缓冲
control ctrl
delete del
document doc
error err
escape esc
increment inc 增量
infomation info
initial init
image img
Internationalization I18N 国际化
length len
library lib
message msg
password pwd
position pos
server srv
string str
temp tmp
window wnd(win)
作者:xiao198504 发表于2016/8/17 16:10:02 原文链接
阅读:41 评论:0 查看评论

Android插件化之资源动态加载

$
0
0
Android插件化之资源动态加载
一.概述
Android插件化的一个重要问题就是插件资源访问问题,先列出会面对的问题
1.如何加载插件资源
2.如何处理插件资源与宿主资源的处突:插件化资源问题要做到的效果是,如果我们要获取的资源在插件中找得到,则加载优先加载插件的,如果找不到,则到宿主资源中找。这样能做到动态更新的效果。
3.如何确保插件和宿主使用到的是被修改过的资源。




二.原理分析
在做一件事之前必须先弄清楚原理,所以,这里先要弄清楚Android的资源体系原理。
1.资源链
 
Context:一个apk里面其context的个数为application+Activity+service的总和,因为他们都 是继承context的,然而context只是一个抽象类,其真正的实现类是ContextImpl, 那。拿Activity来说,在Activity的启动流程中,会在ActivityThread的 performLaunchActivity()方法中调用Activity的attach方法把ContextImp实例传给Activity(即赋值给Activity内的成员变量mBase)。
 


Resources:ContextImpl内有一个Resources的成员变量mResources,代表的是应用的资源, 我们平时在调用getResources()方法获取到的是该Resources。

AssetManager:Resources内部的一个重要成员是AssetManager(mAssets),其指向的是apk 的资源路径,资源的获取最终都是通过它来得到的。这里需要注意的是AssetManager并不是Resources独立持有的,也就是说系统在获取资源的时候不一定是通过Resources获取的,有时候是直接通过AssetManager来获取,比如TypedArray,之前就踩过这个坑。 
2.Android是如何构造一个应用的资源的,并且是如何传递给我们使用的,这个要讲的东西非常的多,可以看另一篇文章,这里主要讲资源插件化。


三.问题的解决方案
1.加载插件资源
资源的加载最后是通过AssetManager内的一个方法addAssetPath(String path)
 
该方法接收的参数是插件apk的路径,内部会调用native方法把插件apk对应的资源加载进来。然而该方法是hide的,我们不能直接调用,所有只能通过反射。
 
这样就成功构造出一个指向插件资源的AssetManager。当然这时候还不能使用,还要调用AssetManager的ensureStringBlocks()方法来初始化其内部参数,同样得使用反射。
 


2.如何解决插件资源与宿主资源的处突
如果使用到的资源,插件和宿主都同时存在,则使用插件的资源;如果使用到的资源只有插件有,则使用插件的;如果使用到的资源只有宿主有的,则使用宿主的。
AssetManager的addAssetPath()方法调用native层AssetManager对象的addAssetPath()方法,通过查看c++代码可以知道,该方法可以被调用多次,每次调用都会把对应资源添加起来,而后来添加的在使用资源是会被首先搜索到。可以怎么理解,C++层的AssetManager有一个存放资源的栈,每次调用addAssetPath()方法都会把资源对象压如栈,而在读取搜索资源时是从栈顶开始搜索,找不到就往下查。所以我们可以这样来处理AssetManager并得到Resources
 
其中dexPath2为宿主apk路径,dexPath为插件apk路径,superRes为宿主资源,resources为融合插件与宿主的资源。


3. 如何确保插件和宿主使用到的是被修改过的资源:
这是很重要的一步,之前我们已经成功获取资源并对其进行修饰,现在要做的是用它替换掉Android为我们生成的那个资源,这就是hook的思想。
使用到资源的地方归纳起来有两处,一处是在Java代码中通过Context.getResources获取,一处是在xml文件(如布局文件)里指定资源,其实xml文件里最终也是通过Context来获取资源的只不过是他一般获取的是Resources里的AssetManager。所以,我们可以在Context对象被创建后且还未使用时把它里面的Resources(mResources)替换掉。之前说过,整个应用的Context数目等于Application+Activity+Service的数目,Context会在这几个类创建对象的时候创建并添加进去。而这些行为都是在ActivityTHread和Instrumentation里做的。
以Activity为例,步骤如下:


a: Activity对象的创建是在ActivityThread里调用Instrumentation的newActivity方法
ActivityThread:
 
Instrumentation:
 


b: Context对象的创建是在ActivityThread里调用createBaseContextForActivity方法
ActivityThread:
 


c: Activity绑定Context是在ActivityThread里调用Activity对象的attach方法,其中appContext就是上面创建的Context对象
ActivityThread:
 


d: Activity的onCreate()方法的回调是在ActivityThread里调用Instrumentation的callActivityOnCreate()方法
ActivityThread:
 


替换掉Activity里Context里的Resources最好要早,基于上面的观察,我们可以在调用Instrumentation的callActivityOnCreate()方法时把Resources替换掉。那么问题又来了,我们如何控制callActivityOnCreate()方法的执行,这里又得使用hook的思想了,即把ActivityThread里面的Instrumentation对象(mInstrumentation)给替换掉,同样得使用反射。步骤如下
a: 获取ActivityThread对象
ActivityThread里面有一个静态方法,该方法返回的是ActivityThread对象本身,所以我们可以调用该方法来获取ActivityTHread对象
 
然而ActivityThread是被hide的,所以得通过反射来处理,处理如下:
 


b: 获取ActivityThread里的Instrumentation对象
 


c: 构建我们自己的Instrumentation对象,并从写callActivityOnCreate方法
在callActivityOnCreate方法里要先获取当前Activity对象里的Context(mBase),再获取Context对象里的Resources(mResources)变量,在把mResources变量指向我们构造的Resources对象,做到移花接木。
 
MyInstrumentation:
 


d: 最后,使ActivityThread里面的mInstrumentation变量指向我们构建的MyInstrumentation对象。
 


代码
 




四.应用
资源动态加载的一个应用当然就是Android插件化方面的使用。还有一个应用就是换肤功能,只需要在在工程里添加这些代码(当然还要处理一些逻辑),然后用户想要给应用换皮肤,主题等,即可从后台下载插件apk,放在指定文件夹就可以关系应用的资源,起到换肤的效果。当然,资源动态加载还有其他应用方法,自己琢磨咯!!!


四.存在问题
1.兼容性问题,因为hook要使用反射,从而来获取系统hide或类的私有属性。把它们隐藏是因为它们的不稳定性,如果哪天Google觉得那个变量的名称起的不吉利给改了,那就报错了。当然解决方法还是有的,就是为不同的API写不同的代码。
2.R方面的问题。当我们添加了一个资源(如在String.xml里添加了一个String),则系统会为我们在R里面为该资源生成一个int型的id与之对应,使用的时候是根据该id找到对应的资源。资源id是按照资源名称的字典顺序来递增的。拿String来说。
假如我们的String.xml里声明了名称为za,zb的资源
 
则会在R里面生成相应的id
 


基于上面的观察,我们会发现一个问题:举个例子
宿主资源情况为:存在za(id=0x7f060004)  zb(id=0x7f060005)
插件资源情况为:存在za(id=0x7f060004)  zab(id=0x7f060005)   ab(0x7f060006)


这时候在宿主里获取资源zb,则根据上面所说,会根据id=0x7f060005先存在插件资源,这时候得到的是zab而不是zb,这就出错了。
解决方案有,在插件中如果有添加新的资源,则其命名要安装字典排序在原有的资源下递增。当然也有其他方案,自己琢磨吧。
作者:sggdjfkf147896325 发表于2016/8/17 16:26:36 原文链接
阅读:43 评论:0 查看评论

2、数据类型、运算符和表达式

$
0
0

概述

数据类型、运算符和表达式在任何的计算机语言中都比较重 要的,在面向对象的Objective-C语言中,除了常规的基本类型,还有对象类型等。运算符和表达式完全遵守C语言 规范。

数据类型

Objective-C数据类型可以分为:基本数据类型、对象类型和i d类型。
基本数据类型有:int、float、double和char类型。 对象类型就是类或协议所声明的指针类型,例如:
NSAutoreleasePool * pool,其中NSAutoreleasePool是一个类, NSAutoreleasePool *是它指针类型。 id类型可以表示任何类型,一般只是表示对象类型,不表示 基本数据类型,所以刚才的变量pool也可以声明为id pool。

int类型

int类型代表整数,它的十六进制表示方式:0xFFED0D,在使 用NSLog函数中格式化字符串使用%i表示十进制的整数,%o(字母o)表示8进制整数,%#x表示十六进制整数。它的取 值范围是与设备相关的,无法一概而论。

NSLog(@"integerVar = %i", integerVar);

float类型

float类型代表单精度浮点数,要表示float类型浮点数,可以 在数值后面加上f或F,例如:13.5f。float浮点数也可以用科学计数法表示,例如:1.7e4。NSLog函数中格式化字符串:%f 表示浮点数,%e表示科学计数法,%g表示浮点数。

NSLog(@“floatingVar = %f”, floatingVar);

double类型

double类型代表双精度浮点数,与float类型很相似,占用的 字节空间double类型大体上是float类型的两倍。大多数计算机是用64位表示double类型。NSLog函数中格式化字符串,与 float的%f、%e和 %g相同。

NSLog(@"doubleVar = %e", doubleVar);

char类型

char类型代表字符类型,存放点个字符,用单引号引用起来 。例如: ‘A’,如果要表示一些特殊字符,要使用转义字 符“\”。

NSLog(@"charVar = %c", charVar);

数据类型实例

 #import <Foundation/Foundation.h>
int main (int argc, const char * argv[]) {
      NSAutoreleasePool * pool =[[NSAutoreleasePool alloc] init];
      int integerVar = 100;
      float floatingVar = 331.79;
      double doubleVar = 8.44e+11;
      char charVar = 'W';
      NSLog(@"integerVar = %i", integerVar);
      NSLog(@"floatingVar = %f", floatingVar);
      NSLog(@"doubleVar = %e", doubleVar);
      NSLog(@"doubleVar = %g", doubleVar);
      NSLog(@"charVar = %c", charVar);
      [pool drain];
    return 0;
}

数据类型限定词

Obejctive-C中数据类型可以在int、float、double和char类型 前面加上限定词,限定词有:long、long long、short、 unsigned和signed,这些限定词从而增强了基本类型。

数据类型限定词

long int,在大部分计算机中代表32位整数,在整数后面 加L(或l)表示,例如:long int numberOfPoints = 131071100L,NSLog函数中格式化字符串使用%li表示;
• long long int可以指定更加宽泛的整数类型,保证变量至 少64位宽度。NSLog函数中格式化字符串使用%lli表示;
• long double ,可以指定更加宽泛的double类型,要显示 这个可以在尾部使用L(大小写)表示,1.234e+7L。NSLog 函数中格式化字符串使用%Lf、%Le和%Lg表示;

数据类型限定词

short int用来指定存放相对小的整数,一般是占用int类 型的一半。大部分计算机是16位;
• unsigned int,告诉编译器只是接受整数,在数值之后 放字母u(或U)表示,例如:0x00ffU;编写整数时候,可 以将字母u(或U)和l(或L)组合起来,例如:20000UL;
• signed char,代表的字符与编译器有关,一般也作为无 符合整数使用。

布尔类型和枚举类型

Objective-C还有两种数据类型是以int类型在计算机内部存 储的,它们是:布尔类型和枚举类型。
• 布尔类型是_Bool(别名BOOL),取值范围1或0,其中1可以用 TRUE和YES表示,0可以用FALSE和NO表示。
• 枚举类型,如果需要定义一组相关常量,可以采用枚举类型 ,把这些常量定义成一个类型,例如游戏在上、下、左、右 方向,可以枚举类型:
• enum direction {up,down,left,right}; • 其中up从0开始,down是1,依次类推加1

枚举类型实例

  #import <Foundation/Foundation.h>
int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool =
         [[NSAutoreleasePool alloc] init];
      enum week {
            Mon, Tue, Wed, Thu, Fri, Sat, Sun
      };
      int days,aweek;
      NSLog(@"Enter week number : ");
      scanf("%i", &aweek);
      ... ...
    [pool drain];
    return 0;
}

说明

其中定义了week的枚举类型,函数scanf(“%i”, &aweek)是C 中标准函数库,用于从终端读取键盘输入,%i是指定接收的类型,&aweek是传递aweek的地址给函数,便于接收键盘输 入内容。

数据类型转换

• 按照数据类型占用存储不同可以自动类型转换或强制类型 转换,总的原则是小存储容量数据类型可以自动转换成为大 存储容量数据类型。
• 不同类型数据间按照下面关系的从左到右(从低到高)自 动转换:
• _Bool、char、short int、枚举类型 -> int ->long int- >long long-> float -> double -> long double。

类型转换先后顺序表
 类型转换先后顺序表

实例

• 如果有下面的表示式,其中,f是float类型,i为int类型,l 为long int 类型,s为short int类型,结果是什么类型?
• f * i + l /s
• 运行结果为float类型,这是因为f是float其它的操作数与 float运算其结果就是float类型。

强制类型转换

• 如果遵守类型转换是右到左情况,就需要强制类型转换了 ,强制类型转换语法形式上很简单,就是在数据前面加上(目标类型),但是这种转换是存在风险的,有可能造成数据 的丢失,需要谨慎进行。例如:

long int l = 6666666666;
NSLog(@"l = %li",l);
int i = (int)l;
NSLog(@"i = %i",i);
运行结 果
l = 6666666666
i = -1923267926

实例

int total = 3446;
int n = 6;
float average = total / n;

运行结果 average结果是574

int total = 3446;
int n = 6;
float average = (float)total / n;

average结果是574.333

常量与变量

• Objective-C中声明常量使用关键字const: • const double PI = 3.141592654;
• Objective-C中变量可以分为成员变量、局部变量和全局 变量。

常量与变量

int gCounter;
@interface MyObject : NSObject {
      int counter;
}@end
@implementation MyObject
-(void) print {
    int cter = 0;
    NSLog(@"%i", cter );
}@end

运算符和表达式

运算符可以分成如下几种:
• 算术运算符,+,―,*,/,%,++,――
• 关系运算符,>,<,>=,<=,==,!=
• 位运算符,&,|,^,~ ,>>,<<
• 赋值运算符,+=,―=,*=,/=
• 条件运算符,? :

短路与和短路或

布尔逻辑运算符中有两个比较特殊的运算 符号,“&&”和“||”,其中“&&”为短路与,如果对两个表达式进行计算,若第1个表达式的值为“假”,则与第2个 表达式的值无关,结果肯定为“假”,所以此时第2个表达 式不再计算。
“||”为短路或,如果对两个表达式进行 计算,若第1个表达式的值为“真”,则与第2个表达式的值无关,结果肯定为“真”,所以此时第2个表达式不再计算。

短路与和短路或

int i = 0;
int a = 10;
int b = 9;
if ((a > b) | (i++ == 1)) {
      NSLog(@" a > b");
} else {
      NSLog(@" a < b");
      NSLog(@"i = %i", i);
}
  结果是a > b和i=1 

位运算符

位运算符有如下几个运算符:&,|,^,~ ,>>,<<,其中& 是按位与,|是按位或,^是异或,~是取反,>>是右位移,< <是左位移。

位运算符实例

• 假设有两个二进制数16位整数(short int),a=1001110110011101和b=0011100100111001,则有如下结果 ,它们的运行结果如下:
位运算符实例
条件运算符
• 条件运算符的语法格式为:
• 布尔表达式?返回值1:返回值2
• 当布尔表达式的值为真时,返回表达式1的值,否则返回表达 式2的值。

int i = 70; int i3 =70 NSString *res = i3 > 60 ? @"及格" : @"不及格" ;
NSLog(@"res = i3 > 60 %@ ", res);

结果是“及格”

作者:syc434432458 发表于2016/8/17 16:44:24 原文链接
阅读:36 评论:0 查看评论

Android多媒体开发

$
0
0

多媒体概念

  • 文字、图片、音频、视频

计算机图片大小的计算

图片大小 = 图片的总像素 * 每个像素占用的大小

  • 单色图:每个像素占用1/8个字节
  • 16色图:每个像素占用1/2个字节
  • 256色图:每个像素占用1个字节
  • 24位图:每个像素占用3个字节

加载大图片到内存

Android系统以ARGB表示每个像素,所以每个像素占用4个字节,很容易内存溢出

对图片进行缩放

  • 获取屏幕宽高

    Display dp = getWindowManager().getDefaultDisplay();
    int screenWidth = dp.getWidth();
    int screenHeight = dp.getHeight();
    
  • 获取图片宽高

    Options opts = new Options();
    //请求图片属性但不申请内存
    opts.inJustDecodeBounds = true;
    BitmapFactory.decodeFile("sdcard/dog.jpg", opts);
    int imageWidth = opts.outWidth;
    int imageHeight = opts.outHeight;
    
  • 图片的宽高除以屏幕宽高,算出宽和高的缩放比例,取较大值作为图片的缩放比例

    int scale = 1;
    int scaleX = imageWidth / screenWidth;
    int scaleY = imageHeight / screenHeight;
    if(scaleX >= scaleY && scaleX > 1){
        scale = scaleX;
    }
    else if(scaleY > scaleX && scaleY > 1){
        scale = scaleY;
    }
    
  • 按缩放比例加载图片

    //设置缩放比例
    opts.inSampleSize = scale;
    //为图片申请内存
    opts.inJustDecodeBounds = false;
    Bitmap bm = BitmapFactory.decodeFile("sdcard/dog.jpg", opts);
    iv.setImageBitmap(bm);
    

在内存中创建图片的副本

直接加载的bitmap对象是只读的,无法修改,要修改图片只能在内存中创建出一个一模一样的bitmap副本,然后修改副本

    //加载原图
    Bitmap srcBm = BitmapFactory.decodeFile("sdcard/photo3.jpg");
    iv_src.setImageBitmap(srcBm);

    //创建与原图大小一致的空白bitmap
    Bitmap copyBm = Bitmap.createBitmap(srcBm.getWidth(), srcBm.getHeight(), srcBm.getConfig());
    //定义画笔
    Paint paint = new Paint();
    //把纸铺在画版上
    Canvas canvas = new Canvas(copyBm);
    //把srcBm的内容绘制在copyBm上
    canvas.drawBitmap(srcBm, new Matrix(), paint);

    iv_copy.setImageBitmap(copyBm);

对图片进行特效处理

  • 首先定义一个矩阵对象

    Matrix mt = new Matrix();
    
  • 缩放效果

    //x轴缩放1倍,y轴缩放0.5倍
    mt.setScale(1, 0.5f);
    
  • 旋转效果

    //以copyBm.getWidth() / 2, copyBm.getHeight() / 2点为轴点,顺时旋转30度
    mt.setRotate(30, copyBm.getWidth() / 2, copyBm.getHeight() / 2);
    
  • 平移

    //x轴坐标+10,y轴坐标+20
    mt.setTranslate(10, 20);
    
  • 镜面

    //把X坐标都变成负数
    mt.setScale(-1, 1);
    //图片整体向右移
    mt.postTranslate(copyBm.getWidth(), 0);
    
  • 倒影

    //把Y坐标都变成负数
    mt.setScale(1, -1);
    //图片整体向下移
    mt.postTranslate(0, copyBm.getHeight());
    

画画板

记录用户触摸事件的XY坐标,绘制直线
* 给ImageView设置触摸侦听,得到用户的触摸事件,并获知用户触摸ImageView的坐标

    iv.setOnTouchListener(new OnTouchListener() {

        @Override
        public boolean onTouch(View v, MotionEvent event) {
            // TODO Auto-generated method stub
            switch (event.getAction()) {
            //触摸屏幕
            case MotionEvent.ACTION_DOWN:
                //得到触摸屏幕时手指的坐标
                startX = (int) event.getX();
                startY = (int) event.getY();
                break;
            //在屏幕上滑动
            case MotionEvent.ACTION_MOVE:
                //用户滑动手指,坐标不断的改变,获取最新坐标
                int newX = (int) event.getX();
                int newY = (int) event.getY();
                //用上次onTouch方法得到的坐标和本次得到的坐标绘制直线
                canvas.drawLine(startX, startY, newX, newY, paint);
                iv.setImageBitmap(copyBm);
                startX = newX;
                startY = newY;
                break;

            }
            return true;
        }
    });

* 刷子效果,加粗画笔

    paint.setStrokeWidth(8);

* 调色板,改变画笔颜色

    paint.setColor(Color.GREEN);

* 保存图片至SD卡

    FileOutputStream fos = null;
    try {
        fos = new FileOutputStream(new File("sdcard/dazuo.png"));
    } catch (FileNotFoundException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    //保存图片
    copyBm.compress(CompressFormat.PNG, 100, fos);

* 系统每次收到SD卡就绪广播时,都会去遍历sd卡的所有文件和文件夹,把遍历到的所有多媒体文件都在MediaStore数据库保存一个索引,这个索引包含多媒体文件的文件名、路径、大小
* 图库每次打开时,并不会去遍历sd卡获取图片,而是通过内容提供者从MediaStore数据库中获取图片的信息,然后读取该图片
* 系统开机或者点击加载sd卡按钮时,系统会发送sd卡就绪广播,我们也可以手动发送就绪广播

    Intent intent = new Intent();
    intent.setAction(Intent.ACTION_MEDIA_MOUNTED);
    intent.setData(Uri.fromFile(Environment.getExternalStorageDirectory()));
    sendBroadcast(intent);

撕衣服

  • 原理:把穿内衣和穿外衣的照片重叠显示,内衣照在下面,用户滑动屏幕时,触摸的是外衣照,把手指经过的像素都置为透明,内衣照就显示出来了

     iv.setOnTouchListener(new OnTouchListener() {
    
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                int newX = (int) event.getX();
                int newY = (int) event.getY();
                //把指定的像素变成透明
                copyBm.setPixel(newX, newY, Color.TRANSPARENT);
                iv.setImageBitmap(copyBm);
                break;
    
            }
            return true;
        }
    });
    
  • 每次只设置一个像素点太慢,以触摸的像素为圆心,半径为5画圆,圆内的像素全部置为透明

    for (int i = -5; i < 6; i++) {
        for (int j = -5; j < 6; j++) {
            if(Math.sqrt(i * i + j * j) <= 5)
                copyBm.setPixel(newX + i, newY + j, Color.TRANSPARENT);
        }
    }
    

音乐播放器

播放服务

  • 播放音频的代码应该运行在服务中,定义一个播放服务MusicService
  • 服务里定义play、stop、pause、continuePlay等方法

        private void play() {
            // TODO Auto-generated method stub
            player.reset();
            try {
                player.setDataSource("sdcard/bzj.mp3");
                player.prepare();
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } 
            player.start();
    
        }
        private void pause() {
            player.pause();
        }
        private void stop() {
            player.stop();
        }
        private void continuePlay() {
            player.start();
        }
    
  • 把这几个方法抽取成一个接口MusicInterface
  • 定义一个中间人类,继承Binder,实现MusicInterface
  • 先start启动MusicService,再bind

    Intent intent = new Intent(this, MusicService.class);
    startService(intent);
    bindService(intent, conn, BIND_AUTO_CREATE);
    

根据播放进度设置进度条

  • 获取当前的播放时间和当前音频的最长时间

    int currentPosition = player.getCurrentPosition();
    int duration = player.getDuration();
    
  • 播放进度需要不停的获取,不停的刷新进度条,使用计时器每500毫秒获取一次播放进度
  • 发消息至Handler,把播放进度放进Message对象中,在Handler中更新SeekBar的进度

    Timer timer = new Timer();
    timer.schedule(new TimerTask() {
    
        @Override
        public void run() {
            int currentPosition = player.getCurrentPosition();
            int duration = player.getDuration();
            Message msg = Message.obtain();
            //把播放进度存入Message中
            Bundle data = new Bundle();
            data.putInt("currentPosition", currentPosition);
            data.putInt("duration", duration);
            msg.setData(data);
            MainActivity.handler.sendMessage(msg);
        }
    }, 5, 500);
    
  • 在Activity中定义Handler

    static Handler handler = new Handler(){
        public void handleMessage(android.os.Message msg) {
            //取出消息携带的数据
            Bundle data = msg.getData();
            int currentPosition = data.getInt("currentPosition");
            int duration = data.getInt("duration");
    
            //设置播放进度
            sb.setMax(duration);
            sb.setProgress(currentPosition);
        };
    };
    

拖动进度条改变播放进度

     //给sb设置一个拖动侦听
     sb.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
        //停止拖动时调用
        @Override
        public void onStopTrackingTouch(SeekBar seekBar) {
            // TODO Auto-generated method stub
            int progress = seekBar.getProgress();
            mi.seekTo(progress);
        }
        //开始拖动时调用           
        @Override
        public void onStartTrackingTouch(SeekBar seekBar) {
            // TODO Auto-generated method stub

        }
        //拖动的时候不断调用         
        @Override
        public void onProgressChanged(SeekBar seekBar, int progress,
                boolean fromUser) {
            // TODO Auto-generated method stub

        }
    }); 

视频播放器

SurfaceView

  • 对画面的实时更新要求较高
  • 双缓冲技术:内存中有两个画布,A画布显示至屏幕,B画布在内存中绘制下一帧画面,绘制完毕后B显示至屏幕,A在内存中继续绘制下一帧画面
  • 播放视频也是用MediaPlayer,不过跟音频不同,要设置显示在哪个SurfaceView

    SurfaceView sv = (SurfaceView) findViewById(R.id.sv);
    SurfaceHolder sh = sv.getHolder();
    
    MediaPlayer player = new MediaPlayer();
    player.reset();
    try {
        player.setDataSource("sdcard/2.3gp");
        player.setDisplay(sh);
        player.prepare();
    } catch (Exception e) {
        e.printStackTrace();
    }
    player.start();
    
  • SurfaceView是重量级组件,可见时才会创建
  • 给SurfaceHolder设置CallBack,类似于侦听,可以知道SurfaceView的状态

    sh.addCallback(new Callback() {
        //SurfaceView销毁时调用
        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            // TODO Auto-generated method stub
    
        }
        //SurfaceView创建时调用
        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            // TODO Auto-generated method stub
    
        }
    
        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width,
                int height) {
            // TODO Auto-generated method stub
    
        }
    });
    
  • SurfaceView一旦不可见,就会被销毁,一旦可见,就会被创建,销毁时停止播放,再次创建时再开始播放

摄像头

  • 启动系统提供的拍照程序

    //隐式启动系统提供的拍照Activity
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    //设置照片的保存路径
    File file = new File(Environment.getExternalStorageDirectory(), "haha.jpg"); 
    intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file)); 
    startActivityForResult(intent, 0);
    
  • 启动系统提供的摄像程序

    Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
    
    File file = new File(Environment.getExternalStorageDirectory(), "haha.3gp"); 
    intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file)); 
    //设置保存视频文件的质量
    intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1);
    startActivityForResult(intent, 0);
    
作者:u013475983 发表于2016/8/17 16:52:50 原文链接
阅读:147 评论:0 查看评论

Android自定义View(二)画一个表

$
0
0

        书接上回:Android自定义View(一)关于super、this和构造方法

        这篇自定义个表盘的CustomWatchView给大家瞅瞅,主要是会说到Canvas和Paint这两个东西。

        先上个图看看效果,大概写了不到150行代码:

        这里大概分成两步:1是获取属性值;2是按照属性值绘图;

        如果不需要在xml文件配置属性,那么在自定义类里面公开几个属性的setter就好了;Android嘛,我们当然是倾向xml配置了。

        在res/values文件夹下新建attrs.xml,我们这里只是用到了主题颜色和表的半径两个属性:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CustomWatchView">
        <attr name="bodyColor" format="color" />
        <attr name="radius" format="float" />
    </declare-styleable>
</resources>

        name在自定义类会当做标识来获取整个styleable,<attr/>标签定义了xml使用的名称name,和对应的值类型format。

        现在有了属性定义了,接着往下走。

 

        新建一个类命名为CustomWatchView,添加成员变量和构造方法:

public class CustomWatchView extends View{
    //画笔工具
    private Paint mPaint;
    //图像颜色
    private int mColor;
    //表盘半径
    private float radius;

    private float mHour = 0;
    private float mMinutes = 0;
    private float mSecond = 0;

    public CustomWatchView(Context context) {
        this(context,null);
    }
    public CustomWatchView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }
    public CustomWatchView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint = new Paint();//实例化画笔
		//获取自定义属性列表
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomWatchView,defStyleAttr,0);
        mColor = typedArray.getColor(R.styleable.CustomWatchView_bodyColor, Color.BLACK);//获取xml配置颜色,默认是黑色
        radius = typedArray.getFloat(R.styleable.CustomWatchView_radius, 150);//获取xml配置的半径,默认为150
        mPaint.setColor(mColor);//设置画笔颜色
        mPaint.setStrokeWidth(4);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }

}

 

        可能有人注意到我在前接构造器中用的都是this,跟有些人的博客写的不一样,看过上一篇博客就知道,这么写其实是一样的,没啥问题,最终还是要调用到View(Context context)的。如果不明白,可以看一下上一篇,地址在本文顶部。

        上面代码我们已经获取到了需要的变量,接下来就是绘图了:

        1.      画外圈的大圆;

        2.      加点自己的痕迹和文本时间(可选)。

        3.      画刻度;

        4.      画时针;

        5.      画分针;

        6.      画秒;

        接下来只贴onDraw(Canvascanvas)的代码,这里把1、2一起放上来:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //画表盘
        mPaint.setAntiAlias(true); //设置平滑
        mPaint.setStyle(Paint.Style.STROKE);
        canvas.translate(getWidth()/2,getHeight()/2);//其实绘制点移动到控件的中心
        canvas.drawCircle(0,0,radius,mPaint);//画圆做表盘最外围
        canvas.save();//保存一下图层

        canvas.translate(-70, -100);//左移70,上移100
        Paint citePaint = new Paint(mPaint);
        citePaint.setColor(Color.GRAY);
        citePaint.setTextSize(28);//设置文字大小
        citePaint.setStrokeWidth(2);
        canvas.drawText("shazeys", 20, 10, citePaint);
        canvas.translate(0,radius);
        citePaint.setColor(Color.RED);
        canvas.drawText(getTimeStr(),15, 10, citePaint);
        canvas.restore();//回复刚刚保存的位置
    }

        关于getTimeStr()的代码,一会儿再贴;这里出现了一对表面看起来和绘画关系不大的方法save()和restore(),关于它俩,后半部分再说,这里先露个脸,混脸熟。

        到这里的效果是这样的:

        继续往下画刻度,代码在上段代码的下面,onDraw的内部:

        //画刻度
        Paint tmpPaint = new Paint(mPaint);
        tmpPaint.setStrokeWidth(2);
        float y = radius;
        int count = 60;//刻度总数
        for (int i=0; i<count; i++){
            if (i%5==0){
                canvas.drawLine(0f,y,0,y-12f,mPaint);//每五个画一个大的刻度
            }else{
                canvas.drawLine(0f,y-7,0f,y,tmpPaint);//普通刻度
            }
            canvas.rotate(360/count,0f,0f);//旋转画布
        }

        效果:


        有前面的铺垫剩下的一锅上了:

        //画中心的小圆和稍大的灰色小圆盘
        tmpPaint.setColor(Color.GRAY);
        tmpPaint.setStrokeWidth(4);
        canvas.drawCircle(0,0,7,tmpPaint);//半径7的圆
        tmpPaint.setStyle(Paint.Style.FILL);
        tmpPaint.setColor(mColor);
        canvas.drawCircle(0,0,4,tmpPaint);//半径4的圆

        //画时针
        canvas.rotate((float) (mHour*30 + mMinutes*0.5));//根据时间计算时针的角度,旋转画布
        canvas.drawLine(0,10,0,-(radius-100),mPaint);//画时针
        canvas.rotate((float) -(mHour*30 + mMinutes*0.5));//将画布转回去
        //画分针
        Paint miPaint = new Paint(mPaint);//新建画笔
        miPaint.setColor(Color.DKGRAY);//设置颜色
        miPaint.setStyle(Paint.Style.FILL);
        miPaint.setStrokeWidth(3);
        canvas.rotate(mMinutes*6);//计算分针的角度并旋转画布
        canvas.drawLine(0,15,0,-(radius-50),miPaint);//画分针
        canvas.rotate(-mMinutes*6);//将画布转回去
        //画秒针的点
        miPaint.setColor(Color.RED);//改变画笔颜色
        canvas.rotate(mSecond*6);//秒针旋转角度
        canvas.drawCircle(0,-y,5,miPaint);
        //canvas.rotate(-mSecond*6);//转回去,最后一步了,可以不恢复

        基本的内容是画完了,那么作为手表,必须动起来吧?

        写个定时器,启动定时器,这里使用handler来实现,下面贴一下定时器的代码:

    //用Handler实现计时器
    final Handler mUpdateTimeHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){
                case MSG_UPDATE_TIME:
                    invalidate();
                    long time = System.currentTimeMillis();
                    mHour = (time/1000/60/60 + 8) % 12;//中国时区+8
                    mMinutes = time/1000/60 % 60;
                    mSecond = time/1000 % 60;
                    this.sendEmptyMessageDelayed(MSG_UPDATE_TIME,1000);
                    Log.i("TIME",mHour+":"+mMinutes+":"+mSecond);
                    break;
            }
        }
    };

        对了,上面还欠个格式化时间值的方法:

    //获取时间字符串
    private String getTimeStr(){
        return timeFormat(mHour)+":"+timeFormat(mMinutes)+":"+timeFormat(mSecond);
    }
    //格式化时间值
    private String timeFormat(float value){
        return value>9 ? (int)value+"" : "0"+(int)value;
    }
        公开启动和停止的两个方法:
    public void start(){
        mUpdateTimeHandler.sendEmptyMessageAtTime(MSG_UPDATE_TIME,0);
    }

    public void stop(){
        mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
    }

        现在这个自定义控件就整体搞完了,可以在activity中看效果了,建议吧开关写在生命周期里,节省资源。

        但是现在还不能结束这篇博客,还有个坑没填:save()和restore()。

        它俩是成对出现的,总是先save后restore,而且是一对一的,这个感觉就像写数据库开启事务和关闭事务一样成双成对。

        save会记下当前的canvas的状态,然后在restore的时候,保留这之间的操作,然后回到save时的状态。这个再比较复杂的绘画比较常用。我们前面画时分秒的时候,都是先做了对应的旋转,然后再转回去,我们也可以改改用save和restore来处理,下面贴出用save和restore完整代码的类:

public class CustomWatchView extends View{
    static final int MSG_UPDATE_TIME = 0;
    //画笔工具
    private Paint mPaint;
    //图像颜色
    private int mColor;
    //表盘半径
    private float radius;

    private float mHour = 0;
    private float mMinutes = 0;
    private float mSecond = 0;

    public CustomWatchView(Context context) {
        this(context,null);
    }
    public CustomWatchView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }
    public CustomWatchView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint = new Paint();//实例化画笔
        //获取自定义属性列表
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomWatchView,defStyleAttr,0);
        //获取xml配置颜色,默认是黑色
        mColor = typedArray.getColor(R.styleable.CustomWatchView_bodyColor, Color.BLACK);
        //获取xml配置的半径,默认为150
        radius = typedArray.getFloat(R.styleable.CustomWatchView_radius, 150);
        mPaint.setColor(mColor);
        mPaint.setStrokeWidth(4);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //画表盘
        mPaint.setAntiAlias(true); //设置平滑
        mPaint.setStyle(Paint.Style.STROKE);
        canvas.translate(getWidth()/2,getHeight()/2);//其实绘制点移动到控件的中心
        canvas.drawCircle(0,0,radius,mPaint);//画圆做表盘最外围
        canvas.save();//保存一下图层

        canvas.translate(-70, -100);//左移70,上移100
        Paint citePaint = new Paint(mPaint);
        citePaint.setColor(Color.GRAY);
        citePaint.setTextSize(28);
        citePaint.setStrokeWidth(2);
        canvas.drawText("shazeys", 20, 10, citePaint);
        canvas.translate(0,radius);
        citePaint.setColor(Color.RED);
        canvas.drawText(getTimeStr(),15, 10, citePaint);
        canvas.restore();//回复刚刚保存的位置

        //画刻度
        Paint tmpPaint = new Paint(mPaint);
        tmpPaint.setStrokeWidth(2);
        float y = radius;
        int count = 60;//刻度总数
        for (int i=0; i<count; i++){
            if (i%5==0){
                canvas.drawLine(0f,y,0,y-12f,mPaint);//每五个画一个大的刻度
            }else{
                canvas.drawLine(0f,y-7,0f,y,tmpPaint);//普通刻度
            }
            canvas.rotate(360/count,0f,0f);//旋转画布
        }

        //画中心的小圆和稍大的灰色小圆盘
        tmpPaint.setColor(Color.GRAY);
        tmpPaint.setStrokeWidth(4);
        canvas.drawCircle(0,0,7,tmpPaint);
        tmpPaint.setStyle(Paint.Style.FILL);
        tmpPaint.setColor(mColor);
        canvas.drawCircle(0,0,4,tmpPaint);

        //画时针
        canvas.save();
        canvas.rotate((float) (mHour*30 + mMinutes*0.5));//根据时间计算时针的角度,旋转画布
        canvas.drawLine(0,10,0,-(radius-100),mPaint);//画时针
//        canvas.rotate((float) -(mHour*30 + mMinutes*0.5));//将画布转回去
        canvas.restore();
        //画分针
        canvas.save();
        Paint miPaint = new Paint(mPaint);//新建画笔
        miPaint.setColor(Color.DKGRAY);//设置颜色
        miPaint.setStyle(Paint.Style.FILL);
        miPaint.setStrokeWidth(3);
        canvas.rotate(mMinutes*6);//计算分针的角度并旋转画布
        canvas.drawLine(0,15,0,-(radius-50),miPaint);//画分针
//        canvas.rotate(-mMinutes*6);//将画布转回去
        canvas.restore();
        //画秒针的点
        canvas.save();
        miPaint.setColor(Color.RED);//改变画笔颜色
        canvas.rotate(mSecond*6);//秒针旋转角度
        canvas.drawCircle(0,-y,5,miPaint);
        //canvas.rotate(-mSecond*6);//转回去,最后一步了,可以不恢复
        canvas.restore();
    }

    //用Handler实现计时器
    final Handler mUpdateTimeHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){
                case MSG_UPDATE_TIME:
                    invalidate();
                    long time = System.currentTimeMillis();
                    mHour = (time/1000/60/60 + 8) % 12;
                    mMinutes = time/1000/60 % 60;
                    mSecond = time/1000 % 60;
                    this.sendEmptyMessageDelayed(MSG_UPDATE_TIME,1000);
                    Log.i("TIME",mHour+":"+mMinutes+":"+mSecond);
                    break;
            }
        }
    };
    //获取时间字符串
    private String getTimeStr(){
        return timeFormat(mHour)+":"+timeFormat(mMinutes)+":"+timeFormat(mSecond);
    }
    //格式化时间值
    private String timeFormat(float value){
        return value>9 ? (int)value+"" : "0"+(int)value;
    }
    public void start(){
        mUpdateTimeHandler.sendEmptyMessageAtTime(MSG_UPDATE_TIME,0);
    }

    public void stop(){
        mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
    }
}







作者:shareye1992 发表于2016/8/17 16:59:24 原文链接
阅读:12 评论:0 查看评论

谈谈Material Design之CoordinatorLayout

$
0
0

本文主要介绍一下如何使用CoordinatorLayout

先看看官方是怎么介绍Material Design的

We challenged ourselves to create a visual language for our users that synthesizes the classic principles of good design with the innovation and possibility of technology and science. This is material design. This spec is a living document that will be updated as we continue to develop the tenets and specifics of material design.

通过上面的这段英文相信大家对Material Design或多或少都有一定的认识了吧!大概的意思也就是google努力的帮你设计出一套很好视图设计规范,你按这个规范来咯~~~

我觉得我们需要关注的控件也就下面4个吧~

  • SnackBar:一个类似于Toast的控件,下文会提及
  • FloatingActionButton:悬浮按钮
  • CoordinatorLayout:也就是本篇博文的猪脚咯~~
  • Tablayout:本篇不聊

讲了这么多废话,那么我们正式进入今天的主题吧!

CoordinatorLayout 实现了多种Material Design中提到的滚动效果。目前这个框架提供了几种不用写动画代码就能工作的方法,这些效果包括:

  1. 当snackbar显示的时候,浮动按钮上移,给snackbar留出位置:效果如下
    这里写图片描述

  2. 扩展或者缩小Toolbar或者头部,让主内容区域有更多的空间:效果如下
    这里写图片描述

  3. 控制哪个view应该扩展还是收缩,以及其显示大小比例,包括视差滚动效果动画。
    这里写图片描述

自行引入Design Support Library,不清楚请自行网补

首先,我们看第一种效果

布局代码

<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                                 xmlns:app="http://schemas.android.com/apk/res-auto"
                                                 android:id="@+id/main_content"
                                                 android:layout_width="match_parent"
                                                 android:layout_height="match_parent"
                                                 android:fitsSystemWindows="true">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        >
        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            app:layout_scrollFlags="scroll|enterAlways"
            android:layout_width="match_parent"
            android:background="?attr/colorPrimary"
            android:layout_height="wrap_content"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
            />
    </android.support.design.widget.AppBarLayout>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerview"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|right"
        android:layout_margin="16dp"
        android:src="@mipmap/ic_launcher"
        app:layout_anchor="@id/recyclerview"
        app:layout_anchorGravity="bottom|right|end"/>

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

嘻嘻,今天猪脚终于亮相了,首先我们使用了CoordinatorLayout作为根布局,CoordinatorLayout可以用来配合浮动操作按钮的 layout_anchor 和 layout_gravity属性创造出浮动效果,只要使用CoordinatorLayout作为基本布局,将自动产生向上移动的动画。浮动操作按钮有一个 默认的 behavior来检测Snackbar的添加并让其在Snackbar之上呈现上移与Snackbar等高的动画。

咳咳,这个我相信大家都能懂,那么我们来说说FloatingActionButton这个控件。

当我们的项目需要一个圆形的Button, 你可能会想到用自定义Shape的方式去做,但那样文本的显示不好居中,这时估计就想到用自定义控件去解决了。以上都是废话,google给我们提供了FloatingActionButton可以轻松的创建圆形Button,而且更牛x的是FloatingActionButton具有更绚丽的效果。
FloatingActionButton继承自ImageView,所以你懂的,ImageView的属性我们阔以直接拿来用咯,so。。如果我们需要整个圆形的ImageView也当然阔以直接用这个咯~~

FloatingActionButton的属性主要有
  1. app:backgroundTint是指定默认的背景颜色
  2. app:rippleColor是指定点击时的背景颜色
  3. app:borderWidth border的宽度
  4. app:fabSize是指FloatingActionButton的大小,可选normal|mini
  5. app:elevation 可以看出该空间有一个海拔的高度
  6. app:pressedTranslationZ 哈,按下去时的z轴的偏移
<android.support.design.widget.FloatingActionButton
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:src="@mipmap/ic_launcher"
        app:backgroundTint="#FF00FF00"
        app:rippleColor="#FF0000FF"
        app:borderWidth="0dp"
        app:fabSize="normal"
        app:elevation="10dp"
        app:pressedTranslationZ="20dp"/>
传说中的一图胜千言

这里写图片描述

卡卡的 凑合看吧!!

第二种效果 Toolbar的扩展与收缩

先看效果吧

这里写图片描述

布局如下
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                                 xmlns:app="http://schemas.android.com/apk/res-auto"
                                                 android:id="@+id/main_content"
                                                 android:layout_width="match_parent"
                                                 android:layout_height="match_parent"
                                                 android:fitsSystemWindows="true">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        >
        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            app:layout_scrollFlags="scroll|enterAlways"
            android:layout_width="match_parent"
            android:background="?attr/colorPrimary"
            android:layout_height="wrap_content"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
            />
        <android.support.design.widget.TabLayout
            android:id="@+id/tabs"
            app:tabMode="scrollable"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">

        </android.support.design.widget.TabLayout>
    </android.support.design.widget.AppBarLayout>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerview"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:src="@mipmap/ic_launcher"
        app:backgroundTint="#FF00FF00"
        app:rippleColor="#FF0000FF"
        app:borderWidth="0dp"
        app:fabSize="normal"
        app:elevation="10dp"
        app:pressedTranslationZ="20dp"
        app:layout_anchor="@id/recyclerview"
        app:layout_anchorGravity="bottom|right|end"/>

</android.support.design.widget.CoordinatorLayout>
首先你必须保证用toolbar替代actionbar,至于toolbar的具体使用,也不是本篇的重点,不清楚的童鞋阔以自行网补(话说这博客到底有木有干货~~啥都要网补)
接下来,我们必须使用一个容器布局:AppBarLayout来让Toolbar响应滚动事件。请注意:AppBarLayout必须是CoordinatorLayout的直接子View。
然后,我们需要定义AppBarLayout与滚动视图之间的联系。在RecyclerView或者任意支持嵌套滚动的view(比如NestedScrollView)上添加app:layout_behavior。support library包含了一个特殊的字符串资源@string/appbar_scrolling_view_behavior(此处是反射),它和AppBarLayout.ScrollingViewBehavior相匹配,用来通知AppBarLayout 这个特殊的view何时发生了滚动事件。
这个behavior需要设置在触发事件(滚动)的view之上。(注意上面提到的嵌套滚动~~~~比如我们常用的scrollView、listview都是属于不支持嵌套滚动的)
<android.support.v7.widget.RecyclerView
        android:id="@+id/rvToDoList"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

当CoordinatorLayout发现RecyclerView中定义了这个属性,它会搜索自己所包含的其他view,看看是否有view与这个behavior相关联。AppBarLayout.ScrollingViewBehavior描述了RecyclerView与AppBarLayout之间的依赖关系。RecyclerView的任意滚动事件都将触发AppBarLayout或者AppBarLayout里面view的改变。

细心的你不知道是否已经发现~额 toolbar有

app:layout_scrollFlags="scroll|enterAlways"

这样一条属性。app:layout_scrollFlags,什么玩意儿,听我慢慢道来。前面我们提到RecyclerView的任意滚动事件都将触发AppBarLayout或者AppBarLayout里面view的改变。那么,AppBarLayout里面的子View到底以怎么样的方式进行滚动呢?此时这个属性就起作用了!!!
那么我们看看这个属性的属性值都用哪些

  1. scroll 谁要滚出屏幕,谁就设置这个值
  2. enterAlways 其他向上滑动时,可以立即显示出来
  3. exitUntilCollapsed 将关闭滚动直到它被折叠起来(有 minHeight) 并且一直保持这样
  4. enterAlwaysCollapsed 定义了 View 是如何回到屏幕的,当你的 view 已经声明了一个最小高度(minHeight) 并且你使用了这个标志,你的 View 只有在回到这个最小的高度的时候才会展开,只有当 view 已经到达顶部之后它才会重新展开全部高度。

第三种,折叠效果

先上图!
这里写图片描述
布局如下

<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/main_content"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        android:fitsSystemWindows="true">

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

            <ImageView
                android:id="@+id/backdrop"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="centerCrop"
                android:fitsSystemWindows="true"
                android:src="@drawable/head"
                app:layout_collapseMode="parallax" />

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

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

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

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerview"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@android:drawable/ic_dialog_email"
        app:layout_anchor="@id/appbar"
        app:layout_anchorGravity="bottom|center"/>


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

可以看到,我们将image和toolbar使用CollapsingToolbarLayout包裹起来了
通常,我们我们都是设置Toolbar的title,而现在,我们需要把title设置在CollapsingToolBarLayout上,而不是Toolbar。

CollapsingToolbarLayout collapsingToolbar =
              (CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar);
      collapsingToolbar.setTitle("Title");

该控件的的子view必须要有Toolbar,他的作用就是提供一个可折叠的标题栏。通常情况下,该控件会和上面我们说的一些控件搭配使用,达到一种固定的效果。

我们先分析一下上面这段布局,最直观的,可以看到有一个行为上的协作, 那肯定需要CoordinatorLayout这个控件,而且还会有一个滑动的效果,那肯定是AppBarLayout啦,当然,细心的朋友可能还会看出来,那个图片和Toolbar会有一种视差的效果,而且整个banner部分是一种折叠的效果,这就需要CollapsingToolbarLayout这个控件了。
AppBarLayout通过CollapsingToolBarLayout把ImageView和Toolbar作为整个app的标题栏,而且表示滚动标识的app:layout_scrollFlags=”scroll|exitUntilCollapsed”也是赋值给了CollapsingToolbarLayout,mageView有一条属性app:layout_collapseMode=”parallax”表示这个ImageView以视差的形式折叠(效果上看着貌似就是有点小偏移)。 Toolbar的app:layout_collapseMode=”pin”表示Toolbar在折叠的过程中会停靠顶部(pin的意思是钉住)。 这样CollapsingToolBarLayout就没有全部滚出界面。
如果你希望滚动的时候渐变的颜色跟随的是图片而不是蓝色渐变,在java代码中使用如下代码

final CollapsingToolbarLayout ctl = (CollapsingToolbarLayout) findViewById(R.id.ctl);
        ctl.setTitle("Temperate_box");
...
Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.drawable.banner);
        Palette.generateAsync(bmp, new Palette.PaletteAsyncListener() {
            @Override
            public void onGenerated(Palette palette) {
                Palette.Swatch swatch = palette.getDarkMutedSwatch();
                ctl.setContentScrimColor(swatch.getRgb());
            }
        });

ok,打完收工~~~~

作者:qq_20383863 发表于2016/8/17 17:06:16 原文链接
阅读:18 评论:0 查看评论

Android动画总结系列(4)——属性动画集成

$
0
0
一、概述
1.1 简述
Android框架提供两大动画方案:属性动画与补间动画。这两者都非常有用,而且从谷歌文档来看,都会持续支持。但官方文档建议我们应优先考虑使用属性动画,因为属性动画更加灵活而且提供更多的可用特性。这两大动画方案之外Android还支持Drawable Animation(帧动画),通过逐帧播放来形成动画视觉效果。

属性动画引入自Android 3.0(API 11),其原理是给定对象的一个或多个属性,指定其开始与结束时的值,通过时间插值生成不同的属性值,并调用更新方法将属性值设置到对象内,引起对象重绘,从而形成动画效果。比如,通过不断的设置View的宽高来更新View的展示,形成缩放效果等。

1.2 属性动画相关概念
属性动画作用的对象,可以是没有渲染到界面的对象,当然这样,动画效果就看不到了,我们能得到的,是一段时间内不断改变属性的对象。同时,属性动画作用的对象属性,也可以是与界面展示无关的属性。而且,属性动画还支持自定义的属性,从而提高扩展性。简而言之:属性动画可以作用任何对象的任何属性上。

一个属性动画执行需要的基本要素包括:承载动画执行的对象属性、动画持续时间和属性的变化区间。
属性动画支持定义动画的以下特性:
  1. Duration:指定动画的执行时间,默认300ms。
  2. Time interpolation:官方解释比较拗口,大概意思就是说这是一个输入为动画真实时间的函数,以此函数来计算动画属性;其实重点在于函数上,这个函数定义的是动画的变化速率,举个例子来看:假如有个函数y=x,x是动画的真实执行时间,y是映射后的执行时间,则这个动画是恒速执行的;假如映射函数是y=x*x,这个动画是就是不断加速运行的,因为y的变化速率在不断的增大。
  3. Repeat count and behavior:动画重复次数表示动画需要被执行的次数,动画重复时的行为有两种:从头开始或者反转执行;在动画执行结束时,如果没有执行完指定次数,则继续执行;假设执行次数为3,模式设为Restart从头执行,则执行顺序为“初始帧--插值-->结束帧-->初始帧--插值-->结束帧-->初始帧--插值-->结束帧”,模式设为Reverse,则执行顺序为“初始帧--插值-->结束帧--插值-->初始帧--插值-->结束帧”。
  4. Animator sets:将多个动画效果组成动画集合并设置彼此间的执行顺序,可设三种执行顺序:一起执行、顺序执行和在指定时间执行。
  5. Frame refresh delay:指定多长时间刷新一次动画的帧,默认值是10ms一次,但最终取决于当前系统的繁忙程度和系统多快能相应定时器,基本上我们可以不用考虑。

1.3 属性动画的过程描述
Android Developers上有完整的动画工作示例,这里简单的描述一下,并盗个图来做个生动的解释(此图来源于android官网):
假设有一个长方体(对象),基于其属性x(x是长方体在水平方向上的位置,单位px)做动画,动画持续时间是40ms,长方体在40ms内从位置x=0px移动到x=40px,帧刷新延迟是10ms,也就是每10ms移动一下长方体的位置:
如果长方体的时间插值是均匀的,也就是匀速运动,那么动画的效果是长方体在位置0停留10ms,位置10px停留10ms,位置20px停留10ms,位置30px停留10ms,最终到达位置40px,停下,动画结束,效果为:

如果我们想要先加速再减速的效果,那么我们调整时间插值器,可以形成加减速效果,长方体在位置0停留10ms,第10ms时计算出位移6px,在位置6px停留10ms,在第20ms时计算出位移20px,在位置20px停留10ms,在第30ms时计算出位移34px,停留在34px位置10ms,最终在第40ms时移动到位置40px,动画结束。这样视觉效果里,我们觉得长方体的移动速率在变化,先加速到中间位置,再减速到最终位置。

好了,这样说下来,应该对属性动画有点认识了吧,我们继续往下,看看属性动画和补间动画的区别。

1.4 属性动画与补间动画的区别
1) 补间动画的应用对象只能是View,不能用于非View对象,属性动画支持任意对象;
2) 补间动画的本质是对View做Transformation,并不能影响View的布局位置,只能影响View的可视位置。如果View从屏幕左边移动到右边,则其展示在右边了,但是这时候View相应点击事件还是在左边的位置,因为View并没有真的移动到右边,只是被绘制到右边了;
3)及时动画对象是View,补间动画能做的也比较有限,只能做平移/旋转/缩放/透明度四种变化效果,假如我们要不断改变View背景色,就搞不定了,属性动画表示毫无压力;
4)属性动画比补间动画更灵活,可以同时支持多个属性的动画,每个属性都可以独立定义插值器,各动画之间还可以做动画同步控制。

二、属性动画使用

2.1 结构概览
属性动画的每次插值执行过程可以分为两步:1)计算插值的属性值,2)将属性值设置到对象的属性上,也就是更新属性值。

Animator:所有属性动画的基类;注意和补间动画不同,补间动画所有类都继承自Animation;
ValueAnimator:属性动画最核心的插值逻辑管理类,包含最核心的属性值插值计算以及属性动画相关的控制策略(动画重复、属性值变化通知、处理自定义的属性值插值等)。前文说到的属性动画执行过程有两步,ValueAnimator只专注于完成第一步插值计算的操作,不负责第二步具体的属性刷新,它借助AnimatorUpdateListener.onAnimationUpdate()设置属性值,或进一步刷新界面;
ObjectAnimator:继承自ValueAnimator,专注于第二步操作,支持调用者指定对象和属性,其通过反射来调用属性的setter方法。通常情况下,我们都是用此类来做属性动画,因为其封装了属性设置操作,不用我们自己做更新;但有时候我们不得不使用ValueAnimator,比如说我要给某个View设置宽度,而View并没有setWidth接口,这时候,我们只能在onAnimationUpdate中更新View的LayoutParams。
AnimatorSet:将多个Animator组合在一起执行,支持一起执行、顺序执行和指定时间执行;
TimeAnimator:作用不大,主要作用是把动画已经执行的耗时和当前帧距离与上一帧的耗时回调到外面去。

2.2 属性动画的属性值计算
属性动画系统内使用Evaluators来告知系统如何计算给定属性的值。其输入是动画当前的执行时间(归一化的值,且已经被插值器处理过)、动画的开始值和结束值,输出是给定的属性开始值和结束值之间的插值。属性动画支持的Evalators包括:
IntEvaluator:计算int型的属性值插值;
FloatEvaluator:计算float型的属性值插值;
ArgbEvaluator:计算颜色的属性值插值;
TypeEvaluator:这是一个接口,我们可以实现此接口来定义自己的Evaluator。如果我们需要操作的属性不是int/float/color,就必须实现TypeEvaluator,告知如何计算属性插值。如果我们需要为int/float/color提供不同于默认实现的插值计算方案,也可以通过重写此接口实现。下面就是接口定义:
public interface TypeEvaluator< T> {
    public T evaluate( float fraction, T startValue, T endValue);
}

2.3 属性动画的插值器
属性动画的插值器与补间动画的插值器完全相同,这里不再细述,这篇文章总结得比较好:
如果觉得现有的插值器不满足你的需求,就实现TimeInterpolator接口自己写个插值器。基本上目前定义的插值器已经足够了。

2.4 使用ValueAnimator实现属性插值效果
前面我们说过,ValueAnimator不负责属性动画第二步,也就是对对象的属性进行操作,所以下面两个动作,都是集中在第一步,计算按时间变化的插值序列。

2.4.1 ValueAnimator支持int/float/color的插值动作,相应的接口是ofInt/ofFloat/ofArgb。一个简单的例子如下:
ValueAnimator animation = ValueAnimator.ofFloat(0f, 1f);
animation.setDuration(1000 );
animation.start();
这段代码表示ValueAnimator在start()执行后的1s内,不断的生成介于0~1之间的插值。这个插值可以作为某个对象的某个属性设置到对象里去。

2.4.2 ValueAnimator支持Object的插值动作,相应接口是ofObject。例子如下:
假设我们有个属性,类型为:
private static class TestData {
    float size = 0 ;

    TestData(float size) {
        this.size = size;
    }
}

我们需要为此对象定义一个TypeEvaluator,如下:
private static class MyTypeEvaluator implements TypeEvaluator<TestData> {

    @Override
    public TestData evaluate(float fraction, TestData startValue, TestData endValue) {
        return new TestData(startValue.size + fraction * (endValue.size - startValue.size));
    }
}
然后我们就可以将其放入ValueAnimator使用了:
ValueAnimator valueAnimator = ValueAnimator.ofObject(new MyTypeEvaluator(),
        new TestData(0), new TestData( 1));
valueAnimator.setDuration(1000 );
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        TestData testData = (TestData) animation.getAnimatedValue();
        Log.d( "TEST_PROPERTY_ANIM", "data is " + testData.size);
    }
});
valueAnimator.start();
ValueAnimator通过MyTypeEvaluator类(自定义类型估值器)完成TestData(自定义属性值)的插值工作,并通知到AnimatorUpdateListener接口,这个接口里可以取到计算出的插值TestData,我们可以根据此值来刷新界面(当然,此Demo内我们只是把值打印出来了,没有刷新界面)。

2.5 使用ObjectAnimator实现属性动画效果
ObjectAnimator继承自ValueAnimator,ValueAnimator已经封装了计算随时间变化的插值属性值序列的任务,剩下来任务就是将计算好的插值属性值设置到对象的指定属性上。ObjectAnimator的目标就是完成剩下来的任务了。
使用ObjectAnimator与使用ValueAnimator非常相似,差别在于,使用ObjectAnimator时需要指定对象和对象属性的名字(也就是字符串):
比如说我们有一个View对象,对其float型的属性alpha做透明度0到1的渐显动画:
View view = new View(context);
ObjectAnimator.ofFloat(view, "alpha", 0f, 1f)
        .setDuration(1000 )
        .start();

ObjectAnimator需要依赖以下条件才能正常工作:
第一点,必须要确保指定的属性在对象内存在setter方法(且是驼峰法命名),上例中View类需要存在setAlpha方法设置透明度值,很幸运,View本来就有此方法。如果我们很不幸的发现并不存在这样的setter咋办呢?
下面有几种办法:
1)如果我们有权限,就直接去改了对象类,加个setter方法,上例中,如果View没有setAlpha,我们去View内加个setAlpha。好吧,我们是做不到的,但是google可以,setAlpha方法就是和属性动画在API 11一起添加的。
2)如果我们没办法改View,那就试着写个类把它包裹起来。类似下面这样的:
public class WrappedView extends View {
    public WrappedView(Context context) {
        super(context);
    }
   
    public void setAlpha(float alpha) {
        AlphaAnimation animation = new AlphaAnimation(alpha, alpha);
        animation.setDuration(0 );
        animation.setFillAfter(true);
        startAnimation(animation);
    }
}
API 10以下我们取不到View的TransformationInfo,只能通过动画来间接实现。
3)如果连包装都难以做到,就只能用ValueAnimator了。在ValueAnimator回调的onAnimationUpdate做处理。

第二点,如果我们交给接口的values...参数只有一个值,也这样写属性动画:ObjectAnimator. ofFloat(view, "alpha", 1f),这唯一的一个值1f被认为是动画的结束值,那动画的初始值是多少呢?这时候就得靠getter方法返回了。所以View得定义getAlpha方法,没有getter方法,动画就不执行。

第三点,getter方法和setter方法必须保持和ObjectAnimator指定的属性值类型相同。假如有这样的调用:ObjectAnimator .ofFloat(targetObject, "propName", 1f ),那么targetObject对象必须存在两个方法:
void setPropName(float value);
float getPropName();
重点就是setter的参数是float,同时getter返回值也必须是float。

第四点,某些情况下我们需要强制调用invalidate/postInvalidate来强制刷新,保证动画效果显示在界面上。比如说我们对ImageView.getDrawable的drawable做属性动画,这些修改只能在View绘制时才会应用,所以我们要不断的调用invalidate来重新绘制View;但如果我们调用是View.setAlpha(基本上是所有View的setter方法),此方法会自己刷新界面,就不需要我们调用invalidate了。如果需要调用invalidate,那就应该放在onAnimationUpdate的回调中进行。

2.6 使用动画集合
在实际应用场景中,可能需要指定动画与其他动画一起执行、在其他动画执行完成后执行或者在指定时间执行,属性动画提供了AnimatorSet来管理多个动画以及他们之间的执行顺序关系。可以指定多个动画一起执行、一个接一个执行或者在指定时间执行,因为AnimatorSet内持有的是抽象Animator类,所以这里也可以在动画集合内再套动画集合,形成复杂的动画效果。
下面这个例子是google官网的,我就没有加工了,假设有以下执行顺序:
1.执行bounceAnim。
2.随后同时执行squashAnim1/squashAnim2/stretchAnim1/stretchAnim2。
3.stretchAnim2执行完成后开始执行bounceBackAnim。
4.最后执行fadeAnim。
bouncer.play(bounceAnim).before(squashAnim1);
bouncer.play(squashAnim1).with(squashAnim2);
bouncer.play(squashAnim1).with(stretchAnim1);
bouncer.play(squashAnim1).with(stretchAnim2);
bouncer.play(bounceBackAnim).after(stretchAnim2);
ValueAnimator fadeAnim = ObjectAnimator.ofFloat(newBall, "alpha" , 1f , 0f);
fadeAnim.setDuration(250);
AnimatorSet animatorSet = new AnimatorSet();
//这里是为了展示AnimationSet嵌套
animatorSet.play(bouncer).before(fadeAnim);
animatorSet.start();

2.7 动画事件监听
2.7.1 监听正常的动画事件如开始/重复/结束/取消应使用Animator.AnimatorListener:
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, "alpha", 0f, 1f);
objectAnimator.addListener(new Animator.AnimatorListener() {
    @Override
    public void onAnimationStart(Animator animation) {
         //动画开始
    }

    @Override
    public void onAnimationEnd(Animator animation) {
         //动画结束
    }

    @Override
    public void onAnimationCancel(Animator animation) {
         //动画取消,不论动画如何结束,取消还是正常结束,都会回调onAnimationEnd
    }

    @Override
    public void onAnimationRepeat(Animator animation) {
         //动画重复执行
    }
});
如果不想监听所有的事件,可以使用AnimatorListenerAdapter,这个类实现了AnimatorListener,并提供空实现。


2.7.2 监听属性动画每一次插值刷新界面的动作,使用ValueAnimator.AnimatorUpdateListener
objectAnimator.addUpdateListener( new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        //属性动画开始了新的插值操作 ,动画每刷新一帧则调用一次
       }
});
在此回调内,可以通过animation.getAnimatedValue()来取得插值的值。如果是使用ValueAnimator,一定需要实现此对象来实现界面刷新,形成动画效果。

2.8 自定义TypeEvaluator
前面已经写过一个简单的TypeEvaluator了,TypeEvaluator的作用是对未知属性类型的值的插值进行抽象,只有一个接口evaluate需要实现。其输入是当前的动画执行时间(经过归一化和速率变化处理的时间)fraction、属性开始值startValue和属性结束值endValue,绝大部分情况下,这里面的逻辑都是startValue+(endValue - startValue) * fraction;
需要注意一点,fraction已经由外面的插值器(TimeInterpolator)做过加速度变化处理,所以在TypeEvaluator内不需要再考虑速率变化。
举个系统实现的Float插值的插值器做例子:
public class FloatEvaluator implements TypeEvaluator<Number> {
    public Float evaluate(float fraction, Number startValue, Number endValue) {
        float startFloat = startValue.floatValue();
        return startFloat + fraction * (endValue.floatValue() - startFloat);
    }
}

2.9 使用关键帧来做属性动画
一个关键帧包含一个“时间/属性值”对,可以此来指定一个动画在特定时间(关键帧中的时间)处于的特殊状态(关键帧 中的属性值),每个关键帧可以包含自己的插值器,此插值器的影响范围是上一个关键帧的时间到当前关键帧的时间内动画的行为。
我们可以用KeyFrame的ofInt()、ofFloat()或者ofObject()来实例化一个关键帧对象(注意,没有ofRgba)。然后通过PropertyValuesHolder.ofKeyframe来根据多个(至少两个,一个就没动画效果了)关键帧生成一个PropertyValuesHolder,有了这个对象后,我们就可以通过ObjectAnimator.ofPropertyValuesHolder(target, pvhRotation)生成一个属性动画了。大概代码如下:
Keyframe kf0 = Keyframe. ofFloat(0f , 0f );
Keyframe kf1 = Keyframe.ofFloat(.5f, 360f);
Keyframe kf2 = Keyframe.ofFloat(1f, 0f);
PropertyValuesHolder pvhRotation = PropertyValuesHolder.ofKeyframe("rotation", kf0, kf1, kf2);
ObjectAnimator rotationAnim = ObjectAnimator.ofPropertyValuesHolder(view, pvhRotation);
rotationAnim.setDuration(5000 );

2.10 PropertyValuesHolder使用
前面已经有过一个PropertyValuesHolder的使用举例了,其实PropertyValuesHolder的作用就是持有一个属性和它的开始值结束值,一个ObjectAnimator可以同时对多个属性进行操作,如果一个个写代码就太难看了,这时候我们可以通过PropertyValuesHolder来先枚举所有的属性和它们的开始结束值,然后将所有的PropertyValuesHolder一次性传入ObjectAnimator构造中,生成一个属性动画。
PropertyValuesHolder支持的属性类型包括:float/int/keyframe/Object,对应的接口是ofFloat/ofInt/ofKeyframe/ofObject等。
举个例子看看其使用:
PropertyValuesHolder valuesHolderX = PropertyValuesHolder.ofFloat("translationX", view.getWidth() * 0.5f, view.getWidth() * 1.5f );
PropertyValuesHolder valuesHolderY = PropertyValuesHolder.ofFloat("translationY", view.getHeight() * 0.5f, view.getHeight() * 1.5f);
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view, valuesHolderX, valuesHolderY);

2.11 ViewPropertyAnimator使用
ViewPropertyAnimator提供一种简单的并行操作View的几种属性形成属性动画的方案。它在操作View属性时比ObjectAnimator效率更高一点,代码可读性更高些。ViewPropertyAnimator虽然以Animator命名,但其不继承Animator,而是调用ValueAnimator完成相关操作。其支持的操作有:translationX、translationY、translationZ、scaleX、scaleY、rotation、rotationX、rotationY、x、y、z、alpha。
下面是官网的一个例子对比,看一下就明白差别了:
同时操作属性,改变View的位置:
1)多个ObjectAnimator
ObjectAnimator animX = ObjectAnimator.ofFloat(myView, "x", 50f );
ObjectAnimator animY = ObjectAnimator.ofFloat(myView, "y", 100f );
AnimatorSet animSetXY = new AnimatorSet();
animSetXY.playTogether(animX, animY);
animSetXY.start();

2)一个ObjectAnimator
PropertyValuesHolder pvhX = PropertyValuesHolder.ofFloat("x", 50f);
PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat("y", 100f);
ObjectAnimator.ofPropertyValuesHolder( myView, pvhX, pvhY).start();

3)使用ViewPropertyAnimator
myView.animate().x(50f ).y(100f );

三、属性动画实现补间动画效果
之前我们已经讨论过属性动画与补间动画的区别,以及属性动画相对于补间动画的优势。我们再强调一遍,补间动画改变的是View的绘制方式与位置,而不改变View的真实位置(影响draw过程而非layout过程),原因是相关的操作(平移旋转缩放)都是由View的父元素完成的,View自身并无操作方法来完成这些动作。结果就是可能View已经发生了动画变化,但自身并无改变,比如说,View在屏幕左边绘制,经过动画到屏幕右边绘制,这是View布局位置并未变化,相应点击的区域还在左边区域。Android3.0(API11)增加了相应的新属性和setter/getter方法来解决这个问题。
属性动画可以通过改变View的属性来真正的改变View的位置,同时当View的属性发生变化时,View会自动调用invalidate来刷新界面。View在3.0增加了以下属性来支持属性动画:
translationX/translationY:这两个变量辅助控制View的位置,它们是真实位置与父元素设置布局位置的差值。假设View的左坐标是left,上坐标是top,这两个值是View的容器设置的,在运行过程中不会发生改变,真实的View位置在left + translationX,top+translationY的位置。
rotation/rotationX/rotationY:这些属性控制View绕中心点的2D和3D旋转效果,其中2D效果由rotation表示。
scaleX/scaleY:控制View绕中心点的2D缩放效果。
pivotX/pivotY:控制View的中心点位置,缩放和旋转动画基于此位置来执行动画,默认在View中心位置。
x/y:描述View最终在其容器内的位置,如上所述,x = left + translationX, y = top + translationY。
alpha:表示View的透明度,1不透明,0全透明(不可见)。
要想用属性动画实现补间动画的效果,其实只需要创建属性动画,并指定上面这些属性即可:

以下这些效果,如果希望启动Activity就执行,应写在Activity的onWindowFocusChanged内。同时,以下效果都是在3.0以上才能运行,因为这些属性和属性动画本身都是3.0以上才有的。
3.1 平移效果
举例:x轴从自身位置50%向右平移相对于自己宽度100%的距离,y轴从自身位置50%向下平移相对于自己100%高度的距离,时间1s,先加速再减速:
PropertyValuesHolder valuesHolderX = PropertyValuesHolder.ofFloat("translationX", view.getWidth() * 0.5f, view.getWidth() * 1.5f );
PropertyValuesHolder valuesHolderY = PropertyValuesHolder.ofFloat("translationY", view.getHeight() * 0.5f, view.getHeight() * 1.5f);
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view, valuesHolderX, valuesHolderY);
animator.setDuration(1000);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.start();

3.2 缩放效果
举例:x轴从自身50%缩放到自身200%,y轴从自身50%缩放到自身200%,中心点(10%,10%)时间1s,先加速再减速:
PropertyValuesHolder valuesHolderX = PropertyValuesHolder.ofFloat("scaleX", 0.5f, 1f);
PropertyValuesHolder valuesHolderY = PropertyValuesHolder.ofFloat("scaleY", 0.5f, 2f );
//两个中心点其实可以用set方法直接设置,mView.setPivotX(pivotX)
float pivotX = mView.getWidth() * 0.1f;
float pivotY = mView .getHeight() * 0.1f;
PropertyValuesHolder valuesHolderPvX = PropertyValuesHolder.ofFloat("pivotX", pivotX, pivotX);
PropertyValuesHolder valuesHolderPvY = PropertyValuesHolder.ofFloat("pivotY", pivotY, pivotY);
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder( mView, valuesHolderX, valuesHolderY, valuesHolderPvX, valuesHolderPvY);
animator.setDuration(1000);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.start();

3.3 旋转效果
举例:View绕自身中心点(10%,10%)位置,从-270旋转到180,时间1s,先加速再减速 :
PropertyValuesHolder valuesHolderX = PropertyValuesHolder.ofFloat("rotation", - 270f, 180f);
//两个中心点其实可以用set方法直接设置,mView.setPivotX(pivotX)
float pivotX = mView .getWidth() * 0.1f;
float pivotY = mView .getHeight() * 0.1f;
PropertyValuesHolder valuesHolderPvX = PropertyValuesHolder.ofFloat("pivotX", pivotX, pivotX);
PropertyValuesHolder valuesHolderPvY = PropertyValuesHolder.ofFloat("pivotY", pivotY, pivotY);
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder( mView, valuesHolderX, valuesHolderPvX, valuesHolderPvY);
animator.setDuration(1000);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.start();

3.4 Alpha效果
举例:View从透明到完全不透明,时间1s,先加速再减速:
ObjectAnimator animator = ObjectAnimator.ofFloat(mView, "alpha", 0f , 1f );
animator.setDuration(1000);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.start();

四、使用XML定义属性动画
在XML中定义属性动画更容易在多个Activity中复用,且更容易编辑动画顺序。考虑到属性动画使用了新的属性动画API,为了与补间动画的动画文件区分开,在Android 3.1后,属性动画的XML动画文件定义在res/animator/文件夹中。
属性动画各接口与XML tag对应关系为:
ValueAnimator对应<animator>;
ObjectAnimator对应<objectAnimator>;
AnimatorSet对应<set>;
下面是官网上提供的一个例子:
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android :ordering="sequentially">
    <set >
        <objectAnimator
            android:duration="500"
            android:propertyName="x"
            android:valueTo="400"
            android:valueType="intType" />
        <objectAnimator
            android:duration="500"
            android:propertyName="y"
            android:valueTo="300"
            android:valueType="intType" />
    </set >
    <objectAnimator
        android:duration= "500"
        android:propertyName= "alpha"
        android:valueTo= "1f" />
</set>
这里有两个动画集合,外层的动画集合嵌套内层的动画集合,外层动画集合有两个子元素,它们按照android:ording的要求顺序播放,子集合(xy变化)执行完成后再执行下面的alpha动画。子集合内,x的属性动画和y的属性动画一起执行。
XML的动画文件定义好后,我们需要将其加载成Java对象使用,并设置各动画的target,具体方法如下:
AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(context, R.anim.property_animator);
set.setTarget(myObject);
set.start();

五、属性动画实现ViewGroup内Layout变化动画
属性动画对处理ViewGroup内子元素变化导致的动画行为提供了非常好的支持。使用LayoutTransition类来处理ViewGroup元素的变化动画。通过给ViewGroup设置LayoutTransition,View从ViewGroup内添加/移除/变的可见(setVisibility(VISIBLE))/变的不可见(setVisibility(GONE))等情况都可以执行一个显示或隐藏的动画。当添加/删除某个View时,ViewGroup内其他的View也可以展示挪到新位置的动画。
具体设置方法如下:
LayoutTransition transition = new LayoutTransition();
transition.setAnimator(LayoutTransition.APPEARING, objectAnimatorApearing);
transition.setAnimator(LayoutTransition.CHANGE_APPEARING, objectAnimatorChangeAppearing);
transition.setAnimator(LayoutTransition.CHANGE_DISAPPEARING, objectAnimatorChangeDisappearing);
transition.setAnimator(LayoutTransition.CHANGING, objectAnimatorChanging);
transition.setAnimator(LayoutTransition.DISAPPEARING, objectAnimatorDisappearing);

ViewGroup group = new LinearLayout(context);
group.setLayoutTransition(transition);

对应的常量意义为:
APPEARING:当View在容器内展示出来时显示的动画;
CHANGE_APPEARING:当View在容器内展示出来时,其他被影响了的View的动画;
DISAPPEARING:当View从容器内消失时展示的动画;
CHANGE_DISAPPEARING:当View从容器内消失时,其他被影响了的View的动画;
CHANGING:当不是View添加/移除导致的容器重新布局(Layout Change)时,所有被影响了的View的动画;这个属性并不是自动默认开启的,需要通过transition.enableTransitionType(LayoutTransition. CHANGING)来开启;
如果不通过setAnimator设置而通过enableTransitionType开启动画效果的话,LayoutTransition对这些情况的动画都支持使用默认值。
在XML内将android:animateLayoutchanges置为true就可以开启ViewGroup的layout transitions。如下:
<LinearLayout
    android :orientation="vertical"
    android :layout_width="wrap_content"
    android :layout_height="match_parent"
    android :id="@+id/verticalContainer"
    android :animateLayoutChanges="true" />

六、其他注意要点
1.Android属性动画只能支持Android3.0以上版本,想要支持3.0以前的版本,需要使用NineOldAndroids包。

七、总结
本文总结了属性动画的使用方法,Android属性动画相对于补间动画而言,的确是发生了质的变化,整个框架的抽象性设计非常合理,扩展性也非常强。在实际使用过程中,如果动画很简单,而且没有文中提到的补间动画的坑(View显示位置与布局位置不同),可以考虑使用补间动画,如果动画比较复杂,建议使用属性动画。

本文主要参考了Android官网和张鸿洋大神的文章,在此附上文章链接,感谢大神!前人栽树,后人乘凉,希望这篇文章能稍微帮到大家一点,就欧啦。
作者:u013478336 发表于2016/8/17 17:18:40 原文链接
阅读:30 评论:0 查看评论

Android开发工具----Android Studio调试技巧

$
0
0

0前言

Android Studio目前已经成为开发Android的主要工具,作为开发者,调试、发现并解决BUG是家常便饭。正所谓,工欲善其事必先利其器,今天我们就来看看Android Studio中的调试技巧


1调试面板

首先,来看看Android studio中为我们提供的调试面板(标准情况下):这里写图片描述

点击右上角Restore ‘Threads’View可先展示目前相关的线程信息

这里写图片描述


2单步调试区

2.1 Show Execution Point这里写图片描述

点击该按钮,光标将定位到当前正在调试的位置

2.2 Force Step Into这里写图片描述

强制单步跳入,和step into功能类似。

主要区别在于:如果当前行有任何方法,不管该方法是我们自行定义还是类库提供的,都能跳入到方法内部继续执行

2.3 Drop Frame这里写图片描述

中断执行,并返回到方法被调用处,在这个过程中该方法对应的栈帧会从栈中移除并且所有上下文变量的值也恢复到该方法未执行时的状态。

2.4 Force Run to Cursor 这里写图片描述

非常好用的一个功能,可以忽视已经存在的断点,跳转到光标所在处。


3.求值表达式

Evaluate expression 这里写图片描述

点击该按钮会在当前调试的语句处嵌入一个交互式解释器,在该解释器中,你可以执行任何你想要执行的表达式进行求值操作

比如,我们在调试时执行到以下代码: 

这里写图片描述

此时执行EvaluateExpression,在该解释器中我们能做什么呢?在这里,我们可以对result进行求值操作:右键要求值,选择evaluate Expression。此时会显示如下:

这里写图片描述

在弹出的输入框中输入求值表达式,比如这里我们输入Math.min(result,50),如下图

这里写图片描述

点击执行,我们发现在Result中已经输出了结果,如下:

这里写图片描述


3.断点管理区

3.1 Return 这里写图片描述

点击该按钮会停止目前的应用,并且重新启动。用于想重新调试时。

3.2 Pause Program 这里写图片描述

点击该按钮将暂停应用的执行,如果想要恢复则可以使用下面提到的Resume Program

3.3 Resume Program这里写图片描述

该操作有恢复应用的含义,但是却有两种行为:
1. 在应用处在
暂停状态下,点击该按钮将恢复应用运行
2. 在很多情况下,我们会设置多个断点以便调试。在某些情况下,我们需要从当前断点移动到下一个断点两个断点之间的代码自动被执行,这样我们就不需要一步一步调试到下一个断点了,省时又省力。

3.4 Stop这里写图片描述

1)对普通的Java项目,点击该按钮意味着退出调试模式,但是应用还会执行完成;

2)而在Android项目中,点击该按钮,则意味这APP结束运行。

3.5 View Breakpoints这里写图片描述

点击该按钮会进入断点管理界面,在这里你可以查看所有断点,管理或者配置断点的行为,如删除,修改属性信息等:

这里写图片描述

3.6 Mute Breakpoints 这里写图片描述

使用该按钮来切换断点的状态:启动或者禁用

在调试过程中,你可以暂时禁用所有的断点,以实现应用正常的运行。该功能非常有用,比如当你在调试过程中,突然不想让断点干扰你所关心的流程时,可以临时禁用断点。

3.7 Get thread dump这里写图片描述

获取线程Dump,点击该按钮将进入线程Dump界面

这里写图片描述

线程工具区中最常用的是这里写图片描述,可以用来过滤线程,其他的不做解释了。

接下来我们来认识一下线程的类型,表示为不同的图标:


线程状态描述图标
Thread is suspended.这里写图片描述
Thread is waiting on a monitor lock.这里写图片描述
Thread is running.这里写图片描述
Thread is executing network operation, and is waiting for data to be passed.这里写图片描述
Thread is idle.这里写图片描述
Event Dispatch Thread that is busy.这里写图片描述
Thread is executing disk operation.

这里写图片描述

3.8 Settings这里写图片描述

点击该按钮将打开有关设置的列表:

这里写图片描述

3.8.1 Show Values lnline

调试过程中开启该功能,将会在代码右边显示变量值,即下图中红框所示部分:

这里写图片描述

3.8.2 Show MethodReturn Values

调试过程中启用该功能,将在变量区显示最后执行方法的返回值

举个例子来说,首先,关闭该功能,我们调试这段代码并观察其变量区:

这里写图片描述

开启该功能之后,再来观察变量区的变化:

这里写图片描述

继续往下调试:

这里写图片描述

这个功能简直是棒极了!在调试一段代码,并想看该代码中最后调用方法的最终结果时就非常有用了。

3.8.3 Auto-VariablesMode

开启这个功能后,ideaDebugger自动评估某些变量,大概就是当你执行在某个断点时,Debugger会检测当前调试点之前或者之后的变量的状态,然后在变量区选择性输出。

举个例子来说明,未开启该功能之前,变量区输出所有的变量信息:

这里写图片描述

开启之后,当你调试到第13行时,Debugger检测到num变量在之后没有被使用,那么在变量区就不会输出该变量的信息。

这里写图片描述

3.8.4 Sort valuesalphabetically

开启这个功能后,变量区中的输出内容会按照按字母顺序进行排序,不常用,还是按照默认的顺序好。

3.9 Help这里写图片描述

这个不用说了,有任何不明白的都可以查看官方帮助文档。

其他几个操作:SettingsPinClose留给各位自己去使用。


4.修改变量值

在调试过程中,我们可以方便的修改某个变量的值,如下:

这里写图片描述 

在上图中,当前result的值经过计算为10,这里我们通过Set Value将其计算结果修改为100


5.变量观察区

该区域将显示你所感兴趣的变量的值。

在调试模式下,你可以通过Add to Watches将某个变量添加到观察区,操作如下:

这里写图片描述

这里我们对name比较感兴趣,希望看到它的值的变化情况,因此我们将其特殊关照

需要注意,此时因为name成员变量,因此在对象观察区也可看到该值。如果是局部变量,无疑只能用这种方式了。


6.断点

6.1 条件断点

所谓条件断点就是在特定条件发生的断点,也就是,我们可将某个断点设置为只对某种事件感兴趣,最典型的应用就是在列表循环中,我们希望在某特定的元素出现时暂停程序运行。比如,现在我们有个list中,其中包含了q1q2q3q四个元素,我们希望在遍历到2q时暂停程序运行,那么需要进行如下操作: 

在需要的地方添加断点,如下:

这里写图片描述

断点处左键单击,在Condition处填写过滤条件.此处我们只关心2q,因此填写s.equals("2q")

这里写图片描述

6.2 日志断点

该类型的断点不会使程序停下来,而是在输出我们要它输出的日志信息,然后继续执行

具体操作如下: 同样在断点处左键单击,在弹出的对话框中取消选中Suspend

这里写图片描述

在弹出的控制面板中,选中Log evaluated expression,然后再填写想要输出的日志信息,如下:

这里写图片描述

当调试过程遇到该断点将会输出结果,如下:

这里写图片描述

6.3 异常断点

所谓的异常断点就是在调试过程中,一旦发生异常,则会立刻定位到异常抛出的地方

比如在调试异常中,我们非常关注运行时异常,希望在产生任何运行异常时及时定位,那么此时就可以利用该类型异常,在上线之前,进行异常断点调试非常有利于减少正式环境中发生crash的几率。

具体操作如下:Run菜单项中,选择ViewBreakpoints,如下:

这里写图片描述

在管理断点面板中,点击+

这里写图片描述

在弹出的下拉选择列表中,我们选择Java ExceptionBreakpoints。

这里写图片描述

这里我们选中Search By Name,在下面的输入框中输入我们所关心的异常类型。

此处我们关心NullPointerException,在调试过程一旦发生NullPointerException,调试器就会定位到异常发生处。

这里写图片描述

6.4 方法断点这里写图片描述

传统的调试方式是以行为单位的,所谓单步调试;但是很多时候我们关心的是某个函数的参数,返回值;(回想一下我们使用日志的时候打印的最多的信息难道不是函数的参数和返回值吗?)使用方法断点,我们可以在函数级别进行调试;如果经常跳进跳出函数或者只对某个函数的参数感兴趣,这种类型的断点非常实用。具体使用方法有两种方式;最简单的是在你感兴趣的方法头那一行打上断点

6.5 Field WatchPoint 这里写图片描述

Filed WatchPoint是本质上是一种特殊的断点,也称为属性断点:当我们某个字段值被修改的时候,程序暂停在修改处。通常在调试多线程时尤为可用,能帮我们及时的定位并发错误的问题。其使用和添加普通的断点并无不同,断点图标稍有不同


7.调试的两种方式

这里写图片描述

到目前,调试的相关基础我们已经介绍完了,但是不少同学对Android Studio这两个按钮感到困惑:Debug和Attach process 
这里我们就简单介绍一下这两者的区别:

Debug:以调试模式安装运行,断点可以在运行之前设置,也可在运行后设置,是多数人最常用的调式方式

Attach process:和Debug方式相比,能够将调试器attach到任何正在运行的进程。比如,我们可以通过attach process想要调试的进程。然后,在需要的地方设置相关断点即可

作者:SEU_Calvin 发表于2016/8/17 17:20:58 原文链接
阅读:90 评论:0 查看评论

安卓日记——仿QQ列表

$
0
0

最近学习了如何做一个像QQ的左滑RecyclerView的item显示选项的,主要是用到Scroller

我们首先新建一个自己的RecyclerView

定义好一些要用的的变量
重写构造方法,把前两个构造方法改为如下,使无论如何构造都要执行第三个构造方法
在第三个构造方法里初始化Scroller

public class LeftSwipeMenuRecyclerView extends RecyclerView {

    //置顶按钮
    private TextView tvTop;
    //删除按钮
    private TextView tvDelete;
    //item相应的布局
    private LinearLayout mItemLayout;
    //菜单的最大宽度
    private int mMaxLength;

    //上一次触摸行为的x坐标
    private int mLastX;
    //上一次触摸行为的y坐标
    private int mLastY;

    //当前触摸的item的位置
    private int mPosition;

    //是否在垂直滑动列表
    private boolean isDragging;
    //item是在否跟随手指移动
    private boolean isItemMoving;
    //item是否开始自动滑动
    private boolean isStartScroll;

    //左滑菜单状态   0:关闭 1:将要关闭 2:将要打开 3:打开
    private int mMenuState;
    private static int MENU_CLOSED = 0;
    private static int MENU_WILL_CLOSED = 1;
    private static int MENU_OPEN = 2;
    private static int MENU_WILL_OPEN = 3;

    //实现弹性滑动,恢复
    private Scroller mScroller;

    //item的事件监听
    private OnItemActionListener mListener;

    public LeftSwipeMenuRecyclerView(Context context) {
        this(context, null);
    }

    public LeftSwipeMenuRecyclerView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public LeftSwipeMenuRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mScroller = new Scroller(context, new LinearInterpolator());
    }

重写onTouchEvent方法

event主要有以下几个Action

  1. ACTION_DOWN 手指接触到屏幕
  2. ACTION_MOVE 手指在屏幕滑动
  3. ACTION_UP 手指离开屏幕

一开始肯定要获取x和y的相对坐标

int x= (int) event.getX();
int y= (int) event.getY();

然后接下来分别处理3个不同的行为

1.ACTION_DOWN

case MotionEvent.ACTION_DOWN:
                if (mMenuState == MENU_CLOSED) {
                    //根据坐标获得view
                    View view = findChildViewUnder(x, y);
                    if (view == null) {
                        return false;
                    }
                    //获得这个view的ViewHolder
                    RVAdapter.Holder holder = (RVAdapter.Holder) getChildViewHolder(view);
                    //获得这个view的position
                    mPosition = holder.getAdapterPosition();
                    //获得这个view的整个布局
                    mItemLayout = holder.llLayout;
                    //获得这个view的删除按钮
                    tvDelete = holder.tvDelete;
                    //这个view的整个置顶按钮
                    tvTop = holder.tvTop;
                    //两个按钮的宽度
                    mMaxLength = tvDelete.getWidth() + tvTop.getWidth();

                    //设置删除按钮点击监听
                    tvDelete.setOnClickListener(new OnClickListener() {
                        @Override
                        public void onClick(View view) {
                            mItemLayout.scrollTo(0, 0);
                            mMenuState =MENU_CLOSED;
                            mListener.OnItemDelete(mPosition);
                        }
                    });
                    //设置置顶按钮点击监听
                    tvTop.setOnClickListener(new OnClickListener() {
                        @Override
                        public void onClick(View view) {
                            mItemLayout.scrollTo(0, 0);
                            mMenuState =MENU_CLOSED;
                            mListener.OnItemTop(mPosition);
                        }
                    });
                    //如果是打开状态,点击其他就把当前menu关闭掉
                } else if (mMenuState == MENU_OPEN) {
                    mScroller.startScroll(mItemLayout.getScrollX(), 0, -mMaxLength, 0, 200);
                    invalidate();
                    mMenuState = MENU_CLOSED;
                    //该点击无效
                    return false;
                } else {
                    return false;
                }
                break;

2.ACTION_MOVE

            case MotionEvent.ACTION_MOVE:
                //计算偏移量
                int dx = mLastX - x;
                int dy = mLastY - y;
                //当前滑动的x
                int scrollx = mItemLayout.getScrollX();

                if (Math.abs(dx) > Math.abs(dy)) {

                    isItemMoving = true;
                    //超出左边界则始终保持不动
                    if (scrollx + dx <= 0) {
                        mItemLayout.scrollTo(0, 0);
                        //滑动无效
                        return false;
                    //超出右边界则始终保持不动
                    } else if (scrollx + dx >= mMaxLength) {
                        mItemLayout.scrollTo(mMaxLength, 0);
                        //滑动无效
                        return false;
                    }
                    //菜单随着手指移动
                    mItemLayout.scrollBy(dx, 0);
                //如果水平移动距离大于30像素的话,recyclerView不会上下滑动
                }else  if (Math.abs(dx) > 30){
                    return true;
                }
                //如果菜单正在打开就不能上下滑动
                if (isItemMoving){
                    mLastX = x;
                    mLastY = y;
                    return true;
                }
                break;

3.ACTION_UP

case MotionEvent.ACTION_UP:
                //手指抬起时判断是否点击,静止且有Listener才能点击
                if (!isItemMoving && !isDragging && mListener != null) {
                    mListener.OnItemClick(mPosition);
                }
                isItemMoving = false;

                //等一下要移动的距离
                int deltaX = 0;
                int upScrollx = mItemLayout.getScrollX();
                //滑动距离大于1/2menu长度就自动展开,否则就关掉
                if (upScrollx >= mMaxLength / 2) {
                    deltaX = mMaxLength - upScrollx;
                    mMenuState = MENU_WILL_OPEN;
                } else if (upScrollx < mMaxLength / 2) {
                    deltaX = -upScrollx;
                    mMenuState = MENU_WILL_CLOSED;
                }
                //知道我们为什么不直接把mMenuState赋值为MENU_OPEN或者MENU_CLOSED吗?因为滑动时有时间的,我们可以在滑动完成时才把状态改为已经完成
                mScroller.startScroll(upScrollx, 0, deltaX, 0, 200);
                isStartScroll = true;
                //刷新界面
                invalidate();
                break;

之后还要在onTouchEvent方法里刷新坐标

        //只有更新mLastX,mLastY数据才会准确
        mLastX = x;
        mLastY = y;
        return super.onTouchEvent(e);

因为我们用到了startScroll所以要重写computeScroll方法

    public void computeScroll() {
        //判断scroller是否完成滑动
        if (mScroller.computeScrollOffset()) {
            mItemLayout.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            //这个很重要
            invalidate();
        //如果已经完成就改变状态
        } else if (isStartScroll) {
            isStartScroll = false;
            if (mMenuState == MENU_WILL_CLOSED) {
                mMenuState = MENU_CLOSED;
            }
            if (mMenuState == MENU_WILL_OPEN) {
                mMenuState = MENU_OPEN;
            }
        }
    }

**还要监听RecyclerView是否在上下滑动

 @Override
    public void onScrollStateChanged(int state) {
        super.onScrollStateChanged(state);
        //是否在上下滑动
        isDragging = state == SCROLL_STATE_DRAGGING;
    }

还要设置Listener

//设置Listener
    public void setOnItemActionListener(OnItemActionListener onItemActionListener) {
        this.mListener = onItemActionListener;
    }

这个Listener是要自己新建的

public interface OnItemActionListener {
    void OnItemClick(int position);
    void OnItemTop(int position);
    void OnItemDelete(int position);
}

最后是点击,置顶,删除在Activity里的回调

这里只展示回调实现部分,我这里用的List是LinkedList,可以在第一位添加数据

rv.setOnItemActionListener(new OnItemActionListener() {
            //点击
            @Override
            public void OnItemClick(int position) {
                Toast.makeText(MainActivity.this,"Click"+position,Toast.LENGTH_SHORT).show();
            }
            //置顶
            @Override
            public void OnItemTop(int position) {
                //获得当前位置的内容
                String temp =list.get(position);
                //移除这个item
                list.remove(position);
                //把它添加到第一个
                list.addFirst(temp);
                //更新数据源
                adapter.notifyDataSetChanged();
            }
            //删除
            @Override
            public void OnItemDelete(int position) {
                list.remove(position);
                //更新数据源
                adapter.notifyDataSetChanged();
            }
        });

Adapter和布局的代码太简单我就不放出来了,大家可以到源码里看看有什么

效果图

效果图

源码地址

作者:qq_32198277 发表于2016/8/17 17:22:02 原文链接
阅读:26 评论:0 查看评论

iOS REST服务

$
0
0

做过iOS开发的人员都知道,iOS应用需要通过某种与远程Web服务器通信。有些应用可以在没有网络连接的情况下使用,只在网络连接可用时才与服务器同步数据的应用就是这样。当然还有一类应用需要在几乎连续的网络连接下才能为用户提供有效的价值。这类应用通常作为Web服务的移动客户端。

大部分iOS应用都要用到这种功能,折让iPhone成为有史以来最好的互联网连接设备。不过,由于设备总是在移动,连接和接收信号可能都很差。作为iOS开发者得确保应用的感知时间差不多恒定,就好像全部内容存储在本地一样。所以本地缓存数据就是来解决这个问题的。今天我们来看看iOS如何用缓存技术来结局很差的网络连接甚至无网络连接带来的问题。

REST
REST服务有三大特征:无状态,统一资源定位和可缓存。
无状态:每次API调用都被视作新的请求,服务器不会记录客户端上下文。客户端需要维护服务器的状态。
统一资源定位:URL是REST的参数。REST使用这种资源定位,而且不维护客户端状态。
可缓存:REST的响应是以一种统一的,双方一致同意的格式返回给客户端,这样能够更好地解耦。目前,最常用的是XML和JSON。

XML解析

XML解析最常见的就是DOM和SAX解析器。两者的区别就是SAX是一串流解析器,它逐句便利整个XML文档,通过回掉函数返回解析结果。大部分SAX解析器接受一个URL作为参数,解析完目标数据就将数据返回。之前说过NSXMLParser,通过代理的方式回调。

parserDidStartDocument
parserDidEndDocument
parser:didiStartElement:namespaceURL:qualifiedName:attributes:
parser:didiEndElement:namespaceURL:qualifiedName:
parser:foundCharacters:

DOM解析

需要先把XML整个文档加载到内存中才开始解析。其优势在于可以使用Xpath查询访问随机数据,不需要像SAX使用委托。对于iOS开发没有内置的DOM解析器。一般使用第三方包装器,我再网上看到有:基于libxml2的KissXML,TouchXML和GDataXML。

使用DOM解析可以让代码更加简洁。虽然在处理请求时会花费更多的执行时间,但是跟网络操作消耗的时间相比,简直微不足道。

不过,我在项目中用的是SAX解析。

JSON解析

相信做客户端开发的,一定都逃不过Json吧。苹果有自己的解析JSON框架,也是我们常用的NSJSONSerialization。还有很多不错的第三方框架:SBJson,JSONKit,TouchJSON。基本上都是提供基于NSString,NSArray,NSDictionary的分类扩展。我在项目中用的是苹果自带的。

// 将字典或者数组转化为JSON串
- (NSData *)toJSONData:(id)theData{

    NSError *error = nil;
    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:theData
                                                      options:NSJSONWritingPrettyPrinted
                                                         error:&error];

    if ([jsonData length] > 0 && error == nil){
        return jsonData;
    }else{
        return nil;
    }
}
使用这个方法的返回,我们就可以得到想要的JSON串
NSString *jsonString = [[NSString alloc] initWithData:jsonData
                                                 encoding:NSUTF8StringEncoding];

// 将JSON串转化为字典或者数组
- (id)toArrayOrNSDictionary:(NSData *)jsonData{
    NSError *error = nil;
    id jsonObject = [NSJSONSerialization JSONObjectWithData:jsonData
                                                   options:NSJSONReadingAllowFragments error:&error];

    if (jsonObject != nil && error == nil){
        return jsonObject;
    }else{
        // 解析错误
         return nil;
    }
}
//将JSON串与NSArray和NSDictionary的操作进行封装

1.NSString转化为NSArray或者NSDictionary
#import "NSString+JSONCategories.h"
@implementation NSString(JSONCategories)
-(id)JSONValue;
{
    NSData* data = [self dataUsingEncoding:NSUTF8StringEncoding];
    __autoreleasing NSError* error = nil;
    id result = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
    if (error != nil) return nil;
    return result;
}
@end
2.NSArray或者NSDictionary转化为NSString
#import "NSObject+JSONCategories.h"
@implementation NSObject (JSONCategories)
-(NSData*)JSONString;
{
    NSError* error = nil;
    id result = [NSJSONSerialization dataWithJSONObject:self
                                                options:kNilOptionserror:&error];
    if (error != nil) return nil;
    return result;
}
@end

之前说过NSURLConnection。但是,为了开发REST服务,用MKNetworkKit更加方便。封装了很多常用的功能:基本认证/摘要认证,表单发布,上传/下载文件。还可以使用队列来管理网络请求。

Note:与浏览器不同的是,大部分运营商网络都会限制并发数据连接的数量,在EDGE连接中不要进行1个以上的网络操作,在3G网络中不要使用2个以上的并发网络操作,在Wi-Fi连接中的并发网络操作数量不要超过6个。

缓存

为了改善性能,提高用户的体验质量,离线工作是重点。一般有两种缓存技术支持离线:
按需缓存:应用缓存起请求应答,和Web浏览器的工作原理一样
预缓存:缓存全部内容以便离线访问

按需缓存思想:把从服务器获取的内容以某种格式存放在本地文件系统,之后对于每次请求,检查缓存中是否存在这块数据,只有当数据不存在(或者过期)的情况下才从服务器获取。这样,缓存层就和处理器的高速缓存差不多。获取数据的速度比数据本身重要。按需缓存的工作原理类似浏览器,允许我们查看以前查看或者访问过的内容。我们开发中,通讯录,消息等这些都是采用的按需缓存,不需要后台线程做这件事,也可以在一个URL请求返回成功实现按需缓存。

预缓存思想:需要一个后台县城访问数据并以有意义的格式保存,本地缓存无需重新连接服务器就可以被编辑。对预缓存来说,数据丢失或者缓存不命中是不可接受的。CoreData (结构化数据)试试先这种缓存的一种方式。

沙盒文件:
每个ios应用都有自己的应用沙盒,应用沙盒就是文件系统目录,与其他应用的文件系统隔离,ios系统不允许访问其他应用的应用沙盒。在ios8中已经开放访问。

应用沙盒一般包括以下几个文件目录:应用程序包、Documents、Libaray(下面有Caches和Preferences目录)、tmp。

应用程序包:包含所有的资源文件和可执行文件。

Documents:保存应用运行时生成的需要持久化的数据,iTunes会自动备份该目录。苹果建议将程序中建立的或在程序中浏览到的文件数据保存在该目录下,iTunes备份和恢复的时候会包括此目录

tmp:保存应用运行时所需的临时数据,使用完毕后再将相应的文件从该目录删除。应用没有运行时,系统也有可能会清除该目录下的文件,iTunes不会同步该目录。iphone重启时,该目录下的文件会丢失。

Library:存储程序的默认设置和其他状态信息,iTunes会自动备份该目录。

Libaray/Caches:存放缓存文件,iTunes不会备份此目录,此目录下文件不会在应用退出删除。一般存放体积比较大,不是特别重要的资源。

Libaray/Preferences:保存应用的所有偏好设置,ios的Settings(设置)应用会在该目录中查找应用的设置信息,iTunes会自动备份该目录。

沙盒文件目录获取代码:

//Home目录NSString *homeDirectory = NSHomeDirectory();

//Document目录NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *path = [paths objectAtIndex:0];

//Cache目录NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *path = [paths objectAtIndex:0];

//Libaray目录NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
NSString *path = [paths objectAtIndex:0];

//tmp目录
NSString *tmpDir = NSTemporaryDirectory();

就像我们实验室做的企业级通信APP,像用户的账号密码,登陆时需要的IP号和端口号,用户头像等这些信息都需要存放到Library/Cache下。而偏好设置像响铃,震动这些存放到Library/preference下。

而我们的源代码打包后放在Document路径下。我们下载的文件,下载过程中会放在tmp下,下载完成再放到Library。

数据缓存方法

1)数据模型缓存

我们可以用NSKeyedArchiver来实现。首先需要把模型类进行编解码重写。也就是要遵循NSCoding协议。

@interface Student : NSObject <NSCoding>
@property (nonatomic, assign) int idNum;
@property (nonatomic, copy) NSString *name;
@end

@implementation Student

#pragma mark 编码 对对象属性进行编码的处理
- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [aCoder encodeInt:_idNum forKey:IDNUM];
    [aCoder encodeObject:_name forKey:NAME];
}

#pragma mark 解码 解码归档数据来初始化对象
- (id)initWithCoder:(NSCoder *)aDecoder
{
    if (self = [super init]) {
        _idNum = [aDecoder decodeIntForKey:IDNUM];
        _name = [aDecoder decodeObjectForKey:NAME];
    }
    return self;
}
@end

然后对模型对象进行序列化

//获得文件路径
    NSString *documentPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSString *filePath = [documentPath stringByAppendingPathComponent:@"file.archiver"];

//归档(序列化)
NSArray *archiveAry = @[@"jereh",@"ios"];
if ([NSKeyedArchiver archiveRootObject: archiveAry toFile:filePath]) {

}

//解归档(反序列化)
NSArray *unArchiveAry = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];

2)直接对文件进行操作我们常用到的plist
创建文件->添加数据
读取:

//path 读取当前程序定义好的provinces.plist省份城市文件
    NSString *path = [[NSBundle mainBundle] pathForResource:@"student" ofType:@"plist"];

    NSDictionary *data = [NSDictionary dictionaryWithContentsOfFile:path];
    self.studentContent = [NSArray arrayWithArray:[data objectForKey:@"student"]];//array数组的名称就叫student
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];//获取标准函数对象
    NSMutableDictionary *default = [defaults objectForKey:@"xxx"];//通过对象获取名称下NSMutableDictionary数据
    NSString *studentname= [default objectForKey:@"name"];

增删改查

    [defaults setObject:@"kaka" forKey:@"newdata"];//添加id对象类型数据
    [defaults objectForKey:@"newdata"];//获取对象类型数据

    [defaults setDouble:2013 forKey:@"double"];//添加double类型数据
    [defaults doubleForKey:@"double"];//获取double类型数据

    [defaults setBool:NO forKey:@"BOOL"];//添加BOOL类型数据
    [defaults boolForKey:@"BOOL"];//获取BOOL类型数据

    [defaults setInteger:12 forKey:@"int"];//添加int类型数据
    [defaults integerForKey:@"int"];//获取int类型数据

    [defaults setFloat:0.23 forKey:@"float"];//添加Float类型数据
    [defaults floatForKey:@"float"];//获取float类型数据

    [defaults removeObjectForKey:@"newdata"];//删除对象数据

3)SQLite
项目中的sqlit3,简单说一下使用过程吧

使用的过程根据使用的函数大致分为如下几个过程:

sqlite3_open()
sqlite3_prepare()
sqlite3_step()
sqlite3_column()
sqlite3_finalize()
sqlite3_close()

具体的讲解,可以参考这微博主:http://blog.csdn.net/zhuzhihai1988/article/details/7878093

4) CoreData
CoreData更像是一个对象序列化框架,而不仅仅是一个数据库的API。我理解的是非关系型数据库的一种,采用key-value的形式存储。

作者:SkySuperWL 发表于2016/8/17 17:22:38 原文链接
阅读:24 评论:0 查看评论

java RSA非对称加密详解

$
0
0

简介

RSA公钥加密算法是1977年由罗纳德·李维斯特(Ron Rivest)、阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)一起提出的。1987年首次公布,当时他们三人都在麻省理工学院工作。RSA就是他们三人姓氏开头字母拼在一起组成的。

RSA算法基于一个十分简单的数论事实:将两个大质数相乘十分容易,但是想要对其乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥。此博文旨在实现具体用法,对算法原理不作阐述,算法详情请百度~

非对称加密 VS 对称加密 VS 不可逆加密

对称加密是因为加密和解密的钥匙相同,而非对称加密是加密和解密的钥匙不同。对称和非对称加密都是可逆的(因为有密钥对,一个负责加密,一个负责解密)。

对称加密

对称加密称为密钥加密,速度快,但加密和解密的钥匙必须相同,只有通信双方才能知道密钥,常见的有DES,3DES,AES对称加密。

非对称加密

非对称加密称为公钥加密,算法更加复杂,速度慢,加密和解密钥匙不相同,任何人都可以知道公钥,只有一个人持有私钥可以解密。常见的就是RSA了。

不可逆加密

还有一种加密方法:不可逆加密。典型的代表就是MD5加密了。

对称加密算法、非对称加密算法和不可逆加密算法可以分别应用于数据加密、身份认证和数据安全传输。

使用场景

使用场景就太多了,网络交互时,我们希望数据能经过加密后再传输,比如账户密码之类~

加解密的两种实现方式

RSA非对称加密,在我们具体实现的环境中,有两种方法,通过文件形式和字符串形式。

通过文件加解密

我们可以通过将公钥和私钥以文件形式保存,对某些需要加密的字符串进行加解密~

生成密钥对

不管是java,c,还是在其他语言当中,rsa算法是不变的。此为java
当中生成密钥对,并将密钥对保存到文件中,代码如下:

private static void generateKeyPair() throws Exception{
        /** RSA算法要求有一个可信任的随机数源 */
        SecureRandom sr = new SecureRandom();
        /** 为RSA算法创建一个KeyPairGenerator对象 */
        KeyPairGenerator kpg = KeyPairGenerator.getInstance(ALGORITHM);
        /** 利用上面的随机数据源初始化这个KeyPairGenerator对象 */
        kpg.initialize(KEYSIZE, sr);
        /** 生成密匙对 */
        KeyPair kp = kpg.generateKeyPair();
        /** 得到公钥 */
        Key publicKey = kp.getPublic();
        /** 得到私钥 */
        Key privateKey = kp.getPrivate();
        /** 用对象流将生成的密钥写入文件 */
        ObjectOutputStream oos1 = new ObjectOutputStream(new FileOutputStream("publickey.keystore"));
        ObjectOutputStream oos2 = new ObjectOutputStream(new FileOutputStream("privatekey.keystore"));
        oos1.writeObject(publicKey);
        oos2.writeObject(privateKey);
        /** 清空缓存,关闭文件输出流 */
        oos1.close();
        oos2.close();
    }

可以在相对路径下找到publickey.keystore和private.keystore两文件。

对字符串加密

public static String encrypt(String source) throws Exception{

        /** 将文件中的公钥对象读出 */
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("publickey.keystore"));
        Key key = (Key) ois.readObject();
        ois.close();
        /** 得到Cipher对象来实现对源数据的RSA加密 */
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, key);
        byte[] b = source.getBytes();
        /** 执行加密操作 */
        byte[] b1 = cipher.doFinal(b);
        BASE64Encoder encoder = new BASE64Encoder();
        return encoder.encode(b1);
    }

对字符串解密

对用公钥加密后的数据,通过相对应的私钥解密:

public static String decrypt(String cryptograph) throws Exception{
        /** 将文件中的私钥对象读出 */
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("private.keystore"));
        Key key = (Key) ois.readObject();
        /** 得到Cipher对象对已用公钥加密的数据进行RSA解密 */
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, key);
        BASE64Decoder decoder = new BASE64Decoder();
        byte[] b1 = decoder.decodeBuffer(cryptograph);
        /** 执行解密操作 */
        byte[] b = cipher.doFinal(b1);
        return new String(b);
    }

测试

public static void main(String[] args) throws Exception {

        String source = "luoxiaohui";//要加密的字符串
        String cryptograph = encrypt(source);//生成的密文
        System.out.println("生成的密文--->"+cryptograph);

        String target = decrypt(cryptograph);//解密密文
        System.out.println("解密密文--->"+target);
    }

打印的log如下所示:

生成的密文--->d0MzejV4uoQ4QJQJ+l22jdHQ0IEJshXxdIbyvmm3NBs7j/+9yPbOhsgLmywytZzKxsPDewSgcRf5
+xi1BedMxVs3amb6tBicRX0uL02kKnE4d/K2W76JMS2g0oqbB+sX9BAFc8YgzJ4ZUoP44dZWSGVd
TZHiRSnz2PPncmFqFsE=
解密密文--->luoxiaohui

优化

说明RSA加解密已经能实现啦!但看着这个密文感觉有点丑,有一些特殊符号,处于强迫症,我想把密文转为16进制:

String source = "luoxiaohui";//要加密的字符串

        String cryptograph = encrypt(source);//生成的密文
        String hexCrypt = HexUtil.bytes2Hex(cryptograph.getBytes(),false);
        System.out.println("生成的密文--->"+hexCrypt);

        String target = decrypt(HexUtil.hex2String(hexCrypt));//解密密文
        System.out.println("解密密文--->"+target);

生成的log如下:

生成的密文--->42376F665A4C425770344D557444664F4E41526E326E5A37665978754F4E6C357062747069454D776F386E5573634D4F382B4475436279774234466A435236386C5631734E6C55724D637A470A7343736C53342F52536664766F313775304A2B4C4C434F326C4552364A7A523363737970477076337537753852376756484C4C704F43454C6E4264324C7A4955626B6D77594F544C6663724A0A75743271564B74512F734270653451546837383D
解密密文--->luoxiaohui

这样生成的密文看着美观很多了,而且,在网络传输时,不用因为有特殊字符而去转码了。

通过字符串加解密

有时候,我们项目将公钥私钥保存在文件中不太方便,或者是跟不同公司项目合作,对方只给你一个公钥字符串,此时要知道如何去实现加解密。

如何将文件形式的公钥私钥转成字符串形式的公钥私钥

其实转为字符串形式,我们主要是要从公钥私钥对象中,得到两个参数,模量modulus和指数系数exponent,这两参数能重新构成公钥私钥对象,从而进行加解密,

private static void generateKeyPairString() throws Exception{
/** RSA算法要求有一个可信任的随机数源 */
        SecureRandom sr = new SecureRandom();
        /** 为RSA算法创建一个KeyPairGenerator对象 */
        KeyPairGenerator kpg = KeyPairGenerator.getInstance(ALGORITHM);
        /** 利用上面的随机数据源初始化这个KeyPairGenerator对象 */
        kpg.initialize(KEYSIZE, sr);
        /** 生成密匙对 */
        KeyPair kp = kpg.generateKeyPair();
        /** 得到公钥 */
        Key publicKey = kp.getPublic();
        /** 得到私钥 */
        Key privateKey = kp.getPrivate();
        /** 用字符串将生成的密钥写入文件 */

        String algorithm = publicKey.getAlgorithm(); // 获取算法
        KeyFactory keyFact = KeyFactory.getInstance(algorithm);
        BigInteger prime = null;
        BigInteger exponent = null;

        RSAPublicKeySpec keySpec = (RSAPublicKeySpec)keyFact.getKeySpec(publicKey, RSAPublicKeySpec.class);

        prime = keySpec.getModulus();
        exponent = keySpec.getPublicExponent();
        System.out.println("公钥模量:"+HexUtil.bytes2Hex(prime.toByteArray()));
        System.out.println("公钥指数:"+HexUtil.bytes2Hex(exponent.toByteArray()));


        System.out.println(privateKey.getAlgorithm());
        RSAPrivateCrtKeySpec privateKeySpec = (RSAPrivateCrtKeySpec)keyFact.getKeySpec(privateKey, RSAPrivateCrtKeySpec.class);
        BigInteger privateModulus = privateKeySpec.getModulus();
        BigInteger privateExponent = privateKeySpec.getPrivateExponent();

        System.out.println("私钥模量:"+HexUtil.bytes2Hex(privateModulus.toByteArray()));
        System.out.println("私钥指数:"+HexUtil.bytes2Hex(privateExponent.toByteArray()));
    }

在main()方法中执行此方法,能得到公钥私钥的指数系数和模量,以16进制保存,打印的log如下:

公钥模量:00d23587d5b17c717c033926981e10b0d39cd162e226dc5fee7073f91201c099fc86b6323acfd7bcf17e3cbb2a0ac4b0918322f0e6d6af94ba8b9094fbab4fe842ac418638c4bc83305e22a3ee9b9c5fa100daa3070f1fa2de56cffe3b80a74553d883e9695be523c568d38dfa56da9f4dab081d753f52a649dca85e07bc0fcdfd
公钥指数:010001
私钥模量:00d23587d5b17c717c033926981e10b0d39cd162e226dc5fee7073f91201c099fc86b6323acfd7bcf17e3cbb2a0ac4b0918322f0e6d6af94ba8b9094fbab4fe842ac418638c4bc83305e22a3ee9b9c5fa100daa3070f1fa2de56cffe3b80a74553d883e9695be523c568d38dfa56da9f4dab081d753f52a649dca85e07bc0fcdfd
私钥指数:00924cc75926c9e181da0c709bf670cf60b807d2b66b2d7d66c9c52d5826f81133fbddda5fac400e345513977fcf36cd5cb8d41cadcc452f5215c86ea829b6d7822c57a96c7f4bd00d121dcde41bddef186d6ca2130047482f0ae92dcb5b9524c856f0b13e948d4fa59fe3fa7d7af4533e662503e314f6840db5523935a15c9e51

用公钥字符串加密

直接上代码

 public static RSAPublicKey getRSAPublicKey(String hexModulus, String hexPublicExponent) {
        if (isBlank(hexModulus) || isBlank(hexPublicExponent)) {
            System.out.println("hexModulus and hexPublicExponent cannot be empty. return null(RSAPublicKey).");
            return null;
        }
        byte[] modulus = null;
        byte[] publicExponent = null;
        try {
            modulus = HexUtil.hex2Bytes(hexModulus);
            publicExponent = HexUtil.hex2Bytes(hexPublicExponent);
        } catch (Exception ex) {
            System.out.println("hexModulus or hexPublicExponent value is invalid. return null(RSAPublicKey).");
            ex.printStackTrace();
        }
        if (modulus != null && publicExponent != null) {
            return generateRSAPublicKey(modulus, publicExponent);
        }
        return null;
    }

    public static String encryptString(PublicKey publicKey, String plaintext) {
        if (publicKey == null || plaintext == null) {
            return null;
        }
        byte[] data = plaintext.getBytes();
        try {
            byte[] en_data = encrypt(publicKey, data);
            return new String(HexUtil.bytes2Hex(en_data));
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

用私钥字符串解密

直接上代码~

/**
     * 根据给定的16进制系数和专用指数字符串构造一个RSA专用的私钥对象。
     *
     * @param hexModulus         系数。
     * @param hexPrivateExponent 专用指数。
     * @return RSA专用私钥对象。
     */
    public static RSAPrivateKey getRSAPrivateKey(String hexModulus, String hexPrivateExponent) {
        if (isBlank(hexModulus) || isBlank(hexPrivateExponent)) {
            System.out.println("hexModulus and hexPrivateExponent cannot be empty. RSAPrivateKey value is null to return.");
            return null;
        }
        byte[] modulus = null;
        byte[] privateExponent = null;
        try {
            modulus = HexUtil.hex2Bytes(hexModulus);
            privateExponent = HexUtil.hex2Bytes(hexPrivateExponent);
        } catch (Exception ex) {
            System.out.println("hexModulus or hexPrivateExponent value is invalid. return null(RSAPrivateKey).");
            ex.printStackTrace();
        }
        if (modulus != null && privateExponent != null) {
            return generateRSAPrivateKey(modulus, privateExponent);
        }
        return null;
    }

    /**
     * 使用给定的私钥解密给定的字符串。
     *
     * 若私钥为 {@code null},或者 {@code encrypttext} 为 {@code null}或空字符串则返回 {@code null}。
     * 私钥不匹配时,返回 {@code null}。
     *
     * @param privateKey  给定的私钥。
     * @param encrypttext 密文。
     * @return 原文字符串。
     */
    public static String decryptString(PrivateKey privateKey, String encrypttext) {
        if (privateKey == null || isBlank(encrypttext)) {
            return null;
        }
        try {
            byte[] en_data = HexUtil.hex2Bytes(encrypttext);
            byte[] data = decrypt(privateKey, en_data);
            return new String(data);
        } catch (Exception ex) {
            System.out.println(String.format("\"%s\" Decryption failed. Cause: %s", encrypttext, ex.getCause().getMessage()));

        }
        return null;
    }

测试

在main()中执行方法:

public static void main(String[] args) {

        String source = "luoxiaohui";

        PublicKey publicKey = RSAUtil.getRSAPublicKey(publicModulus, publicexponent);
        String encript = RSAUtil.encryptString(publicKey, source);
        System.out.println("加密后数据:"+encript);

        PrivateKey privateKey = RSAUtil.getRSAPrivateKey(privateModulus, privateexponent);
        String newSource = RSAUtil.decryptString(privateKey, encript);

        System.out.println("解密后数据:"+newSource);
    }

打印log如下:

加密后数据:35b7eece347b916732af92d293979328573c8f470209a46548d0b7d1a3d5b6751dd162c84b1d5153fdab6b590ea85a3f32fee1fd658f875b210ff792ff24b4b1960b8944f25e671ded9bb087ffe1915b8449f2e517d4b3039a13f27047befa39af8399b5452307fd8751dc8833dbd9b616a264ff2e3bdf3f827d1a90ec4a243f
解密后数据:luoxiaohui

到此,通过原有的模量和指数加密,然后再解密,功能已全部实现~

踩过的坑

java加密 C语言解密

由于项目用RSA非对称加密需求,当前端用java加密,后端用C语言解密,需要注意

Cipher ci = Cipher.getInstance(ALGORITHOM);

当中的填充参数ALGORITHOM,要跟C语言对称,java有多种填充方式,默认是”RSA”,而c语言中有且只有一种”RSA/ECB/NoPadding”,如果后台对应c语言解密,切记我们java前端需要将此参数改为”RSA/ECB/NoPadding”。

代码下载

作者:a394268045 发表于2016/8/17 17:26:27 原文链接
阅读:138 评论:0 查看评论

点击Edittext,同时出现搜索历史展示和系统软键盘(升级版)

$
0
0

上一篇博客,也是写的点击Edittext,同时出现搜索历史展示和系统软键盘,但是,有个小问题:如果有2条以上的搜索历史,展示的时候,默认显示前2条。这个时候,如果点击“X”号删除这条搜索历史,是不行的,必须要全部展示的情况下,才能删除选中的那一条。这篇博客,对这个bug进行了处理

源码如下

展示搜索历史的Listview的Item:option_item_3

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:orientation="vertical"
    >

    <RelativeLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#00aa00"
        android:gravity="center_vertical"
        >

        <ImageView
            android:id="@+id/delImage"
            android:layout_width="20dp"
            android:layout_height="20dp"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:layout_marginRight="10dp"
            android:src="@mipmap/ic_launcher"
            android:textSize="15dp"/>

        <TextView
            android:id="@+id/item_text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:layout_toLeftOf="@id/delImage"
            android:paddingBottom="10dp"
            android:paddingLeft="5dp"
            android:paddingTop="10dp"
            android:textColor="#333333"
            android:textSize="14sp"/>
    </RelativeLayout>

    <RelativeLayout
        android:id="@+id/show_all_history_or_cancel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <TextView
            android:id="@+id/show_all_history"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginLeft="10dp"
            android:paddingBottom="10dp"
            android:paddingTop="10dp"
            android:text="全部搜索记录"
            android:textColor="#888888"
            android:textSize="14sp"/>

        <TextView
            android:id="@+id/cancel_history"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_gravity="center"
            android:layout_marginRight="10dp"
            android:paddingBottom="10dp"
            android:paddingTop="10dp"
            android:text="关闭"
            android:textColor="#888888"
            android:textSize="14sp"/>

    </RelativeLayout>

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="#666666"
        />
</LinearLayout>

展示搜索历史的Listview的Adapter:OptionsAdapter_3

package com.chen.customviewdemo.adapter;

import android.app.Activity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;

import com.chen.customviewdemo.R;
import com.chen.customviewdemo.listener.HandleListviewItemListener;
import com.chen.customviewdemo.utils.CHEN;

import java.util.ArrayList;

/**
 * 适配器
 */
public class OptionsAdapter_3 extends BaseAdapter {
    private ArrayList<String> list = new ArrayList<String>();
    private Activity activity = null;

    private HandleListviewItemListener handleListviewItemListener;

    private int count = 0;

    /**
     * 自定义构造方法
     *
     * @param activity
     * @param list
     */
    public OptionsAdapter_3(Activity activity, HandleListviewItemListener listener, ArrayList<String> list) {
        this.activity = activity;
        this.list = list;
        handleListviewItemListener = listener;
    }

    @Override
    public int getCount() {
        count = CHEN.showAllSearchHistory ? 2 : list.size();
        return count;
        //        return list.size();
    }

    @Override
    public Object getItem(int position) {
        return list.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    class ViewHolder {
        TextView textView;
        ImageView imageView;
        TextView show_all_history;
        TextView cancel_history;
        RelativeLayout show_all_history_or_cancel;
    }

    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        ViewHolder holder = null;
        if (convertView == null) {
            holder = new ViewHolder();
            //下拉项布局
            convertView = LayoutInflater.from(activity).inflate(R.layout.option_item_3, null);
            holder.textView = (TextView) convertView.findViewById(R.id.item_text);
            holder.imageView = (ImageView) convertView.findViewById(R.id.delImage);
            holder.show_all_history = (TextView) convertView.findViewById(R.id.show_all_history);
            holder.cancel_history = (TextView) convertView.findViewById(R.id.cancel_history);
            holder.show_all_history_or_cancel = (RelativeLayout) convertView.findViewById(R.id.show_all_history_or_cancel);

            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }

        if (position == count - 1) {
            holder.show_all_history_or_cancel.setVisibility(View.VISIBLE);
            if (CHEN.showAllSearchHistory) {
                //说明有2个以上的搜索历史记录
                holder.show_all_history.setVisibility(View.VISIBLE);
            } else {
                holder.show_all_history.setVisibility(View.GONE);
            }
        }else{
            holder.show_all_history_or_cancel.setVisibility(View.GONE);
        }

        /**
         * 显示全部搜索记录
         */
        holder.show_all_history.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                handleListviewItemListener.handleListviewItem("showAllSearchHistory", -1);
            }
        });

        /**
         * 关闭搜索记录提示
         */
        holder.cancel_history.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                handleListviewItemListener.handleListviewItem("cancelSearchHistoryTip", -1);
            }
        });

        holder.textView.setText(list.get(position));

        //为下拉框选项文字部分设置事件,最终效果是点击将其文字填充到文本框
        holder.textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                handleListviewItemListener.handleListviewItem("setSelestData", position);
            }
        });

        //为下拉框选项删除图标部分设置事件,最终效果是点击将该选项删除
        holder.imageView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                handleListviewItemListener.handleListviewItem("delSelestData", position);
            }
        });
        return convertView;
    }
}

Activity的布局:activity_main_10

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="fill_parent"
                android:layout_height="fill_parent"
                android:background="#EEEED1"
    >

    <EditText
        android:id="@+id/edittext"
        android:layout_width="200dp"
        android:layout_height="40dp"
        android:layout_marginLeft="30dp"
        android:layout_marginTop="50dp"
        android:background="#dedede"
        android:paddingLeft="3dp"
        android:singleLine="true"/>


    <TextView
        android:id="@+id/save"
        android:layout_width="wrap_content"
        android:layout_height="45dp"
        android:layout_below="@id/edittext"
        android:layout_marginLeft="30dp"
        android:layout_marginTop="30dp"
        android:text="保存"
        android:textColor="#000000"
        android:textSize="20sp"/>

    <TextView
        android:id="@+id/show"
        android:layout_width="wrap_content"
        android:layout_height="45dp"
        android:layout_below="@id/save"
        android:layout_marginLeft="30dp"
        android:layout_marginTop="30dp"
        android:text="展示"
        android:textColor="#000000"
        android:textSize="20sp"/>

    <TextView
        android:id="@+id/clear"
        android:layout_width="wrap_content"
        android:layout_height="45dp"
        android:layout_below="@id/show"
        android:layout_marginLeft="30dp"
        android:layout_marginTop="30dp"
        android:text="清空"
        android:textColor="#000000"
        android:textSize="20sp"/>


    <ListView
        android:id="@+id/search_history_lv"
        android:layout_width="200dp"
        android:layout_height="wrap_content"
        android:layout_below="@id/edittext"
        android:layout_marginLeft="30dp"
        android:background="#ffffff"
        android:visibility="gone"/>

</RelativeLayout>

Activity源码:MainActivity_10

package com.chen.customviewdemo;

import android.app.Activity;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;

import com.chen.customviewdemo.adapter.OptionsAdapter_3;
import com.chen.customviewdemo.listener.HandleListviewItemListener;
import com.chen.customviewdemo.utils.CHEN;
import com.chen.customviewdemo.utils.SharedPreferencesUtil;
import com.chen.customviewdemo.utils.Utils;

import java.util.ArrayList;

/**
 * 搜索历史有2条以上时,默认显示2条,这个时候,也可以点击删除
 */
public class MainActivity_10 extends Activity implements View.OnClickListener, HandleListviewItemListener {

    //下拉框选项数据源
    private ArrayList<String> datas;
    //文本框
    private EditText et;
    //保存输入过的数据
    private TextView save_input_data;
    //用于展示数据的
    private TextView show_pop;
    //清理数据
    private TextView clear;
    //保存搜索数据
    SharedPreferencesUtil spUtil;
    //展示搜索历史的listview
    ListView search_history_lv;
    //适配器
    private OptionsAdapter_3 adapter;

    /**
     * 临时存放数据的
     */
    //    private ArrayList<String> tempDatas;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main_10);

        spUtil = new SharedPreferencesUtil(MainActivity_10.this);
        datas = new ArrayList<String>();

        search_history_lv = (ListView) findViewById(R.id.search_history_lv);
        adapter = new OptionsAdapter_3(this, this, datas);
        search_history_lv.setAdapter(adapter);

        //初始化界面组件
        et = (EditText) findViewById(R.id.edittext);

        clear = (TextView) findViewById(R.id.clear);
        clear.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                datas.clear();
                spUtil.saveSearchKeyHistory("");
            }
        });

        save_input_data = (TextView) findViewById(R.id.save);
        //设置点击事件,恢复下拉框列表数据,没有什么作用,纯粹是为了方便多看几次效果而设置
        save_input_data.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                //编辑框输入的数据
                String willSave = et.getText().toString().trim();
                if (!TextUtils.isEmpty(willSave)) {
                    String saveData = spUtil.getSearchKeyHistory();

                    if (TextUtils.isEmpty(saveData)) {
                        //说明之前没有搜索历史,直接存放当前的搜索数据
                        spUtil.saveSearchKeyHistory(willSave);
                    } else {
                        if (!Utils.isContainsStr(saveData, willSave)) {
                            //已有数据中,不包含新的搜索内容,把最新搜索内容加到最前面
                            saveData = willSave + "-" + saveData;
                        } else {
                            //已有数据中,包含新的搜索内容,要重新排序,把最新的搜索内容,放在最前面
                            String[] oldOrder = saveData.split("-");
                            int equaseIndex = 0;
                            for (int i = 0; i < oldOrder.length; i++) {
                                if (oldOrder[i].equals(willSave)) {
                                    equaseIndex = i;
                                    break;
                                }
                            }
                            if (equaseIndex != 0) {
                                //新的搜索内容,不在保存数据的第一条。(如果在第一条,就不用处理了)
                                saveData = oldOrder[equaseIndex];
                                for (int j = 0; j < equaseIndex; j++) {
                                    saveData = saveData + "-" + oldOrder[j];
                                }
                                for (int k = equaseIndex + 1; k < oldOrder.length; k++) {
                                    saveData = saveData + "-" + oldOrder[k];
                                }
                            }

                        }
                        //项目中,输入框默认的一次最多输入数据是20个字。5条数据是100字。这里再多加点。这里正确的处理,应该是保证只保存5条。这里偷个懒,不在详细写了。
                        if (saveData.length() > 150) {
                            saveData = saveData.substring(0, 150);
                        }
                        spUtil.saveSearchKeyHistory(saveData);
                    }
                }
            }
        });

        show_pop = (TextView) findViewById(R.id.show);
        show_pop.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                showSearchHistory();
            }
        });

        et.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e("chen", "onClick---");
                if (TextUtils.isEmpty(et.getText().toString().trim())) {
                    showSearchHistory();
                }
            }
        });

    }

    /**
     * 显示搜索历史
     */
    public void showSearchHistory() {

        datas.clear();
        String data = spUtil.getSearchKeyHistory();
        if (!TextUtils.isEmpty(data)) {
            if (data.contains("-")) {
                String[] dList = data.split("-");
                int length = dList.length > 5 ? 5 : dList.length;
                for (int i = 0; i < length; i++) {
                    if (!TextUtils.isEmpty(dList[i])) {
                        datas.add(dList[i]);
                    }
                }
            } else {
                datas.add(data);
            }
        }

        if (datas != null && datas.size() > 0) {
            if (datas.size() > 2) {
                CHEN.showAllSearchHistory = true;
            }else{
                CHEN.showAllSearchHistory=false;
            }
            adapter.notifyDataSetChanged();
            search_history_lv.setVisibility(View.VISIBLE);
        }

    }

    @Override
    public void onClick(View v) {
    }

    @Override
    public void handleListviewItem(String flag, int index) {

        //给Edittext设置选中的内容
        if ("setSelestData".equals(flag)) {
            et.setText(datas.get(index));
            search_history_lv.setVisibility(View.GONE);

        }
        //删除选择条目
        if ("delSelestData".equals(flag)) {
            datas.remove(index);

            if(CHEN.showAllSearchHistory){
                //如果当前是只显示前2条,就不用再判断了。
                if (datas.size() > 2) {
                    CHEN.showAllSearchHistory = true;
                } else {
                    CHEN.showAllSearchHistory = false;
                }
            }

            //刷新下拉列表
            adapter.notifyDataSetChanged();

            if (datas == null || datas.size() == 0) {
                spUtil.saveSearchKeyHistory("");
            } else {
                if (datas.size() == 1) {
                    spUtil.saveSearchKeyHistory(datas.get(0));
                } else {
                    String temp = "";
                    for (int j = 0; j < datas.size(); j++) {
                        temp = temp + datas.get(j) + "-";
                    }
                    if (temp.endsWith("-")) {
                        temp = temp.substring(0, temp.length() - 1);
                    }
                    spUtil.saveSearchKeyHistory(temp);
                }
            }

        }

        //展示所有搜索历史
        if ("showAllSearchHistory".equals(flag)) {
            CHEN.showAllSearchHistory = false;
            adapter.notifyDataSetChanged();
        }

        //关闭展示所有搜索历史
        if ("cancelSearchHistoryTip".equals(flag)) {
            CHEN.showAllSearchHistory = false;
            search_history_lv.setVisibility(View.GONE);
        }
    }
}

用到的工具类

package com.chen.customviewdemo.utils;

import android.app.Activity;
import android.text.TextUtils;
import android.util.DisplayMetrics;

/**
 * 工具类
 */
public class Utils {

    /**
     * 判断一个大的string里面,是否包含另一个小的string
     *
     * @param mianStr 大的string(主string)
     * @param data    要判断的string
     * @return true表示包含,false表示不包含
     */
    public static boolean isContainsStr(String mianStr, String data) {

        try {
            if (checkStringIsEmpty(mianStr) || checkStringIsEmpty(data)) {
                return false;
            } else {
                if (mianStr.contains("-")) {
                    boolean b = false;
                    String[] arr = mianStr.split("-");

                    for (int i = 0; i < arr.length; i++) {
                        if (data.equals(arr[i])) {
                            b = true;
                        }
                    }
                    return b;
                } else {
                    if (mianStr.equals(data)) {
                        return true;
                    } else {
                        return false;
                    }
                }
            }
        } catch (Exception e) {
            return false;
        }
    }
}

全局变量

package com.chen.customviewdemo.utils;

/**
 * 用于保存全局变量值
 */
public class CHEN {
    /**
     * 有2条以上的搜索历史记录
     */
    public static boolean showAllSearchHistory = false;
}

保存数据

package com.chen.customviewdemo.utils;

import android.content.Context;
import android.content.SharedPreferences;

public class SharedPreferencesUtil {
    private SharedPreferences sp;
    private Context context;

    public SharedPreferencesUtil(Context context) {
        this.context = context;
    }

    public void saveSearchKeyHistory(String value) {
        sp = context.getSharedPreferences("SaveSearchKeyHistory", Context.MODE_PRIVATE);
        sp.edit().putString("searchKeyHistory", value).apply();
    }

    public String getSearchKeyHistory() {
        sp = context.getSharedPreferences("SaveSearchKeyHistory", Context.MODE_PRIVATE);
        return sp.getString("searchKeyHistory", "");
    }

}

回调接口

package com.chen.customviewdemo.listener;

/**
 * 处理listview中某个条目的监听接口
 */
public interface HandleListviewItemListener {

    void handleListviewItem(String flag, int index);
}
作者:u014620028 发表于2016/8/17 17:28:36 原文链接
阅读:30 评论:0 查看评论

AndroidStudio Git的使用(主要是解决文件冲突)

$
0
0

开始:

1.使用github 来测试。首先准备一个GitHub账号吧
2.在androidStudio 里面新建一个项目 这里我取名TestGit



然后我们上传到GitHub中,



按上图示选择 ,然后出现


点击Share,然后勾选你想要提交的文件


然后点击OK  , 

注意了,只个步骤过程中 会提示让你输入你的GitHub账号和密码,因为我这里之前已经输入过一次了这次没提醒我输入,所以我点击ok以后直接就在GitHub上创建了一个项目



这个项目就是我们刚才创建的,然后成功上传到GitHub中了。现在第一阶段就完成了。


代码提交更新

 现在我们的代码和GitHub 后面简称服务器吧,毕竟我们的私人代码大多时候都不会放到GitHub上,毕竟私有的要收费。  现在我们本地的代码就和服务器上的代码一致了

需求一:新增文件或者文件修改后提交操作


现在我们 修改MainActivity 然后提交到服务器



可以看见我们新增了一行注释   //本地修改MainActiviy 提交到服务器
我们先来看看服务器上的代码现在是怎样的吧,应该是没有这一行的对吧



然后现在我们提交代码到服务器 ,选择你要提交的文件右键选择






然后会有几个弹框,可以不用理会 我觉得,,最后Push , 提交成功以后 我们可以在服务器上去看,服务器上有没有我们刚才更新的代码


可以看见 ,服务器上已经出现了我们刚才添加的注释代码。现在提交操作也可以正常进行了

需求二:更新代码

这个最简单了,
直接点击 

然后就行了。可是这样会遇到代码冲突的情况,下面再说

解决冲突

场景一:服务端修改了MainActivity文件 我本地也修改了MainActivity文件 这个时候我更新代码就会出现一点问题

1, 我们先在服务端修改一点内容


然后提交修改

然后我们本地也修改相同的文件


现在我们2端都修改了文件,现在我们点击更新(刚才前面提到的按钮)



选项安装图中选择
然后点击ok,会提示你有文件冲突


然后点击Merge



然后点击Apply 然后就ok了,这个时候我们合并后的代码还没有提交服务端,现在我们在提交代码,就不会有冲突了。,然后提交完成后我们在服务端再去看看代码





好啦,现在就正常啦,真是高兴呢,

当你上传代码遇到冲突也是一样的解决方法 这里就不贴了。提交如果遇到冲突也是会提示你有文件冲突,然后你在合并之后再提交push 就行了。

完结

刚开始没用过的东西总是感觉很难用,慢慢熟悉一下就好了,学会带着问题解决问题。




作者:xiaxiayige 发表于2016/8/17 17:35:15 原文链接
阅读:41 评论:0 查看评论

App安全(一) Andoid防止升级过程被劫持和换包

$
0
0

文/ Tamic
地址/ http://blog.csdn.net/sk719887916?viewmode=list

前言

APP 安全一直是开发者头痛的事情,越来越多的安全漏洞,使得开发者

越来越重视app安全,目前app安全主要有由以下几部分

APP组件安全

Android 包括四大组件:Activitie、Service、Content Provider、Broadband Receiver,
它们每一个都可以通过外面隐式的Intent方式打开,
android组件对外开放 就会被其他程序劫持,因此必须在manifest里面声明
exported为false,禁止其他程序访问改组件,
对于要和外部交互的组件,应当添加一下访问权限的控制, 在有权限后外部程序才可以开启,
还需要要对传递的数据进行安全的校验。不符合规则的一律不处理。

Webview 安全漏洞

Android API 4.4以前,谷歌的webview存在安全漏洞,网站可以通过js注入就可以随便拿到客户端的重要信息,
甚至轻而易举的调用本地代码进行流氓行为,谷歌后来发现有此漏洞后
,在API 4.4以后增加了防御措施,如果用js调用本地代码,开发者必须在代码申明JavascriptInterface,
列如在4.0之前我们要使得webView加载js只需如下代码:

mWebView.addJavascriptInterface(new JsToJava(), “myjsfunction”);

4.4之后使用需要在调用Java方法加入@JavascriptInterface注解,
如果代码无此申明,那么也就无法使得js生效,也就是说这样就可以避免恶意网页利用js对客户端的进行窃取和攻击。

APP反编译

app被反编后,源码暴露,不仅对数据造成隐私,而且对一些接口造成攻击的潜在风险。
我们必须对apk进行源码混淆,也可以进行apk加固

APP二次打包

即反编译后重新加入恶意的代码逻辑,或置入新病毒重新生成一个新APK文件。
二次的目的一般都是是盈利广告和病毒结合,对正版apk进行解包,插入恶意病毒后重新打包并发布,
因此伪装性很强。截住app重打包就一定程度上防止了病毒的传播。因此app加固是防止二次打包的重要措施。

APP进程劫持

一般我们称为进程注入,也就动态注入技术,hook技术目前主流的进程注入方式,通过对linux进行so注入,达到挂钩远程函数实现监控远程进程的目的。

APP DNS劫持

DNS劫持俗称抓包。通过对url的二次劫持,修改参数和返回值,进行对app的web数据伪装,实现注入广告和假数据,甚至导流用户的作用,严重的可以
通过对登录APi的劫持可以获取用户密码,也可以对app升级做劫持,下载病毒apk等目的,解决方法一般用https进行传输数据。

APP APi接口验签

一般服务端的接口也会被攻击,虽然是服务端安全问题,但还是属于App系统维护体系,如果app后端挂了,app也不叫app了,一般会被
恶意程序频繁请求,导致订单等重复注入,严重的甚至拖垮app.

今天先看下APP升级过程被劫持的问题

我们做app版本神级时一般流程是采用请求升级接口,如果有升级,服务端返回下一个下载地址,下载好Apk后,再点击安装。
其实这个过程中有三个地方会被劫持。 请求升级时,下载文件时,安装时。

  • 升级APi

升级Api建议用https,防止被恶意程序劫持,结果是恶意返回下载地址,这样就把伪装apk下载到本地,结果你应该懂的

  • 下载API:

    那如果升级api你做了加固,下载api没做加过,还是徒劳,恶意程序也可以给你返回恶意文件或者apk,直到被你错误的安装在
    手机上。

    • 安装过程;

    加入你的以上两个过程都做了加固,但是安装时候,本地文件path被错误修改了,仍然可以安装错误apk,这不仅
    会对用户体验产生不利,甚至威胁手机安全。

解决办法:

  • 升级api加入https 这个肯定不用再过多介绍
  • 下载Api也加入https不用介绍,这里指的提出的是你需要服务端吧的文件进行文件Hash值校验,防止文件被篡改,
    通过对文件hash值,还要对服务端的校验验签,防止不是自己服务器返回的文件

  • 安装过程也必须对Apk文件进行包名和签名验证,防止Apk被恶意植入木马

假设我的升级bean为;

public class UpgradeModel {

private int code;
private String msg;

private DataBean data;

public int getCode() {
    return code;
}

public void setCode(int code) {
    this.code = code;
}

public String getMsg() {
    return msg;
}

public void setMsg(String msg) {
    this.msg = msg;
}

public DataBean getData() {
    return data;
}

public void setData(DataBean data) {
    this.data = data;
}

public static class DataBean {
    private String description;
    private String downUrl;
    private String version;
    private String hashcod;
    private String key;
    private String isForce;

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getDownUrl() {
        return downUrl;
    }

    public void setDownUrl(String downUrl) {
        this.downUrl = downUrl;
    }

    public String getVersion() {
        return version;
    }

    public void setVersion(String version) {
        this.version = version;
    }

    public String getHashcod() {
        return hashcod;
    }

    public void setHashcod(String hashcod) {
        this.hashcod = hashcod;
    }

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    public String getIsForce() {
        return isForce;
    }

    public void setIsForce(String isForce) {
        this.isForce = isForce;
    }
}

}

通过一次请求到服务端数据后,如果有版本更新 我们应该先验证
Url和key是不是我们和服务端协商好的Url和key

UpgradeModel aResult = xxxx//

if (aResult != null && aResult.getData() != null ) {

        String url = aResult.getData().getDownUrl();


        if (url == null || !TextUtils.equals(url, "这里是你知道的现在地址: 放松的看hostUrl就行")) {

          // r如果符合,就不去下载

        }

接着我们发现是你自己app的服务器地址,请求下载Api用DownLoadModel接受请求头 ,下载好apk后,继续判断文件的hash和升级api返回的hashcode,加之key是否是现在服务器返回的key,如果不一致,就不安装

      File file = DownUtils.getFile(url);
            // 监测是否要重新下载
     if (file.exists() && TextUtils.equals(aResult.getData().getHashCode(), EncryptUtils.Md5File(file))) {
      && TextUtils.equals(aResult.getData().getKey(), DownLoadModel.getData()..getKey())

      // 如果符合,就去安装 不符合重新下载 删除恶意文件


            }

等我们下载文件是我们服务器提供的,验证没问题就只剩安装了。接着还要对apk文件进行包名和签名校验,

/** installApK
 * @param context
 * @param path
 * @param name
 */
public static void installApK(Context context, final String path, final String name ) {

    if (!SafetyUtils.checkFile(path + name, context)) {
        return;
    }

    if (!SafetyUtils.checkPagakgeName(context, path + name)) {
        Toast.makeText(context, "升级包被恶意软件篡改 请重新升级下载安装", Toast.LENGTH_SHORT ).show();
        DLUtils.deleteFile(path + name);
        ((Activity)context).finish();
        return;
    }

    switch (SafetyUtils.checkPagakgeSign(context, path + name)) {

        case SafetyUtils.SUCCESS:
            DLUtils.openFile(path + name, context);
            break;

        case SafetyUtils.SIGNATURES_INVALIDATE:

            Toast.makeText(context, "升级包安全校验失败 请重新升级", Toast.LENGTH_SHORT ).show();
            ((Activity)context).finish();

            break;

        case SafetyUtils.VERIFY_SIGNATURES_FAIL:

            Toast.makeText(context, "升级包为盗版应用 请重新升级", Toast.LENGTH_SHORT ).show();
            ((Activity)context).finish();
            break;

        default:
            break;
    }

}

SafetyUtils安全类如下:

/**
* 安全校验
* Created by LIUYONGKUI726 on 2016-04-21.
*/
public class SafetyUtils {

/** install sucess */
protected static final int SUCCESS = 0;
/** SIGNATURES_INVALIDATE */
protected static final int SIGNATURES_INVALIDATE = 3;
/** SIGNATURES_NOT_SAME */
protected static final int VERIFY_SIGNATURES_FAIL = 4;
/** is needcheck */
private static final boolean NEED_VERIFY_CERT = true;

/**
 * checkPagakgeSigns.
 */
public static int checkPagakgeSign(Context context, String srcPluginFile) {

    PackageInfo PackageInfo = context.getPackageManager().getPackageArchiveInfo(srcPluginFile, 0);
    //Signature[] pluginSignatures = PackageInfo.signatures;
    Signature[] pluginSignatures = PackageVerifyer.collectCertificates(srcPluginFile, false);
    boolean isDebugable = (0 != (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE));
    if (pluginSignatures == null) {
        PaLog.e("签名验证失败", srcPluginFile);
        new File(srcPluginFile).delete();
        return SIGNATURES_INVALIDATE;
    } else if (NEED_VERIFY_CERT && !isDebugable) {
        //可选步骤,验证APK证书是否和现在程序证书相同。
        Signature[] mainSignatures = null;
        try {
            PackageInfo pkgInfo = context.getPackageManager().getPackageInfo(
                    context.getPackageName(), PackageManager.GET_SIGNATURES);
            mainSignatures = pkgInfo.signatures;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        if (!PackageVerifyer.isSignaturesSame(mainSignatures, pluginSignatures)) {
            PaLog.e("升级包证书和旧版本证书不一致", srcPluginFile);
            new File(srcPluginFile).delete();
            return VERIFY_SIGNATURES_FAIL;
        }
    }
    return SUCCESS;
}

/**
 * checkPagakgeName
 * @param context
 * @param srcNewFile
 * @return
 */
public static boolean checkPagakgeName (Context context, String srcNewFile) {
    PackageInfo packageInfo = context.getPackageManager().getPackageArchiveInfo(srcNewFile, PackageManager.GET_ACTIVITIES);

    if (packageInfo != null) {

       return TextUtils.equals(context.getPackageName(), packageInfo.packageName);
    }

    return false;
}

/**
 * checkFile
 *
 * @param aPath
 *            文件路径
 * @param context
 *            context
 */
public static boolean checkFile(String aPath, Context context) {
    File aFile = new File(aPath);
    if (aFile == null || !aFile.exists()) {
        Toast.makeText(context, "安装包已被恶意软件删除", Toast.LENGTH_SHORT).show();
        return false;
    }
    if (context == null) {
        Toast.makeText(context, "安装包异常", Toast.LENGTH_SHORT).show();
        return false;
    }

    return true;
}

}

后续

这样我们的对升级流程的安全以及做到很安全细微了,很难被恶意程序轻易劫持,其他js注入,hook注入下期接着说;

作者:sk719887916 发表于2016/8/17 17:46:21 原文链接
阅读:26 评论:0 查看评论

Android简易实战教程--第十八话《ListView显示,简单的适配器SimpleAdapter》

$
0
0

本篇介绍Listview的显示,对于listview有许多的适配器,如ArrayAdapter,BaseAdapter,SimpleAdapter等等。本篇先热身一下,介绍最简单的SimpleAdapter适配器。

对于安卓界面的显示。

首先在主界面布局文件main.xml加入如下代码:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" >

    <ListView
        android:id="@+id/lv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />

</RelativeLayout>

只有一个显示数据的组件:ListView。

然后,给ListView的Item定义一个子布局文件。它代表,listview的列表每个条目的布局item_listview.xml。如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal" >
    
    <ImageView 
        android:id="@+id/iv_photo"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:src="@drawable/photo3"
        />
    <TextView 
        android:id="@+id/tv_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="22sp"
        android:layout_gravity="center_vertical"
        />

</LinearLayout>
好了,现在就在MainActivity中加入数据显示的代码吧:

package com.itydl.arraysimple;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.SimpleAdapter;

public class MainActivity extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		//获取listview对象
		ListView lv = (ListView) findViewById(R.id.lv);
		
		//集合中每个元素都包含ListView条目需要的所有数据,该案例中每个条目需要一个字符串和一个整型,所以使用一个map来封装这两种数据
		List<Map<String, Object>> data = new ArrayList<Map<String,Object>>();
		//定义三条map信息
		Map<String, Object> map1 = new HashMap<String, Object>();
		map1.put("photo", R.drawable.photo1);
		map1.put("name", "小志的儿子");
		data.add(map1);
		
		Map<String, Object> map2 = new HashMap<String, Object>();
		map2.put("photo", R.drawable.photo2);
		map2.put("name", "小志");
		data.add(map2);
		
		Map<String, Object> map3 = new HashMap<String, Object>();
		map3.put("photo", R.drawable.photo3);
		map3.put("name", "赵帅哥");
		data.add(map3);
		
		lv.setAdapter(new SimpleAdapter(this, data, R.layout.item_listview, 
				new String[]{"photo", "name"}, new int[]{R.id.iv_photo, R.id.tv_name}));//这里注意from和to两个位置要对应
		//就是制定键和值,在布局文件中哪的子节点中显示。不要搞错和搞反了。
	}


}

其中注意一点:就是new SimpleAdapter(this, data, R.layout.item_listview,
                new String[]{"photo", "name"}, new int[]{R.id.iv_photo, R.id.tv_name})

参数含义:上下文,数据源,item的布局文件id,from,to。其中from是一个数组,里面的键,与map的键相同;to也是个数组,表示ite显示的组件id,注意要与from的顺序一致,不然会报错。

好了,现在运行程序,结果如下:


作者:qq_32059827 发表于2016/8/17 18:18:48 原文链接
阅读:39 评论:0 查看评论

Zygote进程

$
0
0

背景
从字面上看,zygote是受精卵的意思,它的主要工作就是进行细胞分裂。

在Android中,zygote的行为就如同受精卵一样,在系统发出请求时,负责分裂出其它的进程。

Android为什么要这么设计呢?

要知道从受精卵分裂出来的细胞,将继承受精卵的DNA。这些细胞只需要按照规则复制DNA就行了,而不需要重新思考DNA应该怎么排序。

大概也可以按照这个思路来理解zygote进程吧。

zygote进程启动时,将在内部启动Dalvik虚拟机,注册JNI函数,继而加载一些必要的系统资源和必要类等,然后进入到监听状态。

在后续的运作中,当其它系统模块希望创建新进程时,只需向zygote进程发出请求。zygote进程监听到该请求后,会相应地“分裂”出新的进程。

于是新创建出的进程,从一开始就具有了自己的Dalvik虚拟机以及一些必要的系统资源。这将减少每个进程启动消耗的时间。进一步来说,由于fork的copy-on-write策略,zygote的这种分裂方式还有可能减少系统整体内存的占用。

版本
android 6.0

主要流程分析
在分析init进程时,我们知道init进程会解析init.rc文件,然后加载对应的进程。zygote就是以这种方式,被init进程加载的。

在system/core/rootdir/init.rc中,可以看到:

import /init.${ro.zygote}.rc

从import的文件命名方式,我们可以看出在不同的平台(32、64及64_32)上,init.rc将包含不同的zygote.rc文件。不同的zygote.rc内容大致相同,主要区别体现在启动的是32位,还是64位的进程。

service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
    class main
    socket zygote stream 660 root system
    onrestart write /sys/android_power/request_state wake
    onrestart write /sys/power/state on
    onrestart restart media
    onrestart restart netd
    writepid /dev/cpuset/foreground/tasks

以上是init.zygote32.rc中的内容。

从init.zygote32.rc可以看出,zygote实际上对应于app_process进程,对应源文件为frameworks/base/cmds/app_process/app_main.cpp,其main函数对应的传入参数为:–zygote, –start-system-server 。

这里需要补充说明的是:
1、在init.zygote64.rc中,启动的进程为app_process64;在init.zyote64_32.rc中,会启动两个进程,分别为app_process64和app_process32(目前,不太清楚这种设置的实际含义)。
2、从zyote的rc文件,我们可以看到zygote启动的时候,创建了一个socket(后文将介绍这个socket)。

接下来我们按照zygote进程启动涉及文件的先后顺序,一起来看看zygote的主要工作情况。

1、app_main.cpp
首先从入口文件app_main.cpp的main函数开始分析。

int main(int argc, char* const argv[])
{
    //AppRuntime定义于app_main.cpp中,继承自AndroidRuntime
    //个人感觉该对象就是对Android运行时环境的一种抽象
    AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
    ..........
    //开始解析输入参数
    while (i < argc) {
        const char* arg = argv[i++];
        if (strcmp(arg, "--zygote") == 0) {
            //init.zygote.rc中定义了该字段
            zygote = true;
            //记录app_process进程名的nice name,即zygote
            niceName = ZYGOTE_NICE_NAME;
        } else if (strcmp(arg, "--start-system-server") == 0) {
            //init.zygote.rc中定义了该字段
            startSystemServer = true;
        } else if (strcmp(arg, "--application") == 0) {
            application = true;
        } else if (strncmp(arg, "--nice-name=", 12) == 0) {
            niceName.setTo(arg + 12);
        } else if (strncmp(arg, "--", 2) != 0) {
            className.setTo(arg);
            break;
        } else {
            --i;
            break;
        }
    }
    ..........
    //准备下一个函数调用所需的参数
    Vector<String8> args;
    if (!className.isEmpty()) {
        //启动zygote时,class name is empty,不进入该分支
        ........
    } else {
        //创建dalvikCache所需的目录,并定义权限
        maybeCreateDalvikCache();

        if (startSystemServer) {
            //增加参数
            args.add(String8("start-system-server"));
        }
        .........
        for (; i < argc; ++i) {
            //将main函数未处理的参数都递交给下个调用函数处理
            args.add(String8(argv[i]));
        }
    }

    if (!niceName.isEmpty()) {
        //将app_process的进程名,替换为zygote
        runtime.setArgv0(niceName.string());
        set_process_name(niceName.string());
    }

    if (zygote) {
        //调用Runtime的start函数
        runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
    } else if (className) {
        //启动zygote没有进入这个分支,但这个分支说明,通过配置init.rc文件,其实是可以不通过zygote来启动一个进程
        runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
    } else {
        ...............
    }
}

由于AppRuntime继承自AndroidRuntime,且没有重写start方法,因此zygote的流程进入到了AndroidRuntime.cpp。

2、AndroidRuntime.cpp
我们一起来看看在AndroidRuntime的start函数中,进行了哪些重要操作:

void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote) {
    .........
    JniInvocation jni_invocation;
    jni_invocation.Init(NULL);
    JNIEnv* env;
    //创建虚拟机,其中大多数参数由系统属性决定
    //最终,startVm利用JNI_CreateVM创建出虚拟机
    if (startVm(&mJavaVM, &env, zygote) != 0) {
        return;
    }
    ...........
    //注册JNI函数
    if (startReg(env) < 0) {
        ALOGE("Unable to register all android natives\n");
        return;
    }
    ......

==============================以下非主干部分============================

我们先来看看startReg具体是在干什么。

int AndroidRuntime::startReg(JNIEnv* env) {
    ..........
    if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
        env->PopLocalFrame(NULL);
        return -1;
    }
    ........
}

从上述代码可以看出,startReg函数中主要是通过register_jni_procs来注册JNI函数。

其中,gRegJNI是一个全局数组,该数组的定义类似于:

static const RegJNIRec gRegJNI[] = {
    REG_JNI(register_android_util_SeempLog),
    REG_JNI(register_com_android_internal_os_RuntimeInit),
    REG_JNI(register_android_os_SystemClock),
    REG_JNI(register_android_util_EventLog),
    ............

其中,REG_JNI的宏定义及RegJNIRec结构体的定义为:

#ifdef NDEBUG
    #define REG_JNI(name)      { name }
    struct RegJNIRec {
        int (*mProc)(JNIEnv*);
    };
#else
    #define REG_JNI(name)      { name, #name }
    struct RegJNIRec {
        int (*mProc)(JNIEnv*);
        const char* mName;
    };
#endif

根据宏定义可以看出,宏REG_JNI将得到函数名;定义RegJNIRec数组时,函数名被赋值给RegJNIRec结构体,于是每个函数名被强行转换为函数指针。

明白宏及数组的定义后,我们再回头来看register_jni_procs的定义:

static int register_jni_procs(const RegJNIRec array[], size_t count, JNIEnv* env)
{
    for (size_t i = 0; i < count; i++) {
        if (array[i].mProc(env) < 0) {
#ifndef NDEBUG
            ALOGD("----------!!! %s failed to load\n", array[i].mName);
#endif
            return -1;
        }
    }
    return 0;
}

结合前面的分析,容易知道register_jni_procs函数,实际上就是调用函数指针对应的函数,以进行实际的JNI函数注册。

我们随意举个例子,看看register_android_util_SeempLog被调用时的情况:

/*
 * JNI registration.
 */
static JNINativeMethod gMethods[] = {
    /* name, signature, funcPtr */
    { "seemp_println_native",  "(ILjava/lang/String;)I",
            (void*) android_util_SeempLog_println_native },
};

int register_android_util_SeempLog(JNIEnv* env)
{
    jclass clazz = env->FindClass("android/util/SeempLog");
    if (clazz == NULL) {
        return -1;
    }

    return AndroidRuntime::registerNativeMethods(env, "android/util/SeempLog", gMethods, NELEM(gMethods));
}

可以看到,这实际上是自己定义JNI函数并进行动态注册的标准写法。

int AndroidRuntime::registerNativeMethods(JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods)
{
    return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}

最后,还是利用jniRegisterNativeMethods进行实际的注册工作。

==============================以上非主干部分============================

在介绍完AndroidRuntime.cpp中注册JNI的工作后,我们将思路拉回到它的start函数。

    ........
    //将"com.android.internal.os.ZygoteInit"替换为"com/android/internal/os/ZygoteInit"
    char* slashClassName = toSlashClassName(className);
    jclass startClass = env->FindClass(slashClassName);
    if (startClass == NULL) {
        ALOGE("JavaVM unable to locate class '%s'\n", slashClassName);
        /* keep going */
    } else {
        //通过反射找到ZygoteInit的main函数
        jmethodID startMeth = env->GetStaticMethodID(startClass, "main", "([Ljava/lang/String;)V");
        if (startMeth == NULL) {
            ALOGE("JavaVM unable to find main() in '%s'\n", className);
            /* keep going */
        } else {
            //调用ZygoteInit的main函数
            env->CallStaticVoidMethod(startClass, startMeth, strArray);
        ........

当调用ZygoteInit.java的main函数后,zygote进程进入了java世界。

其实我们仔细想一想,就会觉得zygote的整个流程实际上是非常符合实际情况的。

在Android中,每个进程都运行在对应的虚拟机上,因此zygote首先就负责创建出虚拟机。

然后,为了反射调用java代码,必须有对应的JNI函数,于是zygote进行了JNI函数的注册。

当一切准备妥当后,zygote进程才进入到了java世界。

3、ZygoteInit.java

 public static void main(String argv[]) {
    try {
        .........
        String socketName = "zygote";
        .........
        //注册server socket
        registerZygoteSocket(socketName);
        .........
        //预加载
        preload();
        .........
        if (startSystemServer) {
            //启动system server
            startSystemServer(abiList, socketName);
        }

        //zygote进程进入无限循环,处理请求
        runSelectLoop(abiList);

        closeServerSocket();
    } catch (MethodAndArgsCaller caller) {
        //通过反射调用新进程函数的地方
        //后续介绍新进程启动时,再介绍
        caller.run();
    } catch (RuntimeException ex) {
        closeServerSocket();
        throw ex;
    }
}

上面是ZygoteInit的main函数的主干部分,接下来我们进一步分析它们的主要工作。

3.1 创建server socket

private static void registerZygoteSocket(String socketName) {
    if (sServerSocket == null) {
        int fileDesc;
        //此处的socket name,就是zygote
        final String fullSocketName = ANDROID_SOCKET_PREFIX + socketName;
        try {
            //记得么?在init.zygote.rc被加载时,就会创建一个名为zygote的socket
            String env = System.getenv(fullSocketName);
            fileDesc = Integer.parseInt(env);
        } catch (RuntimeException ex) {
            throw new RuntimeException(fullSocketName + " unset or invalid", ex);
        }

        try {
            FileDescriptor fd = new FileDescriptor();
            //获取zygote socket的文件描述符
            fd.setInt$(fileDesc);
            //将zygote socket包装成一个server socket
            sServerSocket = new LocalServerSocket(fd);
        } catch (IOException ex) {
            throw new RuntimeException("Error binding to local socket '" + fileDesc + "'", ex);
        }
    }
}

zygote进程与系统中其它进程的通信没有使用Binder,而是采用了基于AF_UNIX类型的socket。(实际上,在这个时候Binder根本无法使用)。

3.2 预加载

static void preload() {
    Log.d(TAG, "begin preload");
    //读取文件framework/base/preloaded-classes,然后通过反射加载对应的类
    //需要加载数千个类,启动慢的原因之一
    preloadClasses();
    //负载加载一些常用的系统资源
    preloadResources();
    //图形相关的
    preloadOpenGL();
    //一些必要库
    preloadSharedLibraries();
    //好像是语言相关的字符信息
    preloadTextResources();
    // Ask the WebViewFactory to do any initialization that must run in the zygote process, for memory sharing purposes.
    WebViewFactory.prepareWebViewInZygote();
    Log.d(TAG, "end preload");
}

为了让系统实际运行时更加流畅,在zygote启动时候,调用preload函数进行了一些预加载操作。

Android 通过zygote fork的方式创建子进程。zygote进程预加载这些类和资源,在fork子进程时,仅需要做一个复制即可。

这样可以节约子进程的启动时间。同时,根据fork的copy-on-write机制可知,有些类如果不做改变,甚至都不用复制,子进程可以和父进程共享这部分数据,从而省去不少内存的占用。

3.3 启动SystemServer进程

private static boolean startSystemServer(String abiList, String socketName) {
    //准备capabilities参数
    ........
    String args[] = {
        "--setuid=1000",
        "--setgid=1000",
        "--setgroups=.........",
        "--capabilities=" + capabilities + "," + capabilities,
        "--nice-name=system_server",
        "--runtime-args",
        "com.android.server.SystemServer",
    };
    ZygoteConnection.Arguments parsedArgs = null;

    int pid;

    try {
        //将上面准备的参数,按照ZygoteConnection的风格进行封装
        parsedArgs = new ZygoteConnection.Arguments(args);
        ...........

        //通过fork"分裂"出system server,具体的过程在介绍system server时再分析
        /* Request to fork the system server process */
        pid = Zygote.forkSystemServer(
            parsedArgs.uid, parsedArgs.gid,
            parsedArgs.gids,
            parsedArgs.debugFlags,
            null,
            parsedArgs.permittedCapabilities,
            parsedArgs.effectiveCapabilities);
    } catch (IllegalArgumentException ex) {
        throw new RuntimeException(ex);
    }

    if (pid == 0) {
        ............
        //pid = 0, 在进程system server中
        //system server进程处理自己的工作
        handleSystemServerProcess(parsedArgs);
    }

    return true;
}

3.4 处理请求信息

创建出SystemServer进程后,zygote进程利用函数runSelectLoop,处理server socket收到的命令。

private static void runSelectLoop(String abiList) throws MethodAndArgsCaller {
    ArrayList<FileDescriptor> fds = new ArrayList<FileDescriptor>();
    ArrayList<ZygoteConnection> peers = new ArrayList<ZygoteConnection>();

    //首先将server socket加入到fds
    fds.add(sServerSocket.getFileDescriptor());
    peers.add(null);

    while (true) {
        StructPollfd[] pollFds = new StructPollfd[fds.size()];
        for (int i = 0; i < pollFds.length; ++i) {
            pollFds[i] = new StructPollfd();
            pollFds[i].fd = fds.get(i);
            //关注事件到来
            pollFds[i].events = (short) POLLIN;
        }
        try {
            //等待事件到来
            Os.poll(pollFds, -1);
        } catch (ErrnoException ex) {
            throw new RuntimeException("poll failed", ex);
        }
        //注意这里是倒序的
        for (int i = pollFds.length - 1; i >= 0; --i) {
            if ((pollFds[i].revents & POLLIN) == 0) {
                continue;
            }
            //server socket最先加入fds, 因此这里是server socket收到数据
            if (i == 0) {
                //收到新的建立通信的请求,建立通信连接
                ZygoteConnection newPeer = acceptCommandPeer(abiList);
                //加入到peers和fds
                peers.add(newPeer);
                fds.add(newPeer.getFileDesciptor());
             } else {
                //其它通信连接收到数据,runOnce执行对应命令
                boolean done = peers.get(i).runOnce();
                if (done) {
                    //对应通信连接不再需要执行其它命令,关闭并移除
                    peers.remove(i);
                    fds.remove(i);
                }
            }
        }
    }
}

从上面代码可知,初始时,fds中仅有server socket,因此当有数据到来时,将执行i等于0的分支。
此时,显然是需要创建新的通信连接,因此acceptCommandPeer将被调用。

private static ZygoteConnection acceptCommandPeer(String abiList) {
    try {
        return new ZygoteConnection(sServerSocket.accept(), abiList);
    } catch (IOException ex) {
        throw new RuntimeException("IOException during accept()", ex);
    }
}

acceptCommandPeer封装了socket的accpet函数。于是我们知道,对应的新的连接,zygote将会创建出一个新的socket与其通信,并将该socket加入到fds中。因此,一旦通信连接建立后,fds中将会包含有多个socket。

当poll监听到这一组sockets上有数据到来时,就会从阻塞中恢复。于是,我们需要判断到底是哪个socket收到了数据。

在runSelectLoop中采用倒序的方式轮询,由于server socket第一个被加入到fds,因此最后轮询到的socket才需要处理新建连接的操作;其它socket收到数据时,仅需要调用zygoteConnection的runonce函数执行数据对应的操作。

若一个连接处理完所有对应消息后,该连接对应的socket和连接等将被移除。

socket编程中,accept()调用主要用在基于连接的套接字类型,比如SOCK_STREAM和SOCK_SEQPACKET。
它提取出所监听套接字的等待连接队列中第一个连接请求,创建一个新的套接字,并返回指向该套接字的文件描述符。
新建立的套接字不在监听状态,原来所监听的套接字的状态也不受accept()调用的影响。

结束语
以上是自己对zygote进程的一些初步分析,我们知道了zygote如何由init进程加载后,一步一步地进入到java世界。由于经验原因,自己的分析难免存在纰漏和不够详细的地方,欢迎大家指正。

作者:Gaugamela 发表于2016/8/20 13:16:45 原文链接
阅读:79 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>