Android数据库(1)、SQLite数据库介绍

一、关系性数据库

关系型数据库主要有以下三个特征,尤为明显,如果没有这个三个特征约束,当多个客户端使用数据的时候就会出现各种各样的错误,所以关系型数据库定义这些约束,让客户端程序只要遵守这个规则便可以规避很多的错误。

关系性数据库

这三个特征主要是:

  • 强类型
  • 引用完整性
  • 事物

1、强类型:

大多数的数据库都是强类型的,要求数据库引擎严格执行,引擎通常定义一些原生的数据类型,它描述可以存进数据库当中数据的具体类型,这些类型通常是各种长度的浮点数,字符串,整数等,特定的实现,这个类型不能让用户自己特定的去扩展,当引擎定义了关系,数据库当中将无法企图把错误的数据类型的数据存入列当中,

例如:尝试插入一个包含一个字符串作为第三个特性的元组到一个指定第三特性应该是一个浮点数的关系当中,通常引擎会报错,将插入失败。

2、引用完整性

在关系型数据库当中,原生类型可以通过声明一个关系中的列到另一个关系的一个引用进行扩展,也就是我们常说的主外建关系,数据库架构师可以声明一个关系中一些列的内容是其他关系中类似类型列的一个引用,下图进行外建的演示:

上述就是最简单的主外建关系,通过表A当中的外建和表B进行连接,从而如果可以在表A当中获取到数据就可以在表B当中进行查询,

想要实现上述的要求,必须遵循数据库引擎提供的引用完整性,因为数据库引擎将会强制的去执行引用完整性约束,该约束主要分为以下的两种类型,

2.1 强制会去执行表B的主键约束,就如上述表B当中的主键。要保证在表B当中主键列当中主键的唯一性,也就是不可以重复,例如当表B当中的主键有A,B,C三个值,现在插入一行数据的主键列为A或B或C将会失败,因为破坏了主键约束的唯一性。

2.2、强调表A的引用完整性,所谓什么是引用完整性呢,就是表A当中的外键要么是null,要么该指就一定在表B的主键当中存在,下面有两种情况会破环引用的完整性,

  • 如果给表A当中插入一行数据,该数据的外键值在表B当中没有,那么该操作则失败。
  • 如果删除表B当中的一条数据,并且该条数据被表A当中的外键所引用,那么该操作失败

上述两种情况也是我们开发当中常常会用到情况,所以我们的解决方式便是,

  • 先插入表B当中的数据,然后插入表A当中的数据
  • 先将表A当中引用表B的外键值置为null,然后删除表B当中数据

3、数据库事务

事务:对数据的一组操作当作为一个单位去执行,要么全部失败,要么全部成功。事物有以下几个特点,常常被开发者侃侃而谈,便是

  • 原子性:全部成功,全部失败,操作被视为一个单位,不能将操作分离
  • 一致性:事务开启之前,数据库处于有效状态,那么事务开启之后,数据库仍然处于有效状态,不能因为事物导致数据约束被破坏
  • 隔离性:执行完事物以后,数据库的状态可以通过依次执行事物中的命令达到同样的状态
  • 持久性:事物一旦完毕,就不能丢失,比如电源故障等,物理环境都不能撼动。

二、SQL语言

什么是SQL语言呢,就是操作数据库以及数据库当中的数据的命令。称之为SQL语言,SQL语言是由数据库引擎解释并且执行的,SQL语言大多数开发者都或多或少的已经掌握,所以该部分内容为回顾内容,笔者并不打算详细讲解。如有不同,可以自行Google,Baidu。

SQL语言主要分为六大类,分别是DDL(date definition Language)数据定义语言、DML(Data Manipulation Language)数据操作语言、DQL(Data Query Language)数据查询语言,TPL事物处理语言,DCL数据控制语言、CCL指针控制语言。

其中最为常用的便是上述当中的前三类语言,而后面的三类用的不是很多,所以对前三类语言做详细介绍,而后面的三类简单了解就可以了。

1、DDL(数据定义语言)

主要描述的是数据库当中所包含的数据结构,最常见的DDL语言就是用来定义一张表,包含列数、列类型、还有删除一张表,下面为创建一个User表的SQL语言描述。
删除表结构

DROP TABLE users;

创建一个新的表结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE  TABLE users{
_id INTEGER PRIMARY KEY AUTOINCREMENT,
name_raw_user_id INTEGER REFERENCES raw_users(_id),
photo_id INTEGER REFERENCES data(_id),
photo_file_id INTEGER REFERENCES photo_files(_id),
custom_ringtone TEXT,
send_to_voicemail INTEGER NOT NULL DEFAULT 0,
time_contacted INTEGER NOT NULL DEFAULT 0,
last_time_contacted INTEGER,
starred INTEGER NOT NULL DEFAULT 0,
has_phone_number INTEGER NOT NULL DEFAULT 0,
lookup TEXT,
status_update_id INTEGER REFERENCES data(_id)

};

这里需要注意的是:在定义一张表的时候,所有数据库语法的关键字大写,并且列名称是唯一的,这里不允许列名称之间相互重复。并且表的名称在同一个数据库当中也不能重复,通常使用的是一个复数名词

除了创建表以外,SQL的DDL允许创建其他标准的RDBMS的数据结构,而RDBMS当中常见的标准的数据结构除了相应的表以外,还有相应的视图(View)、触发器(trigger)、索引(index)。这里把定义一个给定数据库当中所有对象的DDL语句集合叫做这个数据库的模式(schema),其他的数据结构在什么场景下会用到呢?它们分别扮演着什么样子的角色呢,

  • 视图(View):
  • 触发器(trigger):
  • 索引(index):

2、DML(数据库操作语言)

这类型语句主要操作的是数据库当中数据的变化,有三个类型,分别是插入语句(insert),删除语句(delete),修改语句(update)。下面我们对上述定义的表结构进行数据上面的操作

插入语句

1
2
3
4
INSERT INTO users(
name_raw_contact_id,photo_id,photo_file_id,
last_time_contacted,status_update_id)
VALUES(null,null,null,18944658190,null);

更新语句

1
UPDATE users SET starred=1, has_phone_number=1 WHERE _id=3;

删除语句

1
DELETE FROM users WHERE _id=2;

注意:插入数据的时候,一般要主要强类型的问题,而也要关注主键为NULL以及主键重复的问题(自增长可以解决),还有就是如果字段通过NOT NULL修饰的时候,一般会在定义表结构的时候会给出相应的默认值。

3、DQL(数据库查询语句)

查询语句是最难的,也是使用最为平凡的语句,在关系术语当中,查询创建一个新的关系(通过一个虚拟表),即就是一个或者多个表的叉积的投影约束,具体的查询语句该怎么写请各位看客自行Google,或者baidu,如下所示一个查询示例,

查询语句

1
2
3
4
5
SELECT rc.display_name, u.starred 
FROM users u INNER JOIN raw_users ru
ON u.name_raw_contact_id=rc._id
WHERE NOT ru.display_name IS NULL
ORDER BY ru.display_name ASC;

连接是两个表的叉积的重要凭证。两个表的一个完整叉积把1个表的每一行于第二个表中每一行进行合拼,在上述的语句当中,users表于raw_contacts表进行连接。叉积的结果将有C(users)*C(raw_contacts)行。

完整的叉积有时候不是非常有用,在查询中 ON字句限制只有name_raw_contacts_id列的值和raw_contacts表中_id列的值相等的时候进行叉积,连接所产生的新的关系包含users表当中的行加上raw_contacts表当中的相应信息。这就非常有用了,下面有一个关于叉积理解的图,相信大家会理解什么是叉积的概念

叉积的概念

根据上述的这个小示例,我们很快就会明白多表查询的原理是什么,主要就是通过条件去约束叉积,然后拿到我们想要的数据。到了这里,基本的SQL语言也就差不多了,剩下三种语言类型,我们可以仔细看我的XXXX博客仔细研究。

三、SQLite入门

Android端使用的数据库就是开源数据库SQLite,它是一个小型的无服务器的数据库,由于这种数据库的种种特性,让它成为了在手机端极具吸引力的数据库之一,大体说一下都有什么好处,让该数据库这么火热,

该数据库是2000年开发的,当初开发的初衷就是轻量级方式管理结构话数据,所以非常的轻便,具体的优点有以下几点

  • 存储在里面的数据持久化的跨进程和电源存在。
  • 跨越系统软件升级和重新安装
  • 处理多种异常情况,非常优雅,比如:低内存环境、磁盘错误、电池电量不足、都不足矣破坏已经持久化的数据

当然缺点同时也是存在,关系数据库的几个重要特征部分丢失,SQLite当中没有强类型的支持,虽然有引用完整性的支持,但是默认是关闭的,唯独事务默认情况是开启的。

SQLite数据库其实就是一个文件。在Android设备当中大多数应用程序把数据库存储在文件系统沙箱当中,位于名为datebase的子目录当中,例如对于包是com.suansuan.application的应用程序,其数据库极有可能位于目录/data/data/com.suansuan.application/datebases中。

在以前人们想象能否将数据库放入到SD卡下,让其自己应用程序的数据库被多个程序所共享,但是相比于这么做Android提供了更好的解决思路。

1、SQLite语法

命令行当中是用sqlite3的时候,首先要记住每条命令必须以分号结束,就和我们写代码的时候一样。

当然还有一些命令,我们称之为元命令,这些命令不属于数据库所有,而是SQLite3去解释与执行,这些命令都是以,开头的命令,其中最常用的就是.help和.exit .schema命令

其余的就是常用的SQL命令了,当然也可以叫做SQL语句

2、支持的数据类型

前文已经说过SQLite数据库当中没有强引用的支持,实际上列的类型仅仅只是注释,列的类型仅仅只是提示而已,以帮助SQL引擎为此刻存储在该列的数据选择高效的表示。SQL引擎使用一些简单的规则来调节“类型相似性”,从而决定内部存储类型。这些规则几乎不可见,我们开发能感觉到的是给定数据集占用的磁盘数量而已。

实际开发当中,Google开发人员限制自己只使用SQLite的4种基本类型,(integer、real、text、blob)并且显式的使用文本来表示时间戳,使用integer来表示boolean值。

我们通过一个例子,来了解SQLite当中的“类型弱化”

1
2
3
4
5
sqlite> create table test(
...> c1 biginteger, c2 smalldatetime, c3 float(9,3));
sqlite> insert into test values("la","la","la");
sqlite> select * from test;
la|la|la

3、关于约束

3.1、主键约束

我们可以将列的约束定义在表结构当中,比如我们常常用来修饰主键的 PRIMARY KEY的约束,唯一,表示这一行。同时SQLite支持非整数主键、支持多列复合主键,这里需要注意一下,关于非整数主键,除了“唯一”约束以外,应该还存在“非空”约束。但是由于SQLite早期版本的疏忽,SQLite允许Null作为非整数主键。所以会出现很多行主键为NULL的情况(此NULL非彼NULL),从而我们需要使用代码给插入数据的主键做非空判断,要不然我们数据库当中就会出现无法通过其主键都无法区分的数据,笔者这里把这些数据叫做“脏数据”

通过上述的问题,很多开发人员都使用integer来做为主键列,同时将主键设置为自增长,PRIMARY KEY AUTOINCREMENT。并且SQL引擎为了解决上述问题,会默认的在表当中加入一个隐式的id,

关于自增长,我们可以通过下面示例来了解

1
2
3
4
5
6
7
8
9
sqlite> create table test(
...> key integer primary key,
...> val text);
sqlite> insert into test(val) values("something");
sqlite> insert into test(val) values("liusuansuan");
sqlite> select * from test;
1|something
2|liusuansuan
sqlite>

自增长非常好用,可以唯一表示我们的数据,并且我们都不用去关心,但是正是因为我们的不关心将导致大量丑陋且愚蠢的代码存在,比如我们添加一个新行,并且需要将新行的id存储在SP当中,以便下次快速访问,这种情况下,各位看客想想该如何实现呢。其实笨办法确实有,并且笔者只想到了一种,如果看客有好的想法可以下方留言,那就是再查一遍,

3.2、外键约束

上文提到过外键约束,SQLite不强制执行外键约束,其实和SQLite列类型类似相似本质上都是注释,但是可以通过修改相应的属性来让SQLite强制执行外键约束

pragma FOREIGN_KEYS=true

通过上述指令开启外键的引用完整性。

下面我们来看一下关于外键注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sqlite> creaet table people(
...> name text, address integer references addresses(id));
Error: near "creaet": syntax error
sqlite> create table people(
...> name text, address integer references addresses(id));
sqlite> create table addresses(
...> id integer primary key, street text);
sqlite> insert into people values("liupengcheng",99);
sqlite> insert into addresses(street) values("shahe");
sqlite> select * from people;
liupengcheng|99
sqlite> select * from addresses;
1|shahe
sqlite> select * from people, addresses where address=id;
sqlite>

上述语句,如果是在支持引用完整性的数据库当中,第一个insert将失败,并且第一个create table people也将失败,因为找不到addresses这个外键表,很显然上述我们的试验都成功了,也就意味这SQLite数据库不会去强制执行外键约束。

但是SQLite的优点就是灵活方便,如果我们需要设计一个复杂类型的、并且多个表通过外键所连接,像这种时候,易于修改和高效的数据存储反而成为了SQLite的闪光点,Google官方文档中描述,鼓励开发人员使用标准范式,然后让开发这自己编写代码的时候自己规划引用完整约束,而不是依赖数据库去实现。

下面我们来模拟一个简单的连接

1
2
3
4
5
6
7
8
9
10
11
12
13
sqlite> create table people(
...> name text, address integer references addresses(id));
sqlite> create table addresses(id integer primary key, street text);
sqlite> insert into addresses(street) values("北京");
sqlite> insert into addresses(street) values("上海");
sqlite> insert into people values("张飞“,1);
sqlite> insert into people values("张飞",1);
sqlite> insert into people values("关羽",2);
sqlite> insert into people values("刘备",2);
sqlite> select name,street from people,addresses where address=id;
张飞|北京
关羽|上海
刘备|上海

SQLite中还支持一些其他的约束,比如:

  • unique:该约束表示SQLite将拒绝任何试图在表中添加将导致该列出现重复值的一行。
  • not null:该约束表述SQLite将拒绝任何试图将表中数据置为NULL的所有操纵。
  • check(expression):当次约束修饰列时,每当一个新行添加到表中时,或当修改现有的行时,都会对表达式求值,如果求值的结果转化整数为0时,该操作将失败,操作将取消。如果表达式的计算结果为NULL或者任何其他非零值,则操作成功

下面展示一个列约束:

1
2
3
4
5
6
7
8
9
sqlite> create table test(
...> c1 text unique, c2 text not null, c3 text check(c3 in ("OK","Dandy")));
sqlite> insert into test values("dandy","dandy","dandy");
Error: CHECK constraint failed: test
sqlite> insert into test values("dandy","dandy","Dandy");
sqlite> insert into test values("dandy","dandy","Dandy");
Error: UNIQUE constraint failed: test.c1
sqlite> insert into test values("dandy",null,"Dandy");
Error: NOT NULL constraint failed: test.c2

4、SQLite数据库示例

在开始我们的程序示例之前,我们需要对我们数据库当中的数据进行一些格式化,好方便我们之后的查看数据。使用SQLite的元命令来进行数据库数据展示的格式化。

1
2
3
sqlite>.header on
sqlite>.mode column
sqlite>.timer on

具体详细内容,可以参考SQLite元命令http://www.runoob.com/sqlite/sqlite-commands.html

我们来尝试去创建一个完整的User的数据库。

1、创建User表
1
2
3
4
sqlite> create table users(
...> _id integer primary key autoincrement,
...> name text not null);
sqlite>

我们需要一个字段是用来记录这条记录是什么时候被修改的,好方便我们以后对数据的修改

1
2
sqlite> alter table users add last_modified_time text;
Run Time: real 0.003 user 0.000000 sys 0.000000
2、创建触发器

我们让刚刚我们定义完的字段,可以根据我们插入数据,或者是添加数据跟随我们插入数据和更新数据取当前最新的时间。这里定义两个触发器,一个是跟随insert所进行触发,一个根据update进行触发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sqlite> create trigger t_users_audit_i
...> after insert
...> on users
...> begin
...> update users set last_modified_time=datetime('now','utc')
...> where rowid=new.rowid; end;
Run Time: real 0.003 user 0.000000 sys 0.000000

sqlite> create trigger t_users_audit_u
...> after update
...> on users
...> begin
...> update users set last_modified=datetime('now','utc')
...> where rowid=new.rowid;
...> end;

Run Time: real 0.003 user 0.000000 sys 0.000000

上述触发器创建的时候用到了datetime数据库内置和日期相关的函数。具体内容可以查看http://www.runoob.com/sqlite/sqlite-date-time.html

现在我们插入一条数据来验证我们刚刚创建的结构是否正确。

1
2
3
4
5
6
7
8
9
10
11
sqlite> insert into users(name) values("liupengcheng");

Run Time: real 0.003 user 0.000000 sys 0.004000

sqlite> select * from users;

_id name last_modified_time
---------- ------------ -------------------
2 liupengcheng 2018-03-09 09:50:40

Run Time: real 0.000 user 0.000000 sys 0.000000

我们的User可能需要地址,比如客户的收货地址,所以我们创建一个地址表,

1
2
3
4
5
6
7
8
sqlite> create table addresses(
...> _id integer primary key autoincrement,
...> number integer not null,
...> unit text,
...> street text not null,
...> city integer references cities);

Run Time: real 0.004 user 0.000000 sys 0.000000

如果我们引用完整性的支持已经开启的话,这个表的定义肯定会导致一个错误,因为这里cities这张表是不存在的。不过在SQLite数据库的默认配置当中,是没有问题的,我们可以稍后去定义cities这张表。

现在我们的用户存在了,地址表也存在了,这里分析得到,一个用户可以有多个收货地址,一个收货地址可以被多个人多共有,所以这里的关系是多对多,一般的主外键支持一对多,如果是多对多的关系的话,我们就需要建立第三张表专门的让上述两张表进行对应了。

1
2
3
4
5
sqlite> create table users_addresses(
...> users integer references users,
...> address integer references addresses);

Run Time: real 0.004 user 0.000000 sys 0.004000

现在我们来我们创建的数据库添加数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
insert into users(name) values("wangyiqi");
insert into users(name) values("wwangpeng");
insert into users(name) values("liuna");
insert into users(name) values("yangqiqi");
sqlite> select * from users;

_id name last_modified_time
---------- ------------ -------------------
2 liupengcheng 2018-03-09 09:50:40
3 wangyiqi 2018-03-09 10:18:42
4 wwangpeng 2018-03-09 10:18:42
5 liuna 2018-03-09 10:18:42
6 yangqiqi 2018-03-09 10:19:01
Run Time: real 0.003 user 0.000000 sys 0.000000


insert into addresses(number,street) values(651, "北京昌平");
insert into addresses(number,street) values(1600, "上海浦东");
insert into addresses(number,street) values(259, "哈尔滨江北");
insert into addresses(number,street) values(693, "甘肃天水");
insert into addresses(number,street) values(1658, "北京朝阳");
sqlite> select * from addresses;

_id number unit street city
---------- ---------- ---------- ------------ ----------
1 651 北京昌平
2 1600 上海浦东
3 259 哈尔滨江
4 693 甘肃天水
5 1658 北京朝阳
Run Time: real 0.000 user 0.000000 sys 0.000000


insert into users_addresses(users,address) values(2,1);
insert into users_addresses(users,address) values(3,2);
insert into users_addresses(users,address) values(4,3);
insert into users_addresses(users,address) values(5,4);
insert into users_addresses(users,address) values(6,5);
sqlite> select * from users_addresses;

users address
---------- ----------
2 1
3 2
4 3
5 4
6 5
Run Time: real 0.000 user 0.000000 sys 0.000000

现在数据也有了,关系也键全了,现在可以使用关联关系进行查看我们之前所编辑的数据实体了。

1
2
3
4
5
6
7
8
9
10
11
sqlite> select name,street,number from users,addresses,users_addresses where users._id = users_addresses.users and addresses._id = users_addresses.address;

name street number
------------ ------------ ----------
liupengcheng 北京昌平 651
wangyiqi 上海浦东 1600
wwangpeng 哈尔滨江 259
liuna 甘肃天水 693
yangqiqi 北京朝阳 1658

Run Time: real 0.000 user 0.000000 sys 0.000000

如果我们想确定一个地址下面有多少用户,我们可以使用count函数和group by字句来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
select count(name),number,street 
from users,addresses,users_addresses
where users._id=users_addresses.users
and addresses._id=users_addresses.address
group by number,street;

count(name) number street
----------- ---------- ---------------
1 259 哈尔滨江北
1 651 北京昌平
1 693 甘肃天水
1 1600 上海浦东
1 1658 北京朝阳

Run Time: real 0.000 user 0.000000 sys 0.000000

Bitmap的基础用法

1、前言

在前面的两讲当中,我们研究了Drawable是什么东西,我们今天一块来探讨一下什么是Bitmap,首先我们要明白,Drawable是Android当中所有可以绘制事物的一个抽象的概念,而Bitmap是可以绘制的,所以Bitmap属于Drawable的一种,所以Drawable是大的范围,而Bitmap是小的范围,如果是这样的话,就会衍生出,一个Bitmap是Drawable,而Drawable不一定是Bitmap。这也是两者的区别所在,我们在前两讲当中提过对Bitmap进行的一个封装就是BitmapDrawable而BitmapDrawable是Drawable的一个子类,想想我们当时定义的Xml文件,我相信大家会理解这两者的区别。

2、正文

今天的内容我先给大家看一幅图片,大家应该就明白了

Bitmap知识导航

今天的重点是关于Bitmap,至于关于如何绘制,以后会讲解的,下面我们就上述三点来开始今天的博客,不过在开始之前我们一定到明白,Drawable和Bitmap的区别。

Bitmap是一张位图,扩展名为.bmp.dip,为什么说它是位图呢,因为它将图像定义为由点(像素)组成,每个点可以由多种色彩表示,包括2、4、8、16、24和32位色彩,例如一幅1024×768分辨率的32位真彩图片,其所占存储字节数为:1024×768×32/8=3072KB。位图文件图像效果好,但是非压缩格式的,需要占用较大存储空间,所以我们遇到的OOM大部分都是由Bitmap的造成的

如果还是不太清楚Bitmap和Drawable的区别的话,那我们现在就理解为,Drawable实际上就是一个空的架子,里面可以以装载各式各样的图像,而bitmap就是其中的一幅图像,这种图像图像质量比较好。而前者重行为,而后者重数据(图像数据)像一般的移动架子之类的我们就会对其封装到Drawable当中,而如果我们要去改动画本身的属性,就去修改Bitmap,或者其他的画。

3、Bitmap怎么来的(怎么创建一个Bitmap,或者通过资源生成一个Bitmap)

Bitmap是不能New出来的,因为它的构造器私有,并且它的实例化是JNI做的,所以只有去实例化了JNI我们才能去创建Bitmap,既然是JNI做的,那么必然Google会给我提供使用的方法,不错那就Bitmap.createBitmap();通过这个方法我们就能得到一个Bitmap。

在我们创建Bitmap的时候,涉及到一个名为Bitmap.Config的这个类,这个类的具体的作用就是在我们构建自己新的Bitmap的时候来告诉系统,构建一个什么质量的Bitmap。好的 还是坏的,点进去我们看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public enum Config {
ALPHA_8 (1),
RGB_565 (3),
@Deprecated
ARGB_4444 (4),
ARGB_8888 (5),
final int nativeInt;

private static Config sConfigs[] = {
null, ALPHA_8, null, RGB_565, ARGB_4444, ARGB_8888
};

Config(int ni) {
this.nativeInt = ni;
}

static Config nativeToConfig(int ni) {
return sConfigs[ni];
}
}
public enum Config {
ALPHA_8 (1),
RGB_565 (3),
@Deprecated
ARGB_4444 (4),
ARGB_8888 (5),
final int nativeInt;

private static Config sConfigs[] = {
null, ALPHA_8, null, RGB_565, ARGB_4444, ARGB_8888
};

Config(int ni) {
this.nativeInt = ni;
}

static Config nativeToConfig(int ni) {
return sConfigs[ni];
}
}

大家也看到了在这其中主要维护了4个变量,来表示图片质量的高低,分别是:

  • ALPHA_8 Alpha:由8位组成
  • ARGB_4444:由4个4位组成即16位,
  • ARGB_8888:由4个8位组成即32位,
  • RGB_565:R为5位,G为6位,B为5位共16位,

当然位数越高代表的图片的质量就越高

createBitmap(Bitmap source):根据原有的Bitmap去创建一个新的bitmap

  • createBitmap(Bitmap source, int x, int y, int width, int height):根据原有的bitmap去创建一个从原有bitmap的(x, y)这个点起,宽为width,高为height的bitmap一般使用这个方法去裁剪原有的bitmap

  • createBitmap(Bitmap source, int x, int y, int width, int height, Matrix matrix, boolean) :根据原有的bitmap创建一个从原有bitmap的(x, y)这个点起,宽为width,高为height的bitmap,并且对该bitmap进行matrix的矩阵变换,最后一个参数是:当进行的不仅仅是平移变换时,filter为true的情况下会进行滤波处理,意思就是说让新生成的bitmap的图像质量好点,为false,就没有滤波处理

  • createBitmap(int width, int height, Config config):创建一个宽为width,高为height,图片质量为config的bitmap

  • createBitmap(DisplayMetrics displayMetrics, int width, int height, Config config):根据屏幕信息displayMetrics创建一个宽为width,高为height,图片质量为config的bitmap

  • createBitmap(DisplayMetrics displayMetrics, int[] color, int width, int height, Config onfig):根据屏幕信息displayMetrice创建一个宽为width,高为heighe,图片质量为config,并且使用颜色数组color从上而下,从左到右进行填充

  • createBitmap(DisplayMetrics, int[], int, int, int, int, Config): 这个方法和上述方法功能相近,但是有两个参数博主不知道什么意思,Android官方文档上的解释居然和上述方法相近

  • createBitmap(int[], int, int, int, int, Config):创建一个宽为width,高为heighe,图片质量为config,并且使用颜色数组color从上而下,从左到右进行填充

  • createBitmap(int[], int, int, Config):创建一个宽为width,高为heighe,图片质量为config,并且使用颜色数组color从上而下,从左到右进行填充

  • createScaledBitmap(Bitmap src, int dstWidth, int dstHeight, boolean filter):对原始的Bitmap进行缩放,最后一个参数的意思就是bitmap边缘抗锯齿。
    上述就是我们自己去创建一个一个新的Bitmap,在这里我们得说说 在我们使用裁剪Bitmap的时候createBitmap(Bitmap source, int x, int y, int width, int height)我们得记住我们开始的点x+width<=source.getWidth(),要不然会报异常。

下面我们去看看如何在资源文件当中去解析出我们的Bitmap,在我们开始解析之前,我们要在这里再次为大家介绍一个帮助类,那么这个帮助类是BitmapFactory,这个类有一个decodeXXXXX();的静态方法,通过这个方法我们就可以得到一个Bitmap对象,decodeXXXXX();主要分为四种,分别是读取文件,资源,流,byte[]数组,当中的Bitmap,而在读取的时候会有一些配置需要我们去选择,Google也给我们封装在BitmapFactory的静态内部类Options类当中,我们点进去看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
public static class Options {

public Options() {
inDither = false;
inScaled = true;
inPremultiplied = true;
}

/** 维护了一个Bitmap位图对象,如果更改解码方式则,重用这个位图加载内容。 */
public Bitmap inBitmap;

/**
* 配置Bitmap是否可以更改,比如:在Bitmap上隔几个像素加一条线段
*/
@SuppressWarnings({"UnusedDeclaration"}) // used in native code
public boolean inMutable;

/**
* 如果设置为true,不获取图片,不分配内存,但会返回图片的高度宽度信息。
*/
public boolean inJustDecodeBounds;

/**
* 图片缩放的倍数
*/
public int inSampleSize;

/**
* 设置解码图片质量
*/
public Bitmap.Config inPreferredConfig = Bitmap.Config.ARGB_8888;

/**
* 为true,产生的位图将它的颜色通道pre-multipled alpha通道。
*/
public boolean inPremultiplied;

/**
* 如果为true,解码器尝试抖动解码
*/
public boolean inDither;

/**
* 用于位图的像素压缩比
*/
public int inDensity;

/**
* 用于目标位图的像素压缩比(要生成的位图)
*/
public int inTargetDensity;

/**
* 当前屏幕的像素密度
*/
public int inScreenDensity;

/**
* 设置为true时进行图片压缩,从inDensity到inTargetDensity
*/
public boolean inScaled;

/**
* 当存储Pixel的内存空间在系统内存不足时是否可以被回收
*/
@Deprecated
public boolean inPurgeable;

/**
* inPurgeable为true情况下才生效,是否可以共享一个InputStream
*/
@Deprecated
public boolean inInputShareable;

/**
* 为true则优先保证Bitmap质量其次是解码速度
*/
public boolean inPreferQualityOverSpeed;

/**
* 获取图片的宽度值
*/
public int outWidth;

/**
* 获取图片的高度值
*/
public int outHeight;

/**
* 设置解码图像
*/
public String outMimeType;

/**
* 创建临时文件,将图片存储
*/
public byte[] inTempStorage;

/**
* mCancel为true取消当前Decode
*/
public boolean mCancel;

}

读取文件当中的bitmap文件

1
2
3
4
5
6
7
public static Bitmap decodeFile(String pathName, Options opts) //从文件读取图片 

public static Bitmap decodeFile(String pathName)

public static Bitmap decodeFileDescriptor(FileDescriptor fd)//从文件读取文件 与decodeFile不同的是这个直接调用JNI函数进行读取 效率比较高

public static Bitmap decodeFileDescriptor(FileDescriptor fd, Rect outPadding, Options opts)

读取bitmap资源文件

1
2
3
public static Bitmap decodeResource(Resources res, int id) //从资源文件读取图片

public static Bitmap decodeResource(Resources res, int id, Options opts)

读取流中bitmap

1
2
3
public static Bitmap decodeStream(InputStream is) //从输入流读取图片

public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts)

读取byte[]数组中的Bitmap

1
2
3
public static Bitmap decodeByteArray(byte[] data, int offset, int length) //从数组读取图片

public static Bitmap decodeByteArray(byte[] data, int offset, int length, Options opts)

以上就是如何创建一个Bitmap文件

4、如何显示到屏幕上面,

如果我们要把我们的Bitmap显示到屏幕上我们必须要依赖View通过View进行显示,其实每一个View都是可以显示图片的,用作背景的情况下都是可以的,但是显示的是一个Drawable对象,所以有时候我们要去研究Drawable和Bitmap之间的转换问题,

注意:(这里考虑View,ViewGroup,ImagView,ImageButton等个别可以直接显示的等会再说)

1、Bitmap转换成为Drawable,

我们说过Bitmap是Drawable的一个子类,所以我们只需要通过中间BitmapDrawable去封装一下Bitmap即可

1
2
3
4
5
6
7
8
9
10
11
/** bitmap ---> Drawable */
public Drawable bitmap2Drawable(Bitmap bitmap){
BitmapDrawable bitmapDrawable = new BitmapDrawable(bitmap);
return bitmapDrawable;
}

/** bitmap ---> Drawable */
public Drawable bitmap2Drawable(Resources res, Bitmap bitmap){
BitmapDrawable bitmapDrawable = new BitmapDrawable(res, bitmap);
return bitmapDrawable;
}

2、Drawable转换成为Bitmap

我们说过Bitmap保存的是图像的数据,通过这个我们可以使用Canvas把Drawable的图像数据画到bitmap

1
2
3
4
5
6
7
8
9
/** Drawable --- > Bitmap */
public Bitmap bitmap2Drawable(Drawable drawable){
Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(),
drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
drawable.draw(canvas);
return bitmap;
}

转换成为Drawable以后我们就可以轻松设置到随便一个View的背景显示出来了,至于ImageView,ImageButton,它则提供了直接设置Bitmap的方法,直接调用即可。

5、怎么保存我们的Bitmap文件呢!

我们先来看一下这个方法

1
2
3
public boolean compress(CompressFormat format, int quality, OutputStream stream)//按指定的图片格式以及画质,将图片转换为输出流。
format:Bitmap.CompressFormat.PNG或Bitmap.CompressFormat.JPEG
quality:画质,0-100.0表示最低画质压缩,100以最高画质压缩。对于PNG等无损格式的图片,会忽略此项设置。
1
2
3
4
5
6
7
8
9
10
11
12
13
/** 把Bitmap图片保存在本地 */
public void writeBitmapToFile(String filePath, Bitmap b, int quality) {
try {
File desFile = new File(filePath);
FileOutputStream fos = new FileOutputStream(desFile);
BufferedOutputStream bos = new BufferedOutputStream(fos);
b.compress(Bitmap.CompressFormat.JPEG, quality, bos);
bos.flush();
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}

通过上述方法不仅仅是可以保存,它是可以压缩的,我们来看看这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** 以quality画质对Bitmap进行压缩 */
private Bitmap compressImage(Bitmap image, int quality) {
if (image == null || quality < 0 || quality > 100) { return null; }
ByteArrayOutputStream baos = null;
try {
baos = new ByteArrayOutputStream();
image.compress(Bitmap.CompressFormat.JPEG, quality, baos);
byte[] bytes = baos.toByteArray();
ByteArrayInputStream isBm = new ByteArrayInputStream(bytes);
Bitmap bitmap = BitmapFactory.decodeStream(isBm);
return bitmap;
} catch (OutOfMemoryError e) {
} finally {
try {
if (baos != null) {
baos.close();
}
} catch (IOException e) {
}
}
return null;
}

关于优化的问题,后面博客说吧,毕竟这篇是介绍篇,好了我们接下来看看我们的今天的测试Demo,其实我们学习关于Bitmap的各种操作方法,大多数的情况不是说是为了某种目的去操作它。而是由于OOM异常,被迫的去压缩,去裁剪,去降低图片质量,已达到最优的情况,最优的情况包括,首先程序不能崩,速度最快,用户体验不能卡之类的。所以我打算我的这个Demo就主要去对这一方面去写点东西。

首先我们来看一下效果吧

高斯模糊

生成一种毛玻璃的效果,在这里我要说的是每一种毛玻璃的效果都有一种模糊算法在支持,有的模糊算法是C写的,有的是用Java写的,这里面有很大的学问,这里就不要深究啦总之通过原图的Bitmap生成模糊的Bitmap很吃内存,没办法,我们首先对Bitmap缩小 去生成模糊bitmap 然后再去放大,有可能还会用到压缩Bitmap,最后以ImageView为载体显示出来,所作的一切都是被迫的,这种情况会在以后发生很多,好了看代码吧!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
/**
* 通过Java去实现的模糊算法 不是博主写的,
* Created by jay on 11/7/15.
*/
public class FastBlurUtil {

public static Bitmap doBlur(Bitmap sentBitmap, int radius, boolean canReuseInBitmap) {

Bitmap bitmap;
if (canReuseInBitmap) {
bitmap = sentBitmap;
} else {
bitmap = sentBitmap.copy(sentBitmap.getConfig(), true);
}

if (radius < 1) {
return (null);
}

int w = bitmap.getWidth();
int h = bitmap.getHeight();

int[] pix = new int[w * h];
bitmap.getPixels(pix, 0, w, 0, 0, w, h);

int wm = w - 1;
int hm = h - 1;
int wh = w * h;
int div = radius + radius + 1;

int r[] = new int[wh];
int g[] = new int[wh];
int b[] = new int[wh];
int rsum, gsum, bsum, x, y, i, p, yp, yi, yw;
int vmin[] = new int[Math.max(w, h)];

int divsum = (div + 1) >> 1;
divsum *= divsum;
int dv[] = new int[256 * divsum];
for (i = 0; i < 256 * divsum; i++) {
dv[i] = (i / divsum);
}

yw = yi = 0;

int[][] stack = new int[div][3];
int stackpointer;
int stackstart;
int[] sir;
int rbs;
int r1 = radius + 1;
int routsum, goutsum, boutsum;
int rinsum, ginsum, binsum;

for (y = 0; y < h; y++) {
rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
for (i = -radius; i <= radius; i++) {
p = pix[yi + Math.min(wm, Math.max(i, 0))];
sir = stack[i + radius];
sir[0] = (p & 0xff0000) >> 16;
sir[1] = (p & 0x00ff00) >> 8;
sir[2] = (p & 0x0000ff);
rbs = r1 - Math.abs(i);
rsum += sir[0] * rbs;
gsum += sir[1] * rbs;
bsum += sir[2] * rbs;
if (i > 0) {
rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];
} else {
routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];
}
}
stackpointer = radius;

for (x = 0; x < w; x++) {

r[yi] = dv[rsum];
g[yi] = dv[gsum];
b[yi] = dv[bsum];

rsum -= routsum;
gsum -= goutsum;
bsum -= boutsum;

stackstart = stackpointer - radius + div;
sir = stack[stackstart % div];

routsum -= sir[0];
goutsum -= sir[1];
boutsum -= sir[2];

if (y == 0) {
vmin[x] = Math.min(x + radius + 1, wm);
}
p = pix[yw + vmin[x]];

sir[0] = (p & 0xff0000) >> 16;
sir[1] = (p & 0x00ff00) >> 8;
sir[2] = (p & 0x0000ff);

rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];

rsum += rinsum;
gsum += ginsum;
bsum += binsum;

stackpointer = (stackpointer + 1) % div;
sir = stack[(stackpointer) % div];

routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];

rinsum -= sir[0];
ginsum -= sir[1];
binsum -= sir[2];

yi++;
}
yw += w;
}
for (x = 0; x < w; x++) {
rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
yp = -radius * w;
for (i = -radius; i <= radius; i++) {
yi = Math.max(0, yp) + x;

sir = stack[i + radius];

sir[0] = r[yi];
sir[1] = g[yi];
sir[2] = b[yi];

rbs = r1 - Math.abs(i);

rsum += r[yi] * rbs;
gsum += g[yi] * rbs;
bsum += b[yi] * rbs;

if (i > 0) {
rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];
} else {
routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];
}

if (i < hm) {
yp += w;
}
}
yi = x;
stackpointer = radius;
for (y = 0; y < h; y++) {
// Preserve alpha channel: ( 0xff000000 & pix[yi] )
pix[yi] = (0xff000000 & pix[yi]) | (dv[rsum] << 16) | (dv[gsum] << 8) | dv[bsum];

rsum -= routsum;
gsum -= goutsum;
bsum -= boutsum;

stackstart = stackpointer - radius + div;
sir = stack[stackstart % div];

routsum -= sir[0];
goutsum -= sir[1];
boutsum -= sir[2];

if (x == 0) {
vmin[y] = Math.min(y + r1, hm) * w;
}
p = x + vmin[y];

sir[0] = r[p];
sir[1] = g[p];
sir[2] = b[p];

rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];

rsum += rinsum;
gsum += ginsum;
bsum += binsum;

stackpointer = (stackpointer + 1) % div;
sir = stack[stackpointer];

routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];

rinsum -= sir[0];
ginsum -= sir[1];
binsum -= sir[2];

yi += w;
}
}

bitmap.setPixels(pix, 0, w, 0, 0, w, h);

return (bitmap);
}
}

来看看我们的Layout:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="match_parent"
>

<ImageView
android:layout_width="match_parent"
android:layout_weight="1"
android:src="@mipmap/index"
android:layout_margin="20dip"
android:layout_height="0dip"
/>

<ImageView
android:id="@+id/iv_content"
android:layout_margin="20dip"
android:layout_width="match_parent"
android:layout_weight="1"
android:layout_height="0dip" />

</LinearLayout>

和以前一样,上面的为原始图,下面的为模糊图

activity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class MainActivity extends AppCompatActivity {

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

/** 初始化View */
private void initView() {
ImageView mImage = (ImageView) findViewById(R.id.iv_content);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.index).
copy(Bitmap.Config.ARGB_4444, true);
mImage.setImageBitmap(getBitmapBlur(bitmap, 10));
}

/** 对Bitmap进行处理 */
private Bitmap initBitmap(Bitmap bitmap, boolean isup) {
int width;
int height;
if(isup){
width = bitmap.getWidth() * 6;
height = bitmap.getHeight() * 6;
}else{
width = bitmap.getWidth() / 6;
height = bitmap.getHeight() / 6;
}
return Bitmap.createScaledBitmap(bitmap, width, height, false);
}

/** 初始化Bitmap */
private Bitmap getBitmapBlur(Bitmap bitmap, int level) {
if(level > 0){
Bitmap bitmap1 = FastBlurUtil.doBlur(initBitmap(bitmap, false), level, true);
return initBitmap(bitmap1, true) ;
}
return initBitmap(bitmap, true);

效果就是上述,关于内存的东西,后面会提到,

Drawable的进阶用法

1、前言

在前一篇博客当中提到关于Drawable的一些常用的用法,和使用系统自身的提供Drawable子类,来实现一定的具有功能性的Drawable,那么在今天的博客当中,我们就去自己定义Drawable的子类去实现我们自己功能的Drawable,

2、正文

首先我们去自己考虑一下,自定义Drawable是什么,先不着急,我们思考一下Android系统为我们提供的Drawable的子类实现,我们明确是对我们原有的图片进行一定的封装,然后进行一定的处理,让它和原来的最根本的res资源Drawable有某一种小功能,比如我们的ClipDrawable,ScaleDrawable,RotateDrawable,第一个是对原有的res资源Drawable有裁剪的功能,第二ScaleDrawable,是对原有的的res下的根本Drawable有一定的缩放功能,而第三个RotateDrawable就是对原来最原始的Drawable有一定的旋转功能。我们自定义Drawable首先我们要明确我们自定义的Drawable去实现什么功能,在这里我们去实现一个百叶窗的动画效果,百叶窗效果:为了和原图作比较,我们把原图和百叶窗的效果一起展示出来,

百叶窗效果

首先来看我们的自定义Drawable,我们起名为ShuttersDrawable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.drawable.Drawable;

/**
* 自定义百叶窗的Drawable对象
* @author liusuansuan
*/
//TODO(suansuan):由于创建Bitmap的时候,我们的宽度不能是0,
//TODO(suansuan): 所以在做动画的时候我们不能通过ValueAnimator从0开始
public class ShuttersDrawable extends Drawable{

/** 在这里定义一下我们所要 */
private Bitmap mBitmap; //原图的Bitmap的对象
private Bitmap[] mBitmaps; //对原图进行分割以后百叶窗,叶片的Bitmap对象数组
private int mWidth; //原图的宽度。
private int mHeight; //原图的高度
private int mCellWidth; //单个叶片的宽度
private int mCount; //叶片总数
private Paint mPaint; //画笔
private float scale = 1f; //比例,当前绘制的比例,主要通过该属性去控制整个百叶窗动画


/** 初始化操作 */
public ShuttersDrawable(Bitmap bitmap, int count){
this.mBitmap = bitmap;
this.mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
this.mCount = count;
initBitmap();
}

/** 初始化Bitmap */
private void initBitmap() {
this.mHeight = mBitmap.getHeight();
this.mWidth = mBitmap.getWidth();
mCellWidth = mWidth / mCount;
mBitmaps = new Bitmap[mCount];
}

/** 绘制方法 */
@Override
public void draw(Canvas canvas) {
canvas.save();
float width = (mCellWidth * scale) + 1;
if(width >= mCellWidth){
width = mCellWidth;
}
for(int i = 0 ; i < mBitmaps.length; i++){
mBitmaps[i] = Bitmap.createBitmap(mBitmap, i * mCellWidth, 0,(int)width, mHeight);
canvas.drawBitmap(mBitmaps[i], i * mCellWidth, 0, mPaint);
}
canvas.restore();
}

/** 设置比例 */
public void setScale(float mScale){
this.scale = mScale;
invalidateSelf();
}

/** 主要是给以后使用到该Drawable,可以得到该Drawable的宽度 */
@Override
public int getIntrinsicWidth() {
return mBitmap.getWidth();
}

/** 主要是给以后使用到该Drawable,可以得到该Drawable的高度 */
@Override
public int getIntrinsicHeight() {
return mBitmap.getHeight();
}


@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}

@Override
public void setAlpha(int alpha) {
mPaint.setAlpha(alpha);
}

@Override
public void setColorFilter(ColorFilter cf) {
mPaint.setColorFilter(cf);
}
}

自定义Drawable我们首先需要继承Drawable,有几个方法我们是必须所要实现的那就是

  • 1、draw(Canvas canvas):就是使用参数Canvas画布对象,给该Drawable画上内容的方法。
  • getOpacity():
  • setAlpha(int alpha):设置透明度
  • setColorFilter(ColorFilter cf):设置颜色过滤。

其余我写的都比较简单,大家应该都能看懂是什么意思,我也就不献丑了。说一下具体的思路。就是我们把Bitmap分成一个个的叶片对象,然后对叶片对象进行绘制,然后通过sclae比例去控制绘制叶片的宽度,

activity_main.xml layout:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal">

<!-- 用来放至原来的Drawable对象 -->
<ImageView
android:layout_width="wrap_content"
android:layout_height="0dip"
android:layout_weight="1"
android:background="#F00"
android:src="@drawable/pressed"/>

<!-- 用来放至现在的ShuttersDrawable对象的载体 -->
<ImageView
android:id="@+id/iv_imageView"
android:layout_width="wrap_content"
android:background="#0F0"
android:layout_height="0dip"
android:layout_weight="1"/>

  <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="click me start Animator"
    android:onClick="click"/>


</LinearLayout>

使用Button来控制开启动画,
再来我们就看看在Activity中的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
*
* @author suansuan
*
*/
public class MainActivity extends Activity {

private ShuttersDrawable mShuttersDrawable;

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

/** 初始化ImageView */
private void initImageView() {
ImageView iv = (ImageView) findViewById(R.id.iv_imageView);
Bitmap decodeResource = BitmapFactory.decodeResource(getResources(),
R.drawable.pressed);
mShuttersDrawable = new ShuttersDrawable(decodeResource, 5);
iv.setImageDrawable(mShuttersDrawable);
}

/** Button的点击事件 */
@SuppressLint("NewApi")
//TODO(suansuan):在这里,博主建议大家还是使用V7包下面的Animator,
//TODO(suansuan):博主是因为个人原因没用使用,为了更好的适配,大家请不要这样
public void click(View view){
ValueAnimator mValueAnimation = ValueAnimator.ofFloat(0f, 1f).
setDuration(10000);
mValueAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@SuppressLint("NewApi") @Override
public void onAnimationUpdate(ValueAnimator animation) {
float animatedValue = (Float) animation.getAnimatedValue();
mShuttersDrawable.setScale(animatedValue);
}
});
mValueAnimation.start();
}
}

效果呢就如开头那样所示。

上述就是如何通过自定义Drawable去实现百叶窗的效果,

2、通过自定义View去实现一些关于画图

接下来我们去探讨第二个问题,我们以前会去通过自定义View去实现一些关于画图的东西其实都可以通过自定义Drawable去实现,比如鸿洋大神可以通过自定义Drawable去实现圆角图片一样,这个时候我们想到的也许就是去自定义Imageview去实现,大家应该发现了,自定义Drawable就可以看做是自定义View的一种轻量级实现,下面我们看看一种效果,然后我们去自定义这种效果,这种效果叫什么呢,我也不知,总之我们看吧!

专辑盒子

这种类似于专辑盒的Icon,在各大音乐平台多的是,相信大家也都做过,我们可以类似于这样的专辑盒去做一个自定义Drawable,好了废话少说,我们开始干活。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
package vadsdk.testdrawable.drawable;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.drawable.Drawable;

import vadsdk.testdrawable.R;


public class AlbumDrawable extends Drawable {

/** 画笔对象 */
private Paint mPaint;

/** 右侧图片对象 */
private Bitmap mLeftDrawable;

/** 背景阴影图片 */
private Bitmap mBackgoundDrawable;

/** 内容图片 */
private Bitmap mContentBitmap;

/** 上下文环境 */
private Context mContext;

/** 构造方法 */
public AlbumDrawable(Bitmap bitmap, Context context) {
this.mContentBitmap = bitmap;
this.mContext = context;
init();
}

/** 初始化操作 */
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mLeftDrawable = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.album_chart_left);
}


@Override
public void draw(Canvas canvas) {
onDrawLeftDrawable(canvas);
onDrawLeftBitmap(canvas);
onDrawDrawable(canvas);
}

/** 绘制小图片 */
private void onDrawLeftDrawable(Canvas canvas) {
canvas.save();
canvas.translate(mLeftDrawable.getWidth()/2f, mLeftDrawable.getWidth() * 3f/5f);
Bitmap zoomBitmap = zoomBitmap(mContentBitmap, 1/6f);
canvas.drawBitmap(zoomBitmap, 0, 0, mPaint);
canvas.restore();
}


/** 绘制海报Drawable */
private void onDrawDrawable(Canvas canvas) {
canvas.save();
canvas.translate(mLeftDrawable.getWidth(), 0);
Bitmap zoomBitmap = zoomBitmap(mContentBitmap , mLeftDrawable.getHeight()-3);
canvas.drawBitmap(zoomBitmap, 0, 0, mPaint);
canvas.restore();
}

/** 绘制左侧的图片 */
private void onDrawLeftBitmap(Canvas canvas) {
canvas.save();
canvas.drawBitmap(mLeftDrawable, 0, 0, mPaint);
canvas.restore();
}

/** 按照比例获取Bitmap */
private Bitmap zoomBitmap(Bitmap bitmap, int h){
int width = bitmap.getWidth();
int height = bitmap.getHeight();

float scaleHeight = ((float)h)/height;

Matrix matrix = new Matrix(); // 计算缩放比例
matrix.postScale(scaleHeight, scaleHeight);
return Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true);
}

/** 按照比例获取Bitmap */
private Bitmap zoomBitmap(Bitmap bitmap, float scale){
int width = bitmap.getWidth();
int height = bitmap.getHeight();
Matrix matrix = new Matrix(); // 计算缩放比例
matrix.postScale(scale, scale);
return Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true);
}

@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}

@Override
public void setAlpha(int alpha) {
mPaint.setAlpha(alpha);
}

@Override
public void setColorFilter(ColorFilter cf) {
mPaint.setColorFilter(cf);
}
}

我们现在自己定义了一个Drawable,我说一下思路,大体的思路就是对Bitmap做一定的操作,然后根据宽高去摆放,使用Bitmap去缩放小的海报,然后使用左侧的图片去盖住右侧的图片,主要就是计算方面,博主写的计算方法也有偏差,可以自己明确计算规则写自己的逻辑。

下来在我们的Activity当中使用我们定义的Drawable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MainActivity extends AppCompatActivity {

private ImageView iv;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
iv = (ImageView) findViewById(R.id.iv_imageView);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.album);
AlbumDrawable albumDrawable = new AlbumDrawable(bitmap, this);
iv.setImageDrawable(albumDrawable);
}
}

效果如下:

专辑盒子

以上就是自定义Drawable

Drawable的基础用法

1、前言

今天来跟大家聊一聊关于Android当中的绘图技巧,绘图技巧,不管是自定义控件,还是在我们去做自定义商业动画,都会常常的使用到,所以在这里和大家聊一聊关于绘图技巧的问题,如果那里有错误,希望各位大神指出。

2、正文

我们知道绘图的话,关于画布,画笔,这些Goolge提供的类有很大的关联,但是我们今天着重不是这些,而是从我们要给什么地方画图,这个是今天的重点,至于画图的细节我们以后会说的。

Android系统当中把我们所有的关于可以绘制的对象抽象出来,叫做Drawable对象,相信这个对象大家一定都不会陌生,在我们的实际开发当中我们基本天天都能遇到,今天就和大家聊一聊关于Drawable的各种用法。

我们想一想在我们的实际开发当中我们是怎么样去运用我们的Drawable的,我们会在res/drawable这个资源文件下面定义各种各样的Xml的文件,我们的Xml有不同的节点,其实我们这里的一个Xml的节点,就对应我们的Drawable的实现类,也就是说我们我们创建出来的Xml的文件,会被SDK自动的根据你的节点去创建相应的一个Drawable实现来,从而去实现我们所要的效果,在这里我们可以去类比一下我们的View,我们View当中的控件貌似也是这么定义的,在Xml当中去设定我们View控件所要的属性。但是我们的View可以在代码当中New出来,我们在这里试想一下,我们的Drawable也是可以在代码当中去new出来的,然后通过代码去给他去添加属性,答案是可以的,但是我们又想了想,我们new一个View有用么,我们一般不是new一个Button,一个ImageVeiw的么,我们可以去new一个Drawable的实现类,然后通过Drawable去进行管理,就像我们可以去用我们的View,ViewGroup去管理我们的控件和布局。

那么我们来看看Drawable的哪些节点对应的哪些实现类呢!然后会给大家详细的去做介绍。为了让各位看的更加的清楚,相信大家就非常的清楚了

  • selector : StateListDrawable
  • layer-list : LayerDrawable
  • transition : TransitionDrawable
  • color : ColorDrawable
  • shape : GradientDrawable
  • scale : ScaleDrawable
  • clip : ClipDrawable
  • rotate : RotateDrawable
  • animation-list : AnimationDrawable:
  • insert : InsetDrawable:
  • bitmap : BitmapDrawable
  • nine-patch : NinePatchDrawable:

1 selector StateListDrawable

这个叫做状态的选择器,首先我们需要明白一点,就是StateListDrawable里面维护了不止一个Drawable对象,就是一组Drawable对象。既然它叫做状态选择器,说白了就是可以去根据不同的状态去返回不同的Drawable对象,而这些状态是由系统去进行提供的,至于都有哪些状态,按下状态、选中状态、默认状态、禁用状态等等,如果想了解更加全面,请大家参考Google的API,我们的Button的背景一般都使用这个Drawable,还有就是TV的海报也是用它,基本我们去和用户做交互的控件的背景都会使用到它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<selector xmlns:android="http://schemas.android.com/apk/res/android">

<!-- state_pressed:代表用户按下状态 -->
<item android:state_pressed="true" android:drawable="@drawable/press"/>

<!-- 如果状态什么都不写,代表默认状态,就是什么都不触发的时候 -->
<item android:drawable="@drawable/normal"/>

<!-- 没有焦点时的背景图片 -->
<!--<item android:state_window_focused="false" android:drawable="@drawable/pic1" />-->
<!-- 非触摸模式下获得焦点并单击时的背景图片 -->
<!--<item android:state_focused="true" android:state_pressed="true" android:drawable= "@drawable/pic2" />-->
<!-- 触摸模式下单击时的背景图片-->
<!--<item android:state_focused="false" android:state_pressed="true" android:drawable="@drawable/pic3" />-->
<!--选中时的图片背景-->
<!--<item android:state_selected="true" android:drawable="@drawable/pic4" />-->
<!--获得焦点时的图片背景-->
<!--<item android:state_focused="true" android:drawable="@drawable/pic5" />-->
<!--设置是否响应事件,指所有事件-->
<!--<item android:android:state_enabled="true" android:drawable="@drawable/pic5" />-->

</selector>

在我们的Layout当中只需要引入一下我们刚刚用Xml定义的Drawable,就OK了

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/selector_bg" />

</RelativeLayout>

效果图如下,当我点击的按下的时候会发生改变,当我抬起的时候会变为原来的,这只是一种状态,还有别的状态,笔者这里就不一一列举了

StateListDrawable

2、level-list LevelListDrawable

这个我的理解是自定义选择器,首先在LevelListDrawable里面和我们StateListDrawable一样,维护了一组的Drawable的对象,同时维护了每一个Drawable要显示的Level范围,不同的是上述是通过系统发出的状态去做出一个处理去显示我们的Drawable对象,而这个是自己定义一个范围去匹配响应的Drawable去显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<level-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:drawable="@drawable/fm_vehicle_linking_1"
android:maxLevel="1"/>
<item
android:drawable="@drawable/fm_vehicle_linking_2"
android:maxLevel="2"/>
<item
android:drawable="@drawable/fm_vehicle_linking_3"
android:maxLevel="3"/>
<item
android:drawable="@drawable/fm_vehicle_linking_4"
android:maxLevel="4"/>
<item
android:drawable="@drawable/fm_vehicle_linking_5"
android:maxLevel="5"/>
<item
android:drawable="@drawable/fm_vehicle_linking_6"
android:maxLevel="6"/>
<item
android:drawable="@drawable/fm_vehicle_linking_7"
android:maxLevel="7"/>
<item
android:drawable="@drawable/fm_vehicle_linking_8"
android:maxLevel="8"/>
<item
android:drawable="@drawable/fm_vehicle_linking_9"
android:maxLevel="9"/>
<item
android:drawable="@drawable/fm_vehicle_linking_10"
android:maxLevel="10"/>
<item
android:drawable="@drawable/fm_vehicle_linking_11"
android:maxLevel="11"/>
<item
android:drawable="@drawable/fm_vehicle_linking_12"
android:maxLevel="12"/>
<item
android:drawable="@drawable/fm_vehicle_linking_13"
android:maxLevel="13"/>
<item
android:drawable="@drawable/fm_vehicle_linking_14"
android:maxLevel="14"/>
</level-list>

我们的Layout使用一个seekBar 和一个ImageView去展示我们的图片,相当于一个载体。没有什么难度,楼主在这里就不去贴图片了我们直接来看看我们的Activity的代码,在我们的seekBar上面增加一个监听器,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** 初始化View */
private void initView() {
final ImageView image = (ImageView) findViewById(R.id.iv);
final Drawable drawable = getResources().getDrawable(R.drawable.levellist);
SeekBar seekBar = (SeekBar) findViewById(R.id.seekBar);
seekBar.setMax(14);
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
drawable.setLevel(progress);
image.setImageDrawable(drawable);
}

@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}

@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
}

效果呢!就是如下这样啦

LevelListDrawable

3、layer-list LayerDrawable

这个是图层的Drawable,它同样是维护了一组的Drawable,但是会根据我们在XML当中定义的顺序去给它做一个顺序显示,列表的最后一个drawable绘制在最上层。每一个Drawable处于一个不同的图层,也许会出现重叠,交叉的现象,但是不会首到另外Drawable的影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">

<item android:drawable="@drawable/live_color_2"/>

<!-- android:top - Integer。与top的距离,单位像素
android:right - Integer。与right的距离,单位像素
android:bottom - Integer。与bottom的距离,单位像素
android:left - Integer。与left的距离,单位像素
-->

<!--
在默认的情况下,所有的drawable item都会缩放到合适的大小来适应视图。
因此,在一个layer-list中定义不同的位置可能会增加视图的尺寸和被自动缩放。
为了避免被缩放,可以再<item>节点里加上<bitmap>元素来指定一个drawable,
并且定义一些不会被拉伸的gravity属性,例如center。

<item>
<bitmap android:src="@drawable/image"
android:gravity="center" />
</item>

-->
<item android:drawable="@drawable/live_color_3"
android:top="30dip"
android:left="30dip"
android:right="30dip"
android:bottom="30dip"
/>

<item android:drawable="@drawable/live_color_4"
android:top="60dip"
android:left="60dip"
android:right="60dip"
android:bottom="60dip"
/>

</layer-list>

我们的Layout同第一个一样,直接去引用一下就OK

1
2
3
4
5
6
7
<ImageView
android:id="@+id/iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@drawable/layerlist"
/>

效果呢,如下,但是请注意上述注释里面所说的情况

LayerDrawable

4、transition TransitionDrawable

这是一个LayerDrawable子类,那么它涉及到的也将会是图层的知识,但是他只涉及到了两个图层的变化,就是把上面和下面的图层进行交替显示,通过改变他们的透明度,为维护了一个平滑的透明度变化的动画效果,开启动画调用startTransition()。可以反向开启调用 reverseTransition()。

1
2
3
4
5
6
<transition xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 一个青色的小圆 -->
<item android:drawable="@drawable/live_color_2"/>
<!-- 一个黄色的小圆 -->
 <item android:drawable="@drawable/live_color_4"/>
</transition>

Activity里面去设置一下这个Drawable,比如什么时候开启过度透明度动画,设置动画时长之类的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ImageView image = (ImageView) findViewById(R.id.iv);

Resources res = getResources();
TransitionDrawable transition = (TransitionDrawable)res.getDrawable(R.drawable.transition);

image.setImageDrawable(transition);
transition.startTransition(2000);
// 这个是反向开启动画
// transition.reverseTransition(2000);
}

效果如下:

TransitionDrawable

5、color ColorDrawable

这个Drawable对象,描述的不像上面的,它描述一个单个Drawable,在其内部定义颜色属性,当要绘制的时候,会读取到里面的颜色值,把这个颜色设置给将要绘制的Paint对象上面,注意这个Drawable一旦绘制,就不好修改了。

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

颜色是怎么归类的 6位 ARGB 这个楼主就默认大家都会了,也就不浪费时间了

6、shape GradientDrawable

这个Drawable大家可以理解为上述ColorDrawable的加强版,在这个Drawable中维护了很多属性,好方便我们来使用,使用这个Drawable,我们可以实现是一个区域内的颜色渐变,其中主要分为线性渐变,发散渐变,平铺渐变,下面我们来研究一下,在它的内部定义了什么属性来达到我们实现炫酷的效果

  • 1、size:定义区域的大小
  • 2、gradient:设置区域背景的渐变效果
  • 3、solid:设置区域的背景颜色,如果设置了solid会覆盖gradient的效果
  • 4、stroke:设置区域的边框效果
  • 5、padding:设置区域的内边距,

这个Drawable很常用,同时也很重要,所以楼主会详细一点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<?xml version="1.0" encoding="utf-8"?>
<!--
rectangle 长方形 /默认
oval 椭圆
line 线
ring 环形
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">

<!-- 用来描述渐变的类型,线性渐变,发散渐变,平铺渐变,还有渐变开始颜色,渐变结束颜色 -->
<!--
angle 是颜色变换的角度, 默认是0, 取值必须是45的 倍数. 0: 是颜色从左边到右边, 90: 是颜色从底部到顶部,
startColor centerColor endColor 一起使用: 开始的颜色, 中间的颜色, 结束的颜色
centerX centerY是指定位置坐标, 取值是0.0f ~ 1.0f 之间, 例如: android:centerX="0.5f" 表示X方向的中间位置
gradientRadius 和android:type="radial"一起连用,

android:type:
radial 平铺渐变, 例如: 从一个圆中心到圆的边缘变化
sweep 发散渐变, 类似雷达扫描的那种图形
linear 线性渐变, 就是颜色从左往右, 从下往上
-->
<gradient
android:startColor="#FF0000"
android:centerColor="#00FF00"
android:endColor="#0000FF"
android:type="linear"/>


<!-- 用来定义显示区域的大小 -->
<!--
这里一般不用设置, 它的优先级没有控件的优先级大,
他指定控件的宽高就好, shape图形会随控件拉伸
-->
<!--
<size
android:width="100dp"
android:height="100dp"
/>
-->

<!-- 用来定义圆角 -->
<!--
bottomLeftRadius 左下角
bottomRightRadius 右下角
topLeftRadius 左上角
topRightRadius 右上角

radius 是四个角, 设置了这个就不需要设置上面的四个了, PS:它的优先级比较低, 会被其他参数覆盖
-->
<corners
android:bottomLeftRadius="10dp"
android:bottomRightRadius="10dp"
android:topLeftRadius="10dp"
android:topRightRadius="10dp"/>

<!-- padding标签: 这里的padding是控件中间内容与shape图形图片的距离 -->
<padding
android:bottom="5dip"
android:left="5dip"
android:right="5dip"
android:top="15dip"/>

<!--solid标签: shape图形背景色, 这个和上面的gradient标签会互斥, 一个是设置背景色, 一个是设置渐变色, 上述也说过这个问题 -->
<!--
<solid
android:color="@android:color/white"/>
-->

<!-- 边框的颜色宽度的设置 -->
<!--
width 边框的宽度
color 边框的颜色

边框的样式:
dashGap 虚线中空格的长度
dashWidth 虚线中实线的长度
-->

<stroke
android:width="5dip"
android:color="#0000FF"
android:dashGap="2dip"
android:dashWidth="1dip"/>

</shape>

效果如下:

GradientDrawable

7、scale ScaleDrawable

这个Drawable主要就是对原有的Drawable去进行一定百分比的缩放,我们可以选取不同的缩放中心对这个Drawable进行控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>

<!--
drawable Drawable 资源。必须的。引用一个drawable资源。
scaleHeight 缩放的高度,以百分比的方式表示drawable的缩放。形式例如:100%,12.5%。
scaleWidth 缩放的宽度,以百分比的方式表示drawable的缩放。形式例如:100%,12.5%。
scaleGravity 指定缩放后的gravity的位置。必须是下面的一个或多个值(多个值之间用”|”分隔)
-->

<scale xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/press"
android:scaleGravity="center_vertical|center_horizontal"
android:scaleHeight="50%"
android:scaleWidth="50%" />

准备好以后,我们来运行一下,但是我们发现并没有显示图片,查看ScaleDrawable的源码,发现draw()方法有判断getLevel() != 0才绘制。而我们在没有setLevel()时,默认getLevel()都是0。
那我们先设置10000吧,我们再次运行,发现没有按照我们50%的比例进行缩放,然后我们去看官方文档

  • A Drawable that changes the size of another Drawable based on its current level value,

我们发现ScaleDrawable的缩放,并不是自动的建立在原有Drawable尺寸的基础上的。而是,需要给原有的Drawable指定一个Level,然后ScaleDrawable是在这个Level的基础上进行缩放的!
注意:设置的值是缩小的比例。也就是说,设置0.5,意为缩小50%!!而不是原始大小的50%
那么我们在我们的Activity里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

ScaleDrawable drawable = (ScaleDrawable) getResources().getDrawable(R.drawable.scale);

ImageView image = (ImageView) findViewById(R.id.iv);
image.setImageDrawable(drawable);
//在这个地方设置Level
 image.setImageLevel(1);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

ScaleDrawable drawable = (ScaleDrawable) getResources().getDrawable(R.drawable.scale);

ImageView image = (ImageView) findViewById(R.id.iv);
image.setImageDrawable(drawable);
//在这个地方设置Level
 image.setImageLevel(1);
}

效果呢 如下:

ScaleDrawable

8、clip ClipDrawable

这个Drawable主要就是对原有的Drawable进行一个裁剪,我们会有gravity选择在父容器的对齐方式,然后通过Drawable提供的level去设置我们去裁剪的大小,裁剪是从0到10000,0代表完全不显示,10000代表完全显示。

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<!--
drawable:所需要的Drawable资源
clipOrientation:裁剪的方向,垂直,水平
gravity:对齐方式
-->
<clip xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/press"
android:clipOrientation="horizontal"
android:gravity="center"/>

定义出了我们自已要的Drawable,然后在我们的Activity进行应用就OK了

下面看Activity的代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}

/** 初始化View */
private void initView() {
final ImageView image = (ImageView) findViewById(R.id.iv);
final ClipDrawable drawable = (ClipDrawable) getResources().getDrawable(R.drawable.clip);
SeekBar seekBar = (SeekBar) findViewById(R.id.seekBar);
seekBar.setMax(10000);
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
drawable.setLevel(progress);
image.setImageDrawable(drawable);
}

@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}

@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
}

下面贴出效果图

ClipDrawable

9、rotate RotateDrawable

这个Drawable主要就是对原有的Drawable进行一定角度的旋转,基于当前的level,进行旋转的drawable。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<!--drawable: Drawable 资源。必须的。引用一个drawable资源。
visible:Boolean。是否可见。
fromDegrees:整形。 从多少的角度开始旋转
toDegrees:整形。 到多少的角度结束旋转
pivotX:百分比。 旋转的中心在图片X轴的百分比
pivotY:百分比。 旋转的中心在图片Y轴的百分比-->

<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/press"
android:visible="true"
android:fromDegrees="0"
android:toDegrees="360"
android:pivotX="50%"
android:pivotY="50%">
</rotate>

在这里我们对旋转度数做一个详细的介绍,

图片将从0到360进行旋转。level值为10000,也就是说level每加1000,即顺时针旋转360/10000*1000=36度。
可以根据显示看出来效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}

/** 初始化View */
private void initView() {
final ImageView image = (ImageView) findViewById(R.id.iv);
final RotateDrawable drawable = (RotateDrawable) getResources().getDrawable(R.drawable.rotate);
SeekBar seekBar = (SeekBar) findViewById(R.id.seekBar);
seekBar.setMax(10000);
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
drawable.setLevel(progress);
image.setImageDrawable(drawable);
}

@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}

@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
}

效果呢,如下:

RotateDrawable

10、animation-list AnimationDrawable

这个Drawable里面维护了一组Drawable,通过一定的顺序,一定的显示时长去做一个Drawable的逐个显示,那么这么就会形成一个动画,也就是说每一个Drawable就是这个动画当中的一个帧,你可以通过oneshot属性来控制该动画播放完毕以后是否重新播放,主动调用AnimationDrawable的start播放动画,关于AnimationDrawable这个动画大家可以去看一下我的关于动画专题的博客。

11、inset InsetDrawable

这个Drawable主要是说可以把一个Drawable插入到另外一个Drawable的内部,并且在内部留一些间距

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!--
drawable: Drawable 资源。必须的。引用一个drawable资源。
insetBottom:整形。 底边距的大小
insetLeft:整形。 左边距的大小
insetRight:整形。 右边距的大小
insetTop:整形。 上边距的大小
-->
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/press"
android:insetBottom="30dp"
android:insetLeft="30dp"
android:insetRight="30dp"
android:insetTop="30dp" >
</inset>

layout:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:gravity="center">

<ImageView
android:id="@+id/iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="#aaa"
android:src="@drawable/insert"
/>
</RelativeLayout>

效果如下

InsetDrawable

12、bitmap BitmapDrawable

这个Drawable主要是对Bitmap的一种封装,可以设置它包装的bitmap在BitmapDrawable区域内的绘制方式,如平铺填充、拉伸填充或者保持图片原始大小,也可以在BitmapDrawable区域内部使用gravity指定的对齐方式。
BitmapDrawable就是封装了一个位图。以文件的方式,封装一个原始的位图。以Xml方式,可以对原始的位图进行一系列的处理,比如说抗锯齿,拉伸,对齐等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="utf-8"?>
<!--

src(Drawable) resource:必需。 引用一个drawableResource.
antialias (Boolean): 是否开启抗锯齿。
dither (Boolean): 如果位图与屏幕的像素配置不同时,是否允许抖动.(例如:一个位图的像素设置是 ARGB 8888,但屏幕的设置是RGB 565)
filter (Boolean): 是否允许对位图进行滤波。对位图进行收缩或者延展使用滤波可以获得平滑的外观效果。
gravity: 定义位图的重力(gravity),如果位图小于其容器,使用重力指明在何处绘制
tileMode:
"disabled" :默认值,什么也没有
"clamp" :复制图片边缘的颜色来填充容器剩下的空白部分,比如引入的图片如果是白色的边缘,那么图片所在的容器里除了图片,剩下的空间都会被填充成白色
"repeat" :平铺
"mirror":镜面
-->
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:alpha="1"
android:antialias="true"
android:dither="true"
android:filter="true"
android:gravity="clip_vertical"
android:src="@drawable/press"
android:tileMode="disabled"
/>

layout:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
>

<ImageView
android:id="@+id/iv"
android:layout_width="400dip"
android:layout_height="400dip"
android:src="@drawable/bitmap"
/>

</RelativeLayout>

效果:

BitmapDrawable

13、nine-patch

NinePatchDrawable:这个就是我们知道的点九图片,也就是.9.png,会根据内容的大小对其内容进行拉伸,注意:但是不能压缩,这也就是我们看到的切图为什么一开始都会做得很小,

1
2
3
4
<?xml version="1.0" encoding="utf-8"?>
<nine-patch xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@drawable/list_tag_clear"
android:dither="true"/>

我们要明白src对应的应该是一个.9图片,
dither 是否图像抖动处理,当每个颜色值以低于8位表示时,对应图像做抖动处理可以实现在可显示颜色总数比较低(比如256色)时还保持较好的显示效果。

layout:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
>

<Button
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="assssssssssssssssssssssssssssssssssssssssss"
android:background="@drawable/nine" />

</RelativeLayout>

注意:有可能会出Error:java.lang.RuntimeException: Some file crunching failed, see logs for details

意思就是说

  • 1.构建Gradle的时候,Gradle会去检查一下是否修改过文件的后缀名;
  • 2.一般大多数是出现在图片上,.jpg修改成了.png就会出现这个问题;
  • 3.9patch图片也可能出现这个问题。

很明显我们可能是第三种情况

数据结构表(ADT)解析(下)

1、前言

这部分的内容是根据之前的ADT讲解所遗留下来的内容,如果有什么不太明确,可以看之前的博客。

2、LinkedList的简单实现。

ArrayList底层是由数组实现的,而LinkedList底层是通过链表实现的,但是在Java当中是没有链表这种数据结构的。所以我们要通过自定义来进行。

2.1、分析

我们上述已经知道我们需要通过自定义底层节点,来实现双向链表,所以定义一个Node< T >是必须的,关于链我们直接通过Java当中的索引来做,我们还需要对当前双向链表进行简单的改造,通过标记节点来完成。还需要对该集合的操作进行定义,在这里提供出三个核心方法,其与的方法都是建立在这三个核心方法之上的。最后为了可以通过Iterable进行遍历,提供一个LinkedListIterator。我们可以把我们要完成的在下方总结一下:

  • 自定义Node< T >。
  • 添加头节点和尾节点。
  • 对外提供操作。
  • 对外提供LinkedListIterator 好进行遍历。

2.2、节点和链

我们知道一个节点,应该包含有三个部分,第一个部分应该是指上一个节点的索引,第二部分应该是该节点所含有的数据,第三部分应该是指向下一个节点的索引。

note的内容

具体实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 节点类,用来表示链表当中的一个节点。
*
* @author pengchengliu
* @param <AnyType>
*/
//TODO : 问题一:为什么选择是private。
//TODO : 问题二:为什么选择是static。
private static class Node<AnyType>{

//TODO :为什么内部属性选用public 。
public AnyType data ;
public Node<AnyType> prev ;
public Node<AnyType> next ;

public Node(AnyType data, Node<AnyType> prev, Node<AnyType> next){
this.data = data ;
this.prev = prev ;
this.next = next ;
}
}

在上述代码当中,笔者提到了三个问题。这这里简单的说一下。

第一个问题:为什么选择是private。

答案 : 应该这个内部类没有必要暴露给外部,外部只要默默的使用该类就Ok,不需要知道内部实现细节,注意这里的外部是指使用LinkedList的外部的类,而不是LinkedList。

第二个问题:为什么选择是static。

答案:这里应该是静态内部类和内部类的区别。想深入理解一下的看客可以去输入了解一下。这里笔者简单的说一下,Node这个类是独立于LinkedList的,也就是Node这个类没有使用到LinkedList这个类的实例,并且Node不依赖于LinkedList实例被实例化。所以选择static了。

第三个问题:为什么内部属性选用public

答案:这个问题好回答,既然整个类都是private,内部是用public也就无所谓了。

2.3、标记节点

了解了上述Node类以后,这里特别说明一下有两个比较特殊的位置,就是第一个元素和最后一个元素,我们第一个元素的第一部分应该是null,而最后一个节点的第三部分应该是null,当我们检测到了最后一个节点的第三部分是null,则代表遍历结束,这是基本的链表结构,但是为了省去不必要的麻烦,我们使用标记节点。代替第一个元素和最后一个元素。为什么选择标记节点。

标记节点

需要注意的是由于第一个元素和最后一个元素都比较特殊,所以通过标记节点头节点和尾节点。

为什么使用标记节点呢?

如果我们不使用标记节点,我们会存在特殊情况,比如删除或者添加第一个元素,这样就会存在一种可能,那就是我们需要改变第一个元素的位置,而如果有标记节点的话,第一个元素的位置和最后一个元素的位置就是一定的,我们规避了这一种特殊情况。

2.4、主要操作

我们知道数据结构重要的地方有两点,第一个比较重要的地方就是集合内的元素,而第二个重要的地方就是操作。

2.4.1、清空集合

因为我们有标记节点,所以我们清空集合只需要,让头节点指向尾节点,尾节点指向头节点,然后让size为0即可。效果图如下所示

head节点

代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 清空当前的容器
*/
public void clear() {
beginMarker = new Node<AnyType>(null, null, null);
endMarker = new Node<AnyType>(null, beginMarker, null);
beginMarker.next = endMarker ;

// 重置计数器。
theSize = 0 ;
modCount++ ;
}

2.4.2、插入元素

在固定的位置插入一个元素,也就是说,改变该位置上面前后节点的链即可,具体的步骤如下图所示:

插入元素

代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
/**
* 添加一个元素,将元素item添加到node之前。此方法为其他方法的服务方法,所以不需要暴露给外部类调用
*/
private void addBefore(Node<AnyType> node, AnyType item){
Node<AnyType> itemNode = new Node<AnyType>(item, node.prev, node);
itemNode.prev.next = itemNode ;
node.prev = itemNode ;
//更新计数器。
theSize ++ ;
modCount ++ ;
}

2.4.3、移除元素

这个操作方法和上述插入元素,没有什么区别,也是改变前后节点的链的问题。

移除元素

代码实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 移除一个元素,将oldNode在原有的集合当前删除。
* @param oldNode
* @return
*/
private AnyType remove(Node<AnyType> oldNode){
oldNode.next.prev = oldNode.prev ;
oldNode.prev.next = oldNode.next ;
//更新计数器
theSize-- ;
modCount++ ;
return oldNode.data ;
}

2.4.4、得到节点

通过下标得到相应的Node,具体的代码实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 得到一个元素,根据index下表值,得到一个元素
* @param index 下表值
* @return 得到的元素。
*/
private Node<AnyType> getNode(int index){
Node<AnyType> tempNode ;
//下标合法性验证
if(index < 0 || index > this.size()){
throw new IndexOutOfBoundsException();
}
//由于是双向列表,所以根据index的值不同在两端获取。使得效率更加的高效,O(N/2)的时间复杂度。
if(index < this.size() / 2){
//从开头开始找,获取第一个元素
tempNode = beginMarker.next ;
for(int i = 0; i < index; i ++){
tempNode = tempNode.next ;
}
} else {
//从结尾开始找,获取最后一个元素。
tempNode = endMarker.prev ;
for(int i = this.size(); i > index; i--){
tempNode = tempNode.prev ;
}
}
return tempNode ;
}

2.5、LinkedListIterator的实现。

代码实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/***
* LinkedList的迭代器。
* @author pengchengliu
*
*/
private class LinkedListIterator implements Iterator<AnyType>{

//当前的位置,默认指向第一个元素。
private Node<AnyType> current = beginMarker.next ;
private int expectedModCount = modCount ;
private boolean okToRemove = false ;

@Override
public boolean hasNext() {
return current != endMarker;
}

@Override
public AnyType next() {
//TODO : 为什么需要做这么一步判断呢?
if(modCount == expectedModCount){
throw new ConcurrentModificationException();
}

//TODO :为什么需要这个判断
if(!hasNext()){
throw new NoSuchElementException() ;
}

AnyType data = current.data;
current = current.next ;
okToRemove = true ;
return data;
}

@Override
public void remove() {
if(modCount != expectedModCount){
throw new ConcurrentModificationException();
}

//TODO :为什么需要这个判断
if(!okToRemove){
throw new IllegalStateException();
}

MyLinkedList.this.remove(current.prev);
okToRemove = false ;
expectedModCount++ ;
}
}

在上述代码当中笔者提出了三个问题,在这里笔者简单的说一下。

第一个问题:

1
2
3
if(modCount == expectedModCount){
throw new ConcurrentModificationException();
}

这个判断存在的必要。在遍历阶段,防止对表结构进行改变,

第二个问题:

1
2
3
if(!hasNext()){
throw new NoSuchElementException() ;
}

也就是我们常说的越界吧。

第三个问题

1
2
3
if(!okToRemove){
throw new IllegalStateException();
}

就是没有使用遍历就是调用remove方法。

2.6总体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
package list;

import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.NoSuchElementException;

/**
* 自定义LinkedList .
* 实现选择是选用头节点和尾节点。可以极大简便很多的特殊情况,例如:删除头节点。额外的调整整个链表的头节点。
* @author pengchengliu
*
*/
public class MyLinkedList<AnyType> implements Iterable<AnyType>{

private Node<AnyType> beginMarker ;
private Node<AnyType> endMarker ;

//TODO :为什么会存在modCount计数器呢?
private int theSize ;
private int modCount ;

public MyLinkedList(){
this.clear();
}

/**
* 清空当前的容器
*/
public void clear() {
beginMarker = new Node<AnyType>(null, null, null);
endMarker = new Node<AnyType>(null, beginMarker, null);
beginMarker.next = endMarker ;

// 重置计数器。
theSize = 0 ;
modCount++ ;
}

/**
* 获取当前容器的长度。
* @return 当前容器的长度 。
*/
public int size(){
return theSize ;
}

/**
* 判断当前集合是否为空
* @return 当前容器是否为空
*/
public boolean isEmpty(){
return this.size() == 0 ;
}

/**
* 添加一个节点,到整个集合的最后面。
* @param node
* @return
*/
public boolean add(AnyType node){
add(size(), node);
return true ;
}

/**
* 添加一个一个节点,在index的位置上面添加一个元素。
* @param index :
* @param newVaule :
*/
public void add(int index, AnyType newVaule){
addBefore(getNode(index), newVaule);
}

/**
* 得到集合当中的一个元素,通过索引得到集合当中的一个元素。
* @param index
* @return
*/
public AnyType get(int index){
return getNode(index).data;
}

/**
* 替换一个元素,替换位置在index上面的元素为newValue
* @param index 原有的位置
* @param newValue 新元素的值
* @return 以前index位置的元素值。
*/
public AnyType set(int index, AnyType newValue){
Node<AnyType> node = getNode(index);
AnyType oldValue = node.data;
node.data = newValue ;
return oldValue ;
}

/**
* 移除一个元素,移除index位置的元素并且把给元素返回
* @param index : 指定的位置。
* @return : 要移除的元素。
*/
public AnyType remove(int index){
return remove(getNode(index));
}

/**
* 添加一个元素,将元素item添加到node之前。此方法为其他方法的服务方法,所以不需要暴露给外部类调用
*/
private void addBefore(Node<AnyType> node, AnyType item){
Node<AnyType> itemNode = new Node<AnyType>(item, node.prev, node);
itemNode.prev.next = itemNode ;
node.prev = itemNode ;
//更新计数器。
theSize ++ ;
modCount ++ ;
}

/**
* 移除一个元素,将oldNode在原有的集合当前删除。
* @param oldNode
* @return
*/
private AnyType remove(Node<AnyType> oldNode){
oldNode.next.prev = oldNode.prev ;
oldNode.prev.next = oldNode.next ;
//更新计数器
theSize-- ;
modCount++ ;
return oldNode.data ;
}

/**
* 得到一个元素,根据index下表值,得到一个元素
* @param index 下表值
* @return 得到的元素。
*/
private Node<AnyType> getNode(int index){
Node<AnyType> tempNode ;
//下标合法性验证
if(index < 0 || index > this.size()){
throw new IndexOutOfBoundsException();
}
//由于是双向列表,所以根据index的值不同在两端获取。使得效率更加的高效,O(N/2)的时间复杂度。
if(index < this.size() / 2){
//从开头开始找,获取第一个元素
tempNode = beginMarker.next ;
for(int i = 0; i < index; i ++){
tempNode = tempNode.next ;
}
} else {
//从结尾开始找,获取最后一个元素。
tempNode = endMarker.prev ;
for(int i = this.size(); i > index; i--){
tempNode = tempNode.prev ;
}
}
return tempNode ;
}

@Override
public Iterator<AnyType> iterator() {
return new LinkedListIterator();
}

/***
* LinkedList的迭代器。
* @author pengchengliu
*
*/
private class LinkedListIterator implements Iterator<AnyType>{

//当前的位置,默认指向第一个元素。
//TODO : 下面两个变量存在的意义。
private Node<AnyType> current = beginMarker.next ;
private int expectedModCount = modCount ;
private boolean okToRemove = false ;

@Override
public boolean hasNext() {
return current != endMarker;
}

@Override
public AnyType next() {
//TODO : 为什么需要做这么一步判断呢?
if(modCount == expectedModCount){
throw new ConcurrentModificationException();
}

//TODO :为什么需要这个判断
if(!hasNext()){
throw new NoSuchElementException() ;
}

AnyType data = current.data;
current = current.next ;
okToRemove = true ;
return data;
}

@Override
public void remove() {
if(modCount != expectedModCount){
throw new ConcurrentModificationException();
}

//TODO :为什么需要这个判断
if(!okToRemove){
throw new IllegalStateException();
}

MyLinkedList.this.remove(current.prev);
okToRemove = false ;
expectedModCount++ ;
}
}


/**
* 节点类,用来表示链表当中的一个节点。
*
* @author pengchengliu
* @param <AnyType>
*/
//TODO : 问题一:为什么选择是private。
//TODO : 问题二:为什么选择是static。
private static class Node<AnyType>{

//TODO :为什么内部属性选用public 。
private AnyType data ;
public Node<AnyType> prev ;
public Node<AnyType> next ;

public Node(AnyType data, Node<AnyType> prev, Node<AnyType> next){
this.data = data ;
this.prev = prev ;
this.next = next ;
}
}

}

##3、两者实现方式在效率上面的差别

我们知道了ArrayList适合读,而插入和删除还是选择LinkedList,具体的要看两者之间的差别,还是需要通过一个例子来看。

例子:删除一个List当中为偶数的元素。

1
2
3
4
5
6
7
8
public static void testDome(List<Integer> list){
Iterator<Integer> its = list.iterator();
while(its.hasNext()){
if(itr.next() % 2 == 0){
itr.remove();
}
}
}

如果是LinkedList,40万条数据,大概时间是0.031秒,而如果是ArrayList大概是2.5分钟,而如果是80万数据的话,LinkedList大概是0.062秒,ArrayList则是10分钟。

数据结构表(ADT)解析(上)

1、前言

自工作以来,越来越觉得数据结构的重要性,类似于网络上面拉去下来的数据进行分类,还是自定义View画图的x,y还是滑动的距离。滑动距离的判断种种。感觉自己很是欠缺火候,所以开始恶补数据结构、算法的知识,让自己可以提高。

之前在知乎上面看了一个帖子,在讨论数据结构的重要性,有一句话说的很好,也是笔者开始写一系列数据结构算法的动力,在这里送给给位看客,

  • 对于一个程序员来说,算法的高度决定程序员的高度,而一个程序员将来成就的高度取决于算法的高度

看完以后热情满满,打算从最基本的开始学习,如果各位看客发现在什么地方说的不对,希望指出。谢谢各位。

2、抽象数据类型(ADT)

学习数据结构之前,我们需要明白什么是抽象数据类型,

抽象数据类型(abstract data type):是带有一组操作的一些对象的集合,抽象数据类型是数学方面的抽象。在抽象数据类型定义中没有地方提到关于这组操作时如何实现的任何解释。这里有两个比较重要的地方,首先第一个是一组操作。然后是一些对象的集合,

在这里笔者的理解是有一个装篮球的箱子,箱子上面有各种各样的按钮,每一个按钮都可以操作箱子里面篮球,比如让箱子将所有的篮球全部吐出来,等等,这里这个装篮球的箱子以及里面的篮球和按钮就是抽象数据类型。

常见的抽象数据类型有表、栈、队列、树、图、散列 等,将在后面篇章详细讨论,这里只对表进行说明。

3、表(ADT)

3.1 表当中的元素

我们将处理 $A_{0}$、 $A_{1}$、 $A_{2}$、 $A_{3}$、 $A_{4}$、····· $A_{N-1}$的一般的表。我们就说这个表的长度是N。我们将长度为0的表叫做空表(empty list)

对于一个不为空的表,我们说 $A_{i}$后继 $A_{i-1}$(i < N, i > 0)并且$A_{i-1}$ 前驱 $A_{i}$。$A_{0}$为表当中的第一个元素,没有前驱元,而$A_{N-1}$为表当中的最后一个元素,没有后继元。

3.2 表当中的操作

对于每种ADT并不存在什么固定的操作,而为每种ADT提供合理的操作,一般取决于程序的设计者。对于表这种ADT,笔者在这里提出比较常用的操作。为 :

1
2
3
4
5
6
7
8
9
10
11
12
printList : 按照表的顺序打印各个位置的对象,如是对象打印地址。
makeEmpty : 将该表置空

find : 返回某一项第一次返回的位置。
findValueForIndex : 通过index查找value
findIndexForValue : 通过value查找index

insert : 在表的末尾插入
insertForIndex : 在指定位置插入

remove : 通过value删除元素
remmoveForIndex : 通过index删除元素

4、表的实现

4.1、数组实现简单的表。

我们把通过数组实现的表接口叫做顺序表。顺序表的所有操作都可以通过使用数组来实现,但是这里会涉及到一个问题就是,就是数组应该是固定长度的,我们的表应该让上层用户感觉不到容量,及当用户想要去添加的时候,我们可以让用户无限添加。这就是数组实现表里面的一个通用问题,这里的解决方式是让数组扩容,什么是扩容呢? 就是通过大于旧数组的长度去定义一个新的数组,然后把旧的数组里面的所有元素全部拷贝到新数组。

关于新数组的长度应该是多少呢?笔者在这里给出

  • 新数组长度 = 旧数组的长度 +( 旧数组的长度 * 2)
1
2
3
4
5
6
7
8
9
10
11
12
13
int [] arr = new int[10];
···
···
//开始扩容
int newSize = arr.length + ( arr.length * 2 );
int[] newArray = new int[newSize];

//扩容完成,下面开始拷贝
for(int k = 0; k < arr.length; k++){
newArray[k] = arr[k]
}
//归还引用
arr = newArray ;

这种数据结构的优点在于,查找,基本消耗的时间都是线性时间,而上述提供的findValueForIndex操作,常数时间就可以很轻松的完成。

缺点就是,如果我们需要对顺序表当中的表结构(数据)进行改变呢?我们不妨设想一下,如果我们需要在第2位插入一个元素,那么就代表着,表里面的原先2位置的元素下表变化称为第3,而第3变为第4,以此类推,直到表中元素结束。删除也是同样的道理。当然如果我们在最后一位插入,我们的时间也是常数值。在这里我们考虑最坏情况,那就是在第一位插入活着删除,那么我们的时间就是$o(N)$。

总结:就是如果对于该表数据为只读权限(完全访问),而没有写权限,那么这种表结构的实现无一就是完美的,但是对插入删除操作比较多的话。笔者在这里不推荐使用这种表,而推荐下述的这种表结构(链表),后续会比较两表的效率问题,

4.2、简单链表

为了解决上述问题,人们开始思考,如果我们将表不进行连续存储,可以避免插入和删除的线性开销,下图为链表的一般想法

简单链表

上图可知,链表是一系列节点组成的,我们称之为Node,这些Node在内存当中是不连续的。每一个Node都有一条链,我们称之为next链,直向下一个Node,最后一个元素的next链为空。

我们如果打印全部,或者便利的话,通过第一个元素找到第二个元素以此类推,直到找到最后一个元素。时间是线性的,这个数组实现是一样的,不过其中的常数可能会比使用数组要大,findValueForIndex操作确实没有数组效率高,需要通过第一个开始找,直到遍历到index,所以时间复杂度为 $O(i)$ 。

如果是删除和插入操作,我们只需要修改两个链也就是引用,我们就可以完成插入操作和删除操作,不需要移动项,所以对比上述数组实现确实是简便了一些。下图为删除和插入的实现链表

简单链表

这里如果要插入的话,就把A3指向A4的引用变为A3指向x,然后把x的指向变为A4即可。不需要移动项,所以在这里时间复杂度是常数时间。

简单链表

如果要删除A4节点的话,这里我们直接把A3的链指向A5,然后把A4原本指向A5的链制空。就完成了A4的删除。时间复杂度也是常数时间。

使用上述链表的话,删除最后一个节点,我们得先找到指向最后一个节点的项,但是在上述链表当中,每一个节点不会提供前驱节点的任何信息。所以对上述链表进行改造,得出双向链表,即一个节点会提供下一个节点和上一个节点的信息,结构图如下所示。

简单链表


5、Java中的表(ADT)List

Java语言中,提供了一些普通数据结构的实现。该语言的这一部分叫做Collection API。表(ADT)是在Collection中实现的数据结构之一。

Java当中由java.util包中的List接口指定。根据上述表的实现思想,Java当中由ArrayList类提供了List ADT的一种可增长数组的实现。使用该数据结构的优点在于,get和set的调用花费常数时间。但是插入操作和删除操作代价昂贵(在于移动其与项)。

Java当中由LinkedList类提供了List ADT的双向链表的实现,优点在于新项的插入和删除开销比较小,这里假设变动项的位置是已知的,这意味着,在表的前端和末尾都是常数时间。使用LinkedList的缺点在于get的调用是昂贵的,除非调用的项非常的接近端点

5.1 Java当中Iterable接口

实现了Iterable接口的那些类,可以拥有增强For的循环,该循环施于这些类之上从而观察它们的所有项。

Collection接口当中扩展了Iterable接口。实现Iterable接口的类,必须提供了一个名叫iterator的方法。该方法返回一个实现了Iterator接口类。下面我们看看Iterator接口是干嘛的?

1
2
3
4
5
6
7
8
9
10
public interface Iterator<AnyType>{
// 是否还有下一项?
boolean hasNext();

// 得到当前的项
AnyType next();

// 移除当前的项
void remove();
}

具体每个方法是干嘛的,上述注释说的很清楚了。Iterator接口的思路就是,通过iterator方法,每个集合都可以创建一个实现Iterator接口的对象,并且将当前的位置概念在对象内部保存下来。

1、 使用iterator进行遍历。

获取当前对象的iterator,然后调用hasNext()方法,next()方法进行遍历。具体模拟入下所示:

1
2
3
4
5
6
7
···
Iterator<AnyType> iterator = collection.iterator();
while(iterator.hasNext()){
AnyType item = iterator.next();
System.out.println(item.toString());
}
···

在Iterator接口里面我们看到一个remove方法,这个方法主要是删除由next方法返回的最新的项,在Collection当中同样也提供了remove方法,两者到底有什么不同呢?

这里笔者推荐的是如果可以使用Iterator接口里面的remove,尽量使用。为什么这么说呢?原因有以下两点

1、因为我们知道collection当中的remove是先要找到删除的项,然后进行删除。而Iterator接口里面的remove是已经找到项的,在这里会节省一部分的时间。

2、如果直接使用Iterator的时候。如果对正在被遍历的集合进行结构上面的改变,那么迭代器将不再合法会跑出ConcurrentModificationException异常。如果直接使用Iterator内部的remove则不会抛出异常。

5.2 ArrayList的实现

笔者在这里模拟ArrayList的实现,具体实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
/**
* 自定义ArrayList的实现方式
* @author 刘酸酸
*
* @param <AnyType>
*/
public class MyArrayList<AnyType> implements Iterable<AnyType>{

/** 默认扩容长度 */
private static final int DEFAULT_CAPACITY = 10 ;

/** 当前数据结构的长度 */
private int mSize ;

/** 内部使用的数据来存储数据 */
private AnyType[] mArray ;

public MyArrayList() {
initMyArray();
}

/** 初始化数组 */
private void initMyArray() {
mSize = 0 ;
ensureCapacity(DEFAULT_CAPACITY) ;
}

/** 返回当前元素的长度 */
public int size(){
return mSize ;
}

/** 当前表是否为空 */
public boolean isEmpty(){
return size() == 0 ;
}

/** 根据下表返回元素 */
public AnyType get(int index){
checkIndex(index);
return mArray[index];
}

/** 在index的位置替换newValue,然后返回index位置以前的Vaule */
public AnyType set(int index, AnyType newValue){
checkIndex(index);
AnyType oldValue = mArray[index];
mArray[index] = newValue ;
return oldValue ;
}

/** 默认插入,在当前表的最后一位进行插入 */
public boolean add (AnyType value){
add(size(),value);
return true;
}

/** 移除index的元素并且把给元素返回 */
public AnyType remove(int index){
AnyType old = mArray[index] ;
for(int i = index; i < size() - 1; i++){
mArray[i] = mArray[i + 1] ;
}
mSize -- ;
return old ;
}

/**给index的位置插入一个新的元素,index以后的所有元素后移一位 */
public void add(int index, AnyType value){
if(mArray.length == size()){
ensureCapacity(size() * 2 + 1);
}
for(int i = mSize; i > index; i-- ){
mArray[i] = mArray[i -1];
}
mArray[index] = value ;

mSize ++ ;
}

/** 检查 */
private void checkIndex(int index){
if(index < 0 || index >= size()){
throw new ArrayIndexOutOfBoundsException("index is not true");
}
}

/** 扩容方法 */
@SuppressWarnings("unchecked")
private void ensureCapacity(int newCapactity){
if(newCapactity < mSize){
return ;
}
AnyType[] old = mArray ;
mArray = (AnyType[]) new Object[newCapactity];
for(int i = 0; i < size(); i++){
mArray[i] = old[i];
}
}

@Override
public Iterator<AnyType> iterator() {
return new ArrayListIterator();
}

/**
* MyArrayList的迭代器。
* @author pengchengliu
*
*/
private class ArrayListIterator implements Iterator<AnyType>{

private int mCurrent ;

@Override
public boolean hasNext() {
return mCurrent < size();
}

@Override
public AnyType next() {
return mArray[mCurrent++];
}

@Override
public void remove() {
MyArrayList.this.remove(--mCurrent) ;
}
}
}

SurfaceView的简单使用

SurfaceView

1、SurfaceView的基本介绍

SurfaceView 是继承自View的,属于View的一个小小的分枝。

  • View 通过onDraw()方法里面的Canvas对象去绘制自己到屏幕上面。
  • SurfaceView 并不需要实现OnDraw()方法。那么我们的Surface是如何绘制自身的呢!其实View是在UI线程当中绘制自己,而SurfaceView是在子线程当中绘制自身。就是因为在子线程当中绘制自己所以不管绘制是多么的缓慢,还是绘制方法多么频繁的调用,其都不会阻塞UI线程。所以像播放视频时候使用的View,还是游戏所使用的View都是SurfaceView,而不是View。
  • SurfaceView是在子线程当中进行绘制,那么它是如何获取到Canvas的,如何进行绘制的?

    答:在SurfaceView当中存在一个Surface,而在Surface当中持有Canvas,但是我们拿不到Surface,系统为之暴露了一个SurfaceHolder(Surface的持有者),那么我们可以通过SurfaceView获取SurfaceHolder,然后通过SurfaceHolder获取Canvas。

    1
    2
    SurfaceHolder holder = new SurfaceView().getHolder();
    Canvas canvas = holder.getCanvas();
  • SurfaceView的绘制时机是什么时候,我们知道View的绘制时机是在onDraw()方法里面,但是SurfaceView的绘制时机在什么时候。==

    答:之前说过的SurfaceHolder不仅仅能获取Canvas对象,其实SurfaceHolder还管理者SurfaceView的生命周期。同时生命周期如下所示:

    • SurfaceCreated():创建相应的子线程,然后在子线程的run()方法当中开启对SurfaceView的绘制
    • SurfaceChanged():相当与对SurfaceView的进行监听。
    • SurfaceDestorye():关闭我们子线程,以及对一些资源的回收。

2、SurfaceView的基本写法

常见的SurfaceView的写法,这里是模版代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97

/**
* SurfaceView的自定义流程
* Created by pengchengliu on 2017/8/25.
*/

public class SurfaceDemo extends SurfaceView implements SurfaceHolder.Callback, Runnable{

private SurfaceHolder mHolder ;
private Thread mThread ;
private Canvas mCanvas ;
private boolean isRunning ;

public SurfaceDemo(Context context) {
this(context, null);
}
public SurfaceDemo(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SurfaceDemo(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

@Override
protected void onFinishInflate() {
mHolder = this.getHolder();
this.setFocusable(true); //设置SurfaceView为可以获取焦点。
this.setFocusableInTouchMode(true); //设置SurfaceView可以触摸。
this.setKeepScreenOn(true); //设置SurfaceView的屏幕应该为保持高亮。
mHolder.addCallback(this);
super.onFinishInflate();
}

/**
* 创建相应的子线程,然后在子线程的run()方法当中开启对SurfaceView的绘制
* @param holder 当前SurfaceView的Surface的持有者。
*/
@Override
public void surfaceCreated(SurfaceHolder holder) {
mThread = new Thread(this);
isRunning = true ;
mThread.start();
}

/**
*:相当与对SurfaceView的进行监听。
* 任何结构变更后立即调用(格式或尺寸)已经制成表面。 应该在这一点上更新表面的图像。
* 这种方法总是至少被调用一次,{@link #surfaceCreated}之后。
* @param holder 表面已更改的SurfaceHolder。
* @param format 式表面的新的PixelFormat。
* @param width 表面的新宽度。
* @param height 表面的新高度。
*/
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

}

/**
* 当SurfaceView被销毁的时候进行回调。
* @param holder 当前SurfaceView的Surface的持有者,也就是SurfaceHolder
*/
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
isRunning = false ;
}

@Override
public void run() {
while (isRunning){
draw();
}
}

/**
* 进行绘制操作。主要思想就是得到绘制工具Canvas,然后在不必要的情况下进行Canvas的释放
*/
private void draw() {
try {
//获取Canvas对象
mCanvas = mHolder.lockCanvas();
//TODO 为什么进行判空操作呢?
// 主要原因就是由于这个方法是在不断进行调用,所以当用户点击Home或者回退按键的话,当前的SurfaceView对象就会被销毁,
// 相应的holder对象也会被销毁,所以在这里通过hodler获取到的canvas也就是空的。所以进行判空操作。
if(mCanvas != null){
//进行绘制
}
//TODO 为什么进行try-catch呢?
// 因为就是当前页面如果被销毁掉,也就是说当前的SurfaceView已经被销毁,但是线程不会很快就被销毁,所以在这个地方就会抛出各种各样的异常。
// 在这里是try-catch,就是防止上述情况下写出来的。
} catch (Exception e) {} finally {
if(mCanvas != null){
mHolder.unlockCanvasAndPost(mCanvas);
}
}
}
}

Android,TextView优雅显示长文本、富文本

前言

Android提供了TextView这个类作为Android开发当中展示文字的工作,最近笔者在做类似于一个展示类型的APP,发现TextView这个类真的有点力不从心,好多的功能都让笔者特别头疼,于是就有了今天这篇技术博客。

原生的APi当中提供了TextView这个控件供开发者使用,一般需求都不在话下,可以设置显示文本的字体大小,字体颜色以及字体的显示格式,如:密码格式、数字格式等等。但是在千变万化的开发当中,还是不能满足开发者的需求,比如控制下拉的长文本,以及富文本。我们先来看几种需求。

效果图01

效果图02

上面看到了几点需求,还有好多,笔者相信各位看客心中都懂,所以就不展示太多了,我们今天要做到就是通过几种方式来优化我们TextView,好吧,我们开始。


1、TextView实现长文本的分段展示。

长文本:这个没什么好解释的,就是比较长的文本。直接显示就OK,但是我们知道Android当中的屏幕尺寸是有限的,我们要在有限的屏幕内合理的显示很多的内容,当然这个是侧滑菜单栏出现的原因。我们要让TextView通过用户的交互来显示合理的内容,比如在用户并不对该文本关系的前提,显示重要的前几行就OK ,如果用户想看文本内容,用户可以通过点击当前的TextView进行显示其与的内容,根据这个简单的需求,我们来对TextView进行定制。

首先我们先计划一下我们怎么对当前的TextView进行定制呢!

  • 1、我们继承一个现有的ViewGroup,当中含有一个Button、TextView。实际让Button去控制TextView的显示方式。
  • 2、我们初始化的时候可以根据TextView的长度,来决定是否显示Button,因为我们知道TextView在我们有限的空间里面可以完全显示的时候,也就不需要下拉的功能。
  • 3、通过TextView可显示的行数,完全显示的行数去测量TextView的高度。
  • 4、通过Button的点击去切换可显示的行数、完全显示的行数
  • 5、加入动画,笔者这里加入的属性动画
  • 6、解决不友好的BUG,类似于ViewGroup改变,而当ViewGroup改变动画结束,TextView才完全显示,这里会贴图给看客展示。
  • 7、添加回调定制完成,效果图展示

1、继承ViewGroup开始定制

1
2
3
4
5
6
7
8
/**
* 用于显示长文本,可以展开的TextView
* Created by suansuan on 2017/9/18.
*/
public class PullDownTextView extends LinearLayout implements View.OnClickListener{
private TextView mTextView ;
private ImageButton mImageButton;
}

我们这里选择的是LinearLayout,原因就是我们TextView和Button排列方式是线性布局。

2、初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class PullDownTextView extends LinearLayout implements View.OnClickListener{
//是否处于展开状态。默认为隐藏
private boolean isPull ;

private TextView mTextView ;
private ImageButton mImageButton;

/** ImageButton切换的两种图片 */
private Drawable mPullDownDrawable ;
private Drawable mUpDownDrawable ;

/** 初始化PullDownTextView */
private void initPullDownTextView() {
mPullDownDrawable = getDrawable(R.drawable.ic_pull_small_light);
mUpDownDrawable = getDrawable(R.drawable.ic_not_small_light);
setOrientation(LinearLayout.VERTICAL);
//默认隐藏
setVisibility(View.GONE);
}

/** 当加载完XML布局时回调 */
@Override
protected void onFinishInflate() {
super.onFinishInflate();
initPullDownTextView();
mTextView = (TextView) this.getChildAt(0);
mImageButton = (ImageButton) this.getChildAt(1);
mImageButton.setOnClickListener(this);
mImageButton.setImageDrawable(isPull ? mUpDownDrawable : mPullDownDrawable);
}

@Override
public void setOrientation(int orientation) {
if(orientation == LinearLayout.HORIZONTAL){
throw new IllegalArgumentException("参数错误:当前控件,不支持水平");
}
super.setOrientation(orientation);
}
}

我们对当前的ViewGroup进行初始化设置。对Button的图片进行初始化,以及事件的初始化

3、测量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class PullDownTextView extends LinearLayout implements View.OnClickListener{
/** 位置大小相关属性 */
private int mTextViewPullHeight ; //textView显示全部也就是下拉状态的高度
private int mTextViewNotPullHeight ;

private boolean isPull ; //是否处于展开状态。默认为隐藏
private boolean isReLayout ; //当前布局是否重新绘制。
private boolean isAnimator ; //是否处于动画当中
private boolean isMaxHeightMeasure; //是否进行TextView最大行数的测量
private boolean isMinHeightMeasure; //是否进行TexView可见行数的测量

private TextView mTextView ;
private ImageButton mImageButton;

/** ImageButton切换的两种图片 */
private Drawable mPullDownDrawable ;
private Drawable mUpDownDrawable ;

private int mTextVisibilityCount = 3; //隐藏时 TextView可以显示的最大的行数
private int mAnimatorDuration = 500 ;

...

/** 测量方发,测量自己的宽高,测量孩子的宽高 */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//没有内容的时候,
if(!isReLayout || getVisibility() == View.GONE){
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return ;
}

//有内容,但是内容比较短的时候,正常显示TextView,但是相应的隐藏ImageButton
if(mTextView.getLineCount() <= mTextVisibilityCount){
mTextView.setVisibility(View.VISIBLE);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return ;
}

//有内容,并且显示的内容比较长的时候,这里我们显示TextView、ImageButton。
mImageButton.setVisibility(View.VISIBLE);
if(!isMaxHeightMeasure && mTextViewPullHeight == 0){
mTextView.setMaxLines(Integer.MAX_VALUE);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mTextViewPullHeight = mTextView.getMeasuredHeight() ;
isMaxHeightMeasure = true ;
}

if(!isMinHeightMeasure && mTextViewNotPullHeight == 0){
mTextView.setMaxLines(mTextVisibilityCount);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mTextViewNotPullHeight = mTextView.getMeasuredHeight();
isMinHeightMeasure = true ;
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}

在这里测量为什么只测量一次呢,因为后续我们会通过属性动画去改变TextView的高度,而我们改变后,我们获取就会导致我们获取到的高度不是定值,而是改变后的值。测量这里我们主要分为三种情况,上述代码当中注释也说的很清楚

  • 1、没有内容的时候,
  • 2、有内容,但是内容比较短的时候,正常显示TextView,但是相应的隐藏ImageButton
  • 3、有内容,并且显示的内容比较长的时候,这里我们显示TextView、ImageButton。

4、点击事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public void onClick(View v) {
if(isAnimator){
return ;
}
if(isPull){
startAnimator(mTextView, mTextViewPullHeight, mTextViewNotPullHeight);
} else {
startAnimator(mTextView, mTextViewNotPullHeight, mTextViewPullHeight);
}
//下拉,或者上拉的时候的回调
if(this.mOnTextViewPullListener != null){
this.mOnTextViewPullListener.textViewPull(mTextView, isPull);
}
isPull = !isPull ;
mImageButton.setImageDrawable(isPull ? mUpDownDrawable : mPullDownDrawable);
}

根据用户点击状态去切换Button的图标,还有根据刚刚测量的高度进行开启动画

5、开启动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 开始动画
*/
private void startAnimator(final TextView view, int startHeight, int endHeight){
ValueAnimator valueAnimator = ValueAnimator.ofInt(startHeight , endHeight ).setDuration(mAnimatorDuration);
valueAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {}
@Override
public void onAnimationEnd(Animator animation) {
isAnimator = false ;
}
@Override
public void onAnimationCancel(Animator animation) {}
@Override
public void onAnimationRepeat(Animator animation) {}
});
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int animatedValue = (int) animation.getAnimatedValue();
ViewGroup.LayoutParams params = view.getLayoutParams();
params.height = animatedValue ;
//这句,让TextView文本的高度随TextView高度进行变化
view.setMaxHeight(animatedValue);
view.setLayoutParams(params);
}
});
isAnimator = true ;
valueAnimator.start();
}

这个地方有一个坑,笔者也是想了很久,才弄明白的,说不太清楚,看下效果图吧。为了各位看客能很清楚的BUG,笔者在这里加入不同的背景。

这种效果就是在刚刚开始动画的时候,应该加入

  • view.setMaxHeight(animatedValue);

6、回调接口

1
2
3
4
5
6
7
8
/** TextView展开回调 */
public interface OnTextViewPullListener{
void textViewPull(TextView textView, boolean isPull) ;
}

public void setOnTextViewPullListener(OnTextViewPullListener listener){
this.mOnTextViewPullListener = listener ;
}

7、在MainActivity当中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MainActivity extends AppCompatActivity {

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

PullDownTextView text1 = (PullDownTextView)findViewById(R.id.expand_text_view);
text1.setText(getString(R.string.long_text1));

PullDownTextView text2 = (PullDownTextView)findViewById(R.id.expand1_text_view);
text2.setText(getString(R.string.long_text1));
}
}

这里没有什么好说的,相信各位看客都懂。这就是大体的流程,回调接口就是在onClick事件回调的,上述代码也有说明。到这里就差不多了,源码在文章末尾给出,看一下我们的效果吧


2、TextView显示富文本

我们在开始编码之前,我们先来了解一下什么是富文本,
富文本(Rich Text Format):这个有些开发者比较陌生,那么什么是富文本呢?其实就是一段带有自己的格式的文本。这么说有点抽象,我们来举个例子,其实就是我们常用的Word编辑器所写的文本,每一个字都是带有格式的。我们看下面的一个例子就理解了什么是富文本。

1
2
Hello!
This is some bold text.

仔细观察,上述的一段文字是带有格式。这就是我们常见的富文本。现在我们看富文本的相应代码

1
2
3
4
{\rtf1\ansi
Hello!\par
This is some {\b bold} text.\par
}

上述的富文本格式代码,貌似存在一定的规则可寻。什么规则呢,这里大体的描述一下,因为笔者这里语法也没有太多的深入,反斜线(\)标着这个RTE(富文本)控制的开始。(\par)表示开始新的一行,有点类似于HTML当中的标签了。(\b)将文字粗体显示。({})大括号定义了一个群组,上述例子中使用了一个群组来限制代码\b的作用范围。合法的RTF文档是一个以代码\rtf开始的群组。

了解了基本的什么是富文本之后,我们开始思考在本文开后的图1里面效果,如果让大家在Wold编辑器当中编写,会非常简单。当然Google也考虑到了这一种情况,所以我们不需要定制View就可以达到这种效果,Google为我们提供一个类用来封装我们带有格式的富文本,然后丢给TextView进行显示就OK,

1、Google提供富文本封装类Spannable

Spannable是一个接口,有两个实现类分别是SpannableStringSpannableStringBuilder,我们知道我们以后要使用的话,肯定就是这里面的这两个类啦。那么这两个类有什么区别呢,其实和我们早前学过的String,和StringBuilder是一样的,一个为定长字符串,一个为可变字符串的区别,可以根据看客自己的需求去选择

Spannable里面定义了两个方法,和一个静态工厂,通过静态工厂拿到Spannable默认实现类是SpannableString。具体代码如下所示:
Spannable.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static class Factory {
private static Spannable.Factory sInstance = new Spannable.Factory();

/**
* Returns the standard Spannable Factory.
*/
public static Spannable.Factory getInstance() {
return sInstance;
}

/**
* Returns a new SpannableString from the specified CharSequence.
* You can override this to provide a different kind of Spannable.
*/
public Spannable newSpannable(CharSequence source) {
return new SpannableString(source);
}
}

Spannable里面定义了两个方法,分别是:

1
2
>  public void setSpan(Object what, int start, int end, int flags);
> public void removeSpan(Object what);

这里有几个参数,需要说一下,

  • what : 样式
  • start : 该样式作用范围的起始位置
  • end : 该样式作用范围的结束位置
  • flags : 模式,

最后一个参数的模式,相对的有点抽象,其实看客可以理解成为枚举,也就是说模式是系统为我们定义好的,让我去选择使用就OK了。在系统当中由Spanned给出。

  • Spanned.SPAN_INCLUSIVE_INCLUSIVE 起始结束都包括
  • Spanned.SPAN_EXCLUSIVE_INCLUSIVE 起始不包括,结束包括
  • Spanned.SPAN_INCLUSIVE_EXCLUSIVE 起始包括,结束不包括
  • Spanned.SPAN_EXCLUSIVE_EXCLUSIVE 起始结束都不包括

1.1、获取Spannable实例

我们刚刚看过源码,知道了Spannable内部又一个静态的工厂类,那我们就使用这个类来获取实例。

1
> Spannable spannable = Spannable.Factory.getInstance().newSpannable(string);

1.2、Google为我们提供的样式

其实这里的样式是特别的多的,Google主要按照分类,分成了两大类,分别是字体的样式,和段落的样式。笔者在这里找几个常用的到的进行说明,其他的请各位看客自行去了解CharacterStyle, ParagraphStyle的实现子类,好找出看客所需要的样式。

1.3、颜色相关

颜色相关主要分为一个字体的颜色(ForegroundColorSpan),一个背景的颜色(BackgroundColorSpan)。

在这里我专门给测试TextView加入了背景和字体,我们发现,在背景方面,Span只能作用于Text的绘制区域。在字体颜色方面Span是优于我们设置的字体颜色的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/***
* 颜色相关
* BackgroundColorSpan : 背景颜色样式
* ForegroundColorSpan : 字体颜色
*/
public void colorSpan(){
Spannable spannable = Spannable.Factory.getInstance().newSpannable(text);
BackgroundColorSpan backgroundColorSpan = new BackgroundColorSpan(Color.parseColor("#FF0000"));
spannable.setSpan(backgroundColorSpan, 0, 9, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
mTextView.setText(spannable);

Spannable spannable1 = Spannable.Factory.getInstance().newSpannable(text);
ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(Color.parseColor("#FF0000"));
spannable1.setSpan(foregroundColorSpan, 0, 9, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
mTextView1.setText(spannable1);
}

1.4、大小位置相关

大小方面主要是RelativeSizeSpan,构造时传入一个数值来说明比较当前字体大小的变化,大于0为变大,小于0为变小
位置方面主要是上移(SuperscriptSpan),下移(SubscriptSpan),移动完成以后大小是不会变化的,上移距离为当前文本高度的一半,下一距离也是当前文本的一半。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 大小相关
* RelativeSizeSpan :显示大小
*
* 位置相关
* SuperscriptSpan :上移
* SubscriptSpan : 下移
*/
public void sizeSpan(){
Spannable spannable = Spannable.Factory.getInstance().newSpannable(text);
RelativeSizeSpan sizeSpanBig = new RelativeSizeSpan(1.4f);
RelativeSizeSpan sizeSpanSmall = new RelativeSizeSpan(0.6f);
spannable.setSpan(sizeSpanSmall, 0, 9, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
spannable.setSpan(sizeSpanBig, 11, spannable.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
mTextView.setText(spannable);

Spannable SuperscriptSpanAble = Spannable.Factory.getInstance().newSpannable(text);
SuperscriptSpan sizeSpan = new SuperscriptSpan();
SuperscriptSpanAble.setSpan(sizeSpan, 12, spannable.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
mTextView1.setText(SuperscriptSpanAble);

Spannable SubscriptSpanAble = Spannable.Factory.getInstance().newSpannable(text);
SubscriptSpan SubscriptSpan = new SubscriptSpan();
SubscriptSpanAble.setSpan(SubscriptSpan, 12, spannable.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
mTextView2.setText(SubscriptSpanAble);
}

1.5、常见样式相关

常见样式有下划线,删除线,textStyle(粗体、斜体)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/***
* 样式相关:
* StrikethroughSpan : 删除线
* UnderlineSpan : 下划线
*
* StyleSpan : 一般样式
*/
public void styleSpan(){
Spannable spannable = Spannable.Factory.getInstance().newSpannable(text);
StrikethroughSpan strikethroughSpan = new StrikethroughSpan();
spannable.setSpan(strikethroughSpan, 0, 9, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
mTextView.setText(spannable);

Spannable underlineSpanAble = Spannable.Factory.getInstance().newSpannable(text);
UnderlineSpan sizeSpan = new UnderlineSpan();
underlineSpanAble.setSpan(sizeSpan, 11, spannable.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
mTextView1.setText(underlineSpanAble);

Spannable styleSpan = Spannable.Factory.getInstance().newSpannable(text);
StyleSpan styleSpan_Bold = new StyleSpan(Typeface.BOLD);
StyleSpan styleSpan_Italic = new StyleSpan(Typeface.ITALIC);
styleSpan.setSpan(styleSpan_Bold, 0, 9, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
styleSpan.setSpan(styleSpan_Italic, 11, spannable.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
mTextView2.setText(styleSpan);
}

1.6、跳转相关

常见的跳转相关有,点击事件,超链接。其实超链接的实现就是点击事件,只不过点击以后由当前手机的默认浏览器去打开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* ClickableSpan : 可点击的文字
*
*/
public void clickSpan(){
Spannable spannable = Spannable.Factory.getInstance().newSpannable(text);
spannable.setSpan(new ClickableSpan() {
@Override
public void onClick(View widget) {
Toast.makeText(RichTextActivity.this, "点击测试", Toast.LENGTH_LONG).show();
}
},
9, spannable.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
mTextView.setMovementMethod(LinkMovementMethod.getInstance());
mTextView.setText(spannable);


Spannable spannableUrl = Spannable.Factory.getInstance().newSpannable(text);
URLSpan urlSpan = new URLSpan("http://blog.csdn.net/lpc_java?viewmode=list");
spannableUrl.setSpan(urlSpan, 9, spannable.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
mTextView1.setText(spannableUrl);
mTextView1.setMovementMethod(LinkMovementMethod.getInstance());

mTextView2.setVisibility(View.GONE);
}

注意:mTextView1.setMovementMethod(LinkMovementMethod.getInstance());必须设置TextView的MovementMethod才有点击效果

1.7、图片相关

在文字当中使用图片,其实这个我们可以联想一下社交软件当中的聊天表情。

在这里我们发现,是图片去替代了我们原有的文字,看客们在这里注意一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 图片相关
* ImageSpan
*/
public void imageSpan(){
Spannable spannableImgae = Spannable.Factory.getInstance().newSpannable(text);
Drawable image = this.getResources().getDrawable(R.mipmap.star);
image.setBounds(0,0,60,60);

ImageSpan imageSpan = new ImageSpan(image);
spannableImgae.setSpan(imageSpan, spannableImgae.length()-2, spannableImgae.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
mTextView.setText(spannableImgae);

mTextView1.setVisibility(View.GONE);
mTextView2.setVisibility(View.GONE);
}

好了基本的几种在这里都已经说完


2 、其他方式实现富文本显示

HTML,也可以实现上述的功能,就是把含有HTML标签的语句直接在TextView当中进行显示,

1
2
3
4
5
6
7
8
9
10
public void html(){
String html = "测 <br/> 试 <br/> 文 <br/> 字 <br/>" ;
mTextView.setText(Html.fromHtml(html));

String html1 = "<h1>标题</h1>" ;
mTextView1.setText(Html.fromHtml(html1));

String html2 = "<font color ='#FF0000'>测试文字</font>" ;
mTextView2.setText(Html.fromHtml(html2));
}

但是TextView对HTML的支持不是很全,下面就把TextView对HTML的支持列举一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
a href="...">  定义链接内容
b> 定义粗体文字 b 是blod的缩写
big> 定义大字体的文字
blockquote> 引用块标签
属性:
Common -- 一般属性
cite -- 被引用内容的URI
br> 定义换行
cite> 表示引用的URI
dfn> 定义标签 dfn 是defining instance的缩写
div align="...">
em> 强调标签 em 是emphasis的缩写
font size="..." color="..." face="...">
h1>
h2>
h3>
h4>
h5>
h6>
i> 定义斜体文字
img src="...">
p> 段落标签,里面可以加入文字,列表,表格等
small> 定义小字体的文字
strike> 定义删除线样式的文字 不符合标准网页设计的理念,不赞成使用. strike是strikethrough的缩写
strong> 重点强调标签
sub> 下标标签 sub 是subscript的缩写
sup> 上标标签 sup 是superscript的缩写
tt> 定义monospaced字体的文字 不赞成使用. 此标签对中文没意义 tt是teletype or monospaced text style的意思
u> 定义带有下划线的文字 u是underlined text style的意思

笔者在这里把第一个 < 取消啦 因为格式会乱,相信各位看客也能理解

结束语

在此 算是结束啦 在这里附上源码链接,
源码下载

参考文献
https://developer.android.com/reference/android/text/Spannable.html
https://developer.android.com/reference/android/text/style/CharacterStyle.html
https://developer.android.com/reference/android/text/style/ParagraphStyle.html
https://github.com/Manabu-GT/ExpandableTextView

http://www.jianshu.com/p/84067ad289d2
http://www.jianshu.com/p/aa53ee98d954
http://2960629.blog.51cto.com/2950629/751360
https://juejin.im/entry/5729d28f1ea49300606854c9

webView的简单使用

###

是基于Android使用了WebKit内核的浏览器框架,和其他组件没有什么区别,主要作用就是加载一些基于HTML页面的基本信息

为什么使用WebView

优点:
  • 1、兼容已有的项目 例如淘宝
  • 2、可以动态更新 BUG的处理

基于以上好处,人们开始思考是否可以通过WebView去替代原有的Native原生开发。于是FaceBook去研究通过WebView去代替原有的Native开发,但是好景不长,facebook发现了WebView不是想象当中的那么好用,发现了如下缺点。

缺点:
  • 1、使用webView的App统一耗电量都比较庞大
  • 2、使用WebView的APP的加载速度也比较卡,
  • 3、由于耗电量比较大导致手机容特别容易发热

WebView基本去使用

其实Google把相应的方式都封装在相应的WebKit内核当中所以对于客户端的我们来说我们只需要创建WebView的对象,通过该对象的loadUrl(String url)方法去加载即可。

1、在XMl布局文件当中引入WebView

activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
<?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="match_parent">

<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent" />

</LinearLayout>

2、在MainActivity当中对WebView控件进行设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private WebView mWebView;
private String mUrl = "http://www.baidu.com";

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

/**
* 初始化控件
*/
private void initView() {
mWebView = (WebView) findViewById(R.id.webView);
mWebView.loadUrl(mUrl);
}

效果就是当我们的应用程序跑起来以后我们的WebView回去通过浏览器去加载我们的网页,所以我们去对WebView进行设置防止通过浏览器去进行加载

这个时候我们就需要自己去定义一个WebViewClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 mWebView.setWebViewClient(new WebViewClient(){
/**
* 覆盖原有的加载方式
* 如果没有给WebView设置WebViewClient,默认情况下WebView会要求Activity Manager选择URL的正确处理程序(浏览器)
* 如果给WebView设置WebViewClient,
* 注意:使用POST“方法”的请求不会调用此方法。
* @param view : WebView
* @param url :请求地址
* @return :返回true,表示主机应用程序处理url,而return false则表示当前WebView处理URL。
*/
// @Override
// public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
// view.loadUrl(request.getUrl().toString());
// return super.shouldOverrideUrlLoading(view, request);
// }
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return super.shouldOverrideUrlLoading(view, url);
}
});

WebView自定义Title

我们手机在显示WebView的时候会在标题的位置去显示当前加载的网页的地址之类的信息。

1、我们去修改XML布局,添加要显示地址的View

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?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="match_parent"
android:orientation="vertical">

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="返回"/>

<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"/>

<Button
android:id="@+id/refresh"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:text="刷新"/>
</RelativeLayout>

<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent" />

</LinearLayout>

2、对WebView控件进行设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
package pengchengliu.imooc.webview;

import android.graphics.Bitmap;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

private String TAG = "WebViewTag";

private WebView mWebView;
Button mBackButton, mRefreshButton;
private TextView mTitleTextView;
String mUrl = "https://www.baidu.com";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.i(TAG ,"Activity创建," + "线程Id为" + Thread.currentThread().getId());
initView();
}

/**
* 初始化控件
*/
private void initView() {
findView();
mWebView.setWebViewClient(new WebViewClient(){
/**
* 页面可以试加载的时候进行回调。
*/
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
Log.i(TAG ,"页面开始加载," + "线程Id为" + Thread.currentThread().getId());
super.onPageStarted(view, url, favicon);
}
/**
* 页面用户可见的时候进行回调
*/
@Override
public void onPageCommitVisible(WebView view, String url) {
Log.i(TAG ,"页面开始加载," + "线程Id为" + Thread.currentThread().getId());
super.onPageCommitVisible(view, url);
}
/**
* 页面加载完成的时候进行回调
*/
@Override
public void onPageFinished(WebView view, String url) {
Log.i(TAG ,"页面加载完成," + "线程Id为" + Thread.currentThread().getId());
super.onPageFinished(view, url);
}
/**
* 覆盖原有的加载方式
* 如果没有给WebView设置WebViewClient,默认情况下WebView会要求Activity Manager选择URL的正确处理程序(浏览器)
* 如果给WebView设置WebViewClient,
* 注意:使用POST“方法”的请求不会调用此方法。
* @param view : WebView
* @param url :请求地址
* @return :返回true,表示主机应用程序处理url,而return false则表示当前WebView处理URL。
*/
// @Override
// public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
// view.loadUrl(request.getUrl().toString());
// return super.shouldOverrideUrlLoading(view, request);
// }
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return super.shouldOverrideUrlLoading(view, url);
}
});
mWebView.setWebChromeClient(new WebChromeClient(){
/**
* 通知主机应用程序更改文档标题。
* @param view 启动回调的WebView。
* @param title 包含文档的新标题的字符串。
*/
@Override
public void onReceivedTitle(WebView view, String title) {
//在主线程当中,可以直接更新UI
mTitleTextView.setText(title);
super.onReceivedTitle(view, title);
}
});
mWebView.loadUrl(mUrl);
}

/**
* 通过ID查找View
*/
private void findView() {
mWebView = (WebView) findViewById(R.id.webView);
mBackButton = (Button) findViewById(R.id.back);
mRefreshButton = (Button) findViewById(R.id.refresh);
mTitleTextView = (TextView) findViewById(R.id.title);
MyListener myListener = new MyListener();
mBackButton.setOnClickListener(myListener);
mRefreshButton.setOnClickListener(myListener);
}

/**
* 按钮监听事件
*/
private class MyListener implements View.OnClickListener {
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.back:
//可以通过这个方法去重新加载
mWebView.goBack();
Log.i(TAG, mWebView.getUrl());
break;
case R.id.refresh:
mWebView.reload();
//mWebView.goForward();//前进
//mWebView.reload(); //刷新
Log.i(TAG, mWebView.getUrl());
break;
}
}
}
}

使用WebView去下载文件

在下载之前必须要获取到文件的地址,需要DownloadListener接口的实现类,重写他的方法,而下载的具体url遍会出现在它的参数当中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* WebView下载的监听器
*/
private class WebViewDownload implements DownloadListener{
/**
* 通过WebView去下载文件时所需要的监听器
* 通知主机应用程序应该下载文件
     * @param url应该下载的内容的完整网址
     * @param userAgent要用于下载的用户代理。
     * @param contentDisposition Content-disposition http header,if当下。
     * @param mimetype服务器报告的内容的mimetype
     * @param contentLength服务器报告的文件大小
*/
@Override
public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
Log.i("haha", "这是一个下载地址, url ------ > " + url);
if(!url.endsWith(".apk")){
Log.i("haha", "下载文件不是apk文件");
return ;
}
new DownloadThread(url).start();
}
}

主要就是两种方式,

1、通过WebView获取将要下载的文件地址,然后通过Intent将要下载的Uri地址传过去,让系统自己为我们下载。
1
2
3
Uri uri = Uri.parse(url);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);
2、通过WebView获取将要下载的文件地址,然后自己新建一个线程去进行下载。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class DownloadThread extends Thread {

private String url ;

DownloadThread(String url){
this.url = url;
}

@Override
public void run() {
try {
Log.i("haha", "开始下载");
URL url = new URL(this.url);
HttpURLConnection connect = (HttpURLConnection) url.openConnection();
connect.setDoInput(true);
connect.setDoOutput(true);
InputStream inputStream = connect.getInputStream();
if(!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
Log.e("haha", "不存在SD卡,或者外部存储空间不足");
return;
}
File descFile = Environment.getExternalStorageDirectory();
File srcFile = new File(descFile, "temp.apk");
OutputStream outputStream = new FileOutputStream(srcFile);
byte[] bytes = new byte[6 * 1024];
int len ;
while((len = inputStream.read(bytes)) != -1){
outputStream.write(bytes, 0, len);
Log.i("haha", "0,~~~" + len);
}
Log.i("haha", "下载成功");
} catch (IOException e) {
Log.i("haha", "下载失败");
e.printStackTrace();
}
}
}

WebView对错误页面的处理

在手机没有网络的情况下,WebView会默认给我们展示一个断网页面,但是对于一个商业项目来说,展示这个页面略显粗糙,我们如何自定义错误页面呢?有以下两种方法

  • 1、还在本地错误页面,但是还属于HTML页面
1
2
3
4
5
6
@Override
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
//加载本地资源
mWebView.loadData(file://xxx.xxx.xxx);
super.onReceivedError(view, request, error);
}
  • 2、加载一个本地的Native页面,在相应的WebViewClient内部重写监听WebView加载错误的回调,然后在错误回调当中去做相应的处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mWebView.setWebViewClient(new WebViewClient(){
/**
* 当WebView出现错误的时候进行回调。一般用来进行错误页面的重写,
* 向主机应用程序报告Web资源加载错误。 这些错误通常表示无法连接到服务器。 请注意,与回拨版本不同的是,
* 新版本将被调用任何资源(iframe,image等),而不仅仅是为主页面。 因此,建议在此回调中执行最低要求的工作。
* @param view 启动回调的WebView。
* @param request 请求发起请求。
* @param error 错误有关错误的信息。
*/
@Override
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
Log.i("xixi" ,"onReceivedError,");
mErrorTextView .setText("404有错误");
mWebView.setVisibility(View.GONE);
super.onReceivedError(view, request, error);
}
}

WebView如何同步Cookie问题

开发过程中如果我们已经让用户登陆过了,在另外的页面是WebView,在刷到最新的WebView的时候会提示用户重新登录,但是如果我们把登录信息保存在Cookie当中,在刷WebView的时候一起传递给服务器,这样遍不会提示用户重新登录了。

这种问题就是客户端和服务器去同步Cookie问题。

流程就是:客户端先去登录,登录以后可以拿到相应的Cookie信息,当我们去第二次进行登陆的时候,拿着我们第一次登录Cookie信息,去设置相应的Cookie,已达到免登录的状态

问题 : 如何通过JavaAPI去进行POST请求,然后根据相应的POST请求拿到相应的Cookie,然后如何通过设置Cookie给WebView进行相应的设置,以至于达到免登录的状态

如何进行POST请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CookieStore cookieStore = new BasicCookieStore();
CloseableHttpClient client = HttpClientBuilder.create().setDefaultCookieStore(cookieStore).build();
HttpPost httpPost = new HttpPost(requestUrl);

List<NameValuePair> params = new ArrayList<>();
NameValuePair name = new BasicNameValuePair("","");
NameValuePair password = new BasicNameValuePair("","");
params.add(name);params.add(password);

if(params.size() <= 0){
return ;
}

UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, charset);
httpPost.setEntity(entity);
CloseableHttpResponse execute = client.execute(httpPost);

如何获取Cookie

1
2
3
4
5
6
7
if (execute.getStatusLine().getStatusCode() == 200){
List<Cookie> cookies = cookieStore.getCookies();
for (Cookie tempCookie : cookies) {
Log.i("cookies", tempCookie.getName() + ": " + tempCookie.getValue() );
//使用Handler
}
}

如何给WebView设置Cookie

1
2
3
4
5
6
7
8
9
10
11
12
13
private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
CookieSyncManager.createInstance(MainActivity.this);
CookieManager manager = CookieManager.getInstance();
manager.setAcceptCookie(true);
manager.setCookie(mUrl, msg.obj.toString());
CookieSyncManager.getInstance().sync();

mWebView.loadUrl(mUrl);
super.handleMessage(msg);
}
};

WebView和JS混淆回调

首先我们应该明白,在正常的情况下WebView和JS是可以相互调用的通过@JavaScriptIntface注解,但是在我们的apk上线的时候我们需要给我们的apk打入一个replase包,让我们的apk当中的代码混淆,这样就会使我们原有的JS调不到我们的android端的接口。

解决方案:就是在打入混淆的时候,对我们要给Js回调的类和方法进行保护,这样当前类就不能混淆了。

在当前项目里面的proguard.cfg文件当中加入如下代码

1
2
3
-keep public class com.example.webview_01.WebHost{
public <methods>
}

所涉及到的知识点:

WebHost.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 当前类为暴露给JS用户的接口
* Created by pengchengliu on 2017/8/13.
*/

public class WebHost {

private Context mContext;

public WebHost(Context context){
this.mContext = context;
}
public void callJs(){
Toast.makeText(mContext, "12334", Toast.LENGTH_LONG).show();
}
}

MainActivity.java

1
2
3
4
5
6
7
8
9
/**
* Java 暴露给 Js 回调接口
*/
@SuppressLint("AddJavascriptInterface")
private void setWebViewJavaScriptInterface() {
WebSettings settings = mWebView.getSettings();
settings.setJavaScriptEnabled(true);
mWebView.addJavascriptInterface(new WebHost(MainActivity.this),"callJs");
}

WebView导致的远程注入的问题

在android本地和js交互的途中会出现种种的注入问题,大体的流程就是我们把客户端的对象暴露给了服务器,服务器当中可以拿到我们暴露给它的对象,拿到这个对象,服务器就可以拿到我们本地的Runtime对象,就哭执行种种命令来获取我们本地的信息。Google也是意识到这样的危险性,所以在版本4.2之前就已经被修复,浏览器厂商也是做了相应的BUG修复,但是在开发者做WebView的时候,需要注意。

解决办法:尽可能少的去做Java和JS之间的交互,最好的就是通过自定义协议来处理相应的问题。


WebView自定义协议拦截问题

就是客户端人员和Web前端人员进行商议得到一个协议,然后就是通过Url上面带有不同的参数在Android里面的回调根据不同的参数进行不同的参数,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 覆盖原有的加载方式
* 如果没有给WebView设置WebViewClient,默认情况下WebView会要求Activity Manager选择URL的正确处理程序(浏览器)
* 如果给WebView设置WebViewClient,
* 注意:使用POST“方法”的请求不会调用此方法。
* @param view : WebView
* @param url :请求地址
* @return :返回true,表示主机应用程序处理url,而return false则表示当前WebView处理URL。
*/
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
//xxxxx就是约定的协议
if(url.endsWith("xxxxxxx")){
//做相应的操作
return true
}
view.loadUrl(url);
return super.shouldOverrideUrlLoading(view, url);
}

Http03 - Http状态码

Http的状态码表示客户端请求服务器的返回结果,标记服务器端的处理是否正常,通知客户端出现错误的情况,


学习目标

  • 状态码的类别定义
  • 2XX成功
  • 3XX重定向
  • 4XX客户端错误
  • 5XX服务器错误
  • 状态码和状况不一致

状态码的类别定义

状态码如 200 OK 以3位数字和原因短句组成,接下来我们就来看看14种常用的状态码

补充:在RFC2616中定义的Http状态码就达到40多种,再加上WebDAV,就达到60多种。


2XX 成功

  • 200 ok

    表示从从客户端发来的请求已经被正常处理了

  • 204 No Content

    请求处理成功,但是没有资源可返回

  • 206 Partial Content

    客户端对服务器端进行一部分资源请求的时候,返回,并且客户端的请求处理成功


3XX 重定向

响应结果表明浏览器需要执行某些特殊的处以正确处理请求

  • 301 Moved Permanently

    永久性重定向,该状态码已被分配了新的URI,以后应使用资源现在所指的URI。

  • 302 Found

    临时重定向。该状态码表示请求资源已被分配了新的URI,希望用户(本次)能使用新的URI访问


4XX 客户端错误

表示客户端是发生错误的原因所在

  • 400 Bad Request

    表示请求报文中存在语法错误。当错误发生的时候,需要修改请求的内容后再次发送请求。另外浏览器方面会像对待200 OK 一样对待该状态码

  • 401 Unauthorized

    表示发送的请求需要Http认证(BASIC认证、DIGEST认证)的认证信息。若之前已经进行过1次认证,则表示用户认证失败

    返回含有401的响应必须包含一个适用于请求资源WWW-Authenticate首部用以质问用户信息。当浏览器第一次收到401响应,会默认弹出认证用的对话窗口

  • 403 Forbidden

    表示请求资源的访问被服务器拒绝。服务器没有必要给出拒绝的详细理由,但如果想作说明的话,可以在实体的主体部分对原因进行描述,这样就能让用户看到了

    未获得文件系统的访问授权,访问权限出现某些问题(从未授权的发送源IP地址试图访问)等列举的情况都可能发生403的原因

  • 404 No Found

    表示客户端请求的资源在服务器端找不到,或者在拒绝客户端请求访问,并且不想给出说明理由的时候使用该状态码!