基础

工具

adb 命令

  • adb kill-server 杀死模拟器
  • adb start-server 启动模拟器
  • cd desktop 进入桌面文件夹
  • adb install path/x.apk 安装应用
  • adb uninstall com.xxx 卸载应用
  • adb shell 进入linux指令(ctrl+C退出)
  • # ls 列出目录下所有文件(夹)清单
  • adb pull a.txt 从手机中导出文件
  • adb push a.txt /mnt/sdcard 把一个文件导入到手机
  • monkey 1000 随机点模拟器1000次(冒烟测试(压力测试))

简易拨号应用

设置按钮点击事件的四种方式

1、内部类

使用 onClickListener() 类

1
2
3
4
5
6
7
8
9
10
btn = (Button) findViewById(R.id.button1); // 找到按钮
btn.setOnClickListener(new MyClickListener()); // 设置按钮点击事件

// 在 MainActivity 里面再定义一个类去实现 按钮需要的接口类型
private class MyClickListener implements OnClickListener{
@Override
public void onClick(View v) { // 当按钮被单击的时候调用
System.out.println("按钮被点击了");
}
}

2、匿名内部类

1
2
3
4
5
6
btn.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v){
// code process
}
});

3、setOnClickListener(this)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MainActivity extends Activity implements onClickListener{
// 注意:接口所属的包名有多个,不能选错:onClickListener - android.view.View
public void onCreate()
{
btn.setOnClickListener(this); // 适用于多个按钮使用同一个事件的情况
}

@Override
public void onClick(View v){
switch (v.getID()){ // 可根据参数来判断按钮
case R.id.btn1 :
break;
case R.id.btn2 :
break;
}
}
}

4、布局中的 onClick

在 AndroidManiFest 中设置 attribute(属性)

1
2
3
4
5
6
7
<Button
//……
android:onClick="myClick" />

public void myClick(View v){ // 方法名必须和 attribute 的 onClick 属性一致
// Kabloey
}

获取 EdiText 文本内容

1
2
3
4
5
ed = (EditText) findViewById(R.id.editText1);
String str = ed.getText().toString().trim(); // trim()去空格
if (str.equals("")){ // 如果文本为空
Toast.makeText(MainActivity.this, "文本不能为空", 1).show(); // 弹出吐司
}

判断文本是否为空的API:TextUtils.isEmpty(str)

吐司 Toast

1
2
Toast.makeText(context/*上下文,可用 this,指当前的activity,即context*/,
text, duration).show();

注意:如果是在自定义的类例如 MyClickListener 中,context 不能是 this
context 可以改成:MainActivity.this
duration 弹出时长,参数有:

  • Toast.LENGTH_LONG 值为1
    • Toast.LENGTH_SHORT值为0

最后别忘了弹出吐司: .show()

意图 Intent

1
2
3
4
Intent intent = new Intent(); // 创建一个意图对象
intent.setAction(Intent.ACTION_CALL); // 设置动作:打电话
intent.setData(Uri.parse("tel:" + num)); // 设置数据:tel:119
startActivity(intent); // 开启意图:调用电话进行拨号(注意:权限 android.permission.CALL_PHONE)

布局

1、线性布局

1
2
3
4
5
6
android:orientation="horizontal" // 水平的(默认)
android:orientation="vertitcal" // 竖直的
android:layout_width="match_parent" // 填充父
android:layout_height="wrap_content" // 自适应
android:paddingBottom="@dimen/activity_vertical_margin" // 控件内部内容(文字)位置
android:marginLeft="10dp" // 控件整体左边留空 10 像素
1
2
3
4
5
6
7
8
9
<!-- 总体布局 -->
<LinearLaout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:toos="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity" >
<!-- 内部控件布局 -->
</LinearLaout>

2、相对布局

控件默认左上角,需手动确定相对关系

1
2
3
4
5
android:id="@+id/btn2" // 添加一个 ID
android:layout_below="@id/btn1" // 在 btn1 的下面(即当前控件的的上面)
android:layout_toRightOf="@id/btn3" // 在 btn3 的右边
android:layout_alignParentRgiht="true" // 在布局的最右边
android:layout_alignBottom="@id/bt4" // 与 bt4 底部对齐
1
2
3
<RelativeLayout xmlns="">
<!-- 控件布局 -->
</RelativeLayout>

3、帧布局

一层一层显示,后面的Frame在前面的Frame上面(默认窗口左上角)

1
android:layout_gravity="center"
  • center 居中

  • center_vertical 垂直居中

  • center_horizontal 水平居中

4、表格布局

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
<TableLayout
android:layout_width="match_parent"
android:layout_height="match_parent"

<!--代表一行-->
<TableRow
android:layout_width="match_parent"
android:layout_height="wrap_content" >

<!--加入第一个控件-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="所有控件开头都是大写的"
android:textColoe="#ff0000" />

<!--加入第二个控件-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:text="这是第二列"
android:textSize="18sp" />

</TableRow>

<TableRow> <!-- 第二行 --> </TableRow>
<TableRow> <!-- 第三行 --> </TableRow>

</TableLayout>

5、绝对布局

absolution 已经废弃了

权重

只能在线性布局中使用,平分布局。

1
2
3
4
<LinearLayout>
<TextView android:layout_weight="2"/> <!--占三分之二-->
<TextView android:layout_weight="1"/> <!--占三分之一-->
</LinearLayout>

单位

  • px 像素(不适配屏幕,不建议使用)
    • dp自动适应屏幕的单位(px的替代单位)
    • sp控件文字大小 textSize

调试

单元测试

  1. 定义一个类继承 AndroidTestCase

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import android.test.AndroidTestCase

    public class CalcTest extends AndroidTestCase {
    // 写测试方法
    public void testAdd()
    {
    Calc calc = new Calc(); // 这时已经写好的待测试类
    int result = calc.add(5, 3);

    assertEquals(8/*expected*/, result/*actual*/); // 期望与实际进行对比
    }
    }
  2. 在清单文件(AndroidManifest) 中加入 uses-libraryinstrumentation

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <manifest xmlns:android...>
    <application android:icon="@drawable/icon"...>
    <!-- 配置函数库 (application 里面) -->
    <uses-library android:name="android.test.runner" />
    ...
    </application>

    <uses-sdk android:minSdkVersion="6" />

    <!-- 测试指令集 (application 外面) -->
    <instrumentation android:name="android.test.InstrumentationTestRunner"
    android:targetPackage="应用包名" android:lable="标题(可省)" />
    </manifest>
    \<application android:icon="@drawable/icon"...>
        ***\<uses-library android:name="android.test.runner" />***
    \</application>
    ***<instrumentation android:name="android.test.InstrumentationTestRunner"***
        ***android:targetPackage="cn.itcast.action" android:lable="Tests for My APP" />***
  3. Run As : Android JUnit Test

  4. 加入误相减,console 会出现:junit.framework.AssertionFailedError: expected:<8> but was <2>

    可根据不同的错误级别来显示

日志猫

1
2
3
4
5
Log.v(tag/*一般是 类名*/, msg); // v 级别(和 println 一样)	绿
Log.i(tag/*一般是 类名*/, msg); // info 级别 ---- 绿
Log.d(tag/*一般是 类名*/, msg); // debug 级别 ---- 蓝
Log.w(tag/*一般是 类名*/, msg); // warn 级别 ---- 黄
Log.e(tag/*一般是 类名*/, msg); // error 级别 ---- 红

文件存储操作

过程:File -> FileStream -> write / read & read_buffer -> close

向SD卡写数据需要存储权限
AndroidManiFest 增加权限:android.permission.WRITE_EXTERNAL_STORAGE

txt读写例程:绝对路径

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
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileInputStream;

public class UserInfoUtiles
{

// 写入到文件
public static boolean saveInfo(String username, String password)
{

try
{
String result = username + "&" + password;

// 创建 File类 指定数据存储的位置
File file = new File("/data/data/包名/info.txt");
// 创建一个文件输出流
FileOutputStream fos = new FileOutputStream(file);
fos.write(result.getBytes());
fos.close();

return true;

}
catch (Exception e)
{
e.printStackTrace();
return false;
}
}

// 从文件读出
public static Map<String, String> readInfo()
{

try
{
Map<String, String> maps = new HashMap<String, String>();
File file = new File("/data/data/包名/info.txt");
FileInputStream fis = new FileInputStream(file);
BufferedReader bufr = new BufferedReader(new InputStreamReader(fis));
String content = bufr.readLine(); // 读取一行

// 分割文本
String[] splits = content.split("&");
String name = splits[0];
String pwd = splits[1];

// 把数据放到 Map 中
maps.put("username", name);
maps.put("password", pwd);

fis.close();
return maps;
}
catch (Exception e)
{
e.printStackTrace();
}

return null;
}
}


// 调用存入
if (UserInfoUtils.saveInfo(username, password))
{
...
}

// 调用取出
Map<String, String> maps = UserInfoUtils.readInfo();
if (maps != null)
{
String name = maps.get("username");
String pwd = maps.get("password");
...
}

API 获取内部路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 使用API获取路径
public static boolean saveInfo(Context context, String username, String password)
{

// path = "/data/data/包名/files/"
String path = context.getFilesDir().getpath();
// 用 path/info.txt 代替原来的 路径常量
File file = new File(path, "info.txt");

// 和前面一样的
FileOutputStream fos = new FileOutputStream(file);
fos.write(result.getBytes());
fos.close();
}


// 调用
UserInfoUtils.saveInfo(MainActivity.this, username, password);
UserInfoUtils.readInfo(MainActivity.this);

context 获取内部路径

1
2
3
4
5
// 使用上下文直接获取 FileOutputStream (类似API)
FileOutputStream fos = context.openFileOutput("info,txt", 0/*模式,4种*/);

fos.write(result.getBytes());
fos.close();

Environment 获取SD卡路径

1
2
3
// sdPath = "/mnt/sdcard"
String sdPath = Environment.getExternalStorageDirectory().getPath();
File file = new File(sdPath, "info.txt");

判断SD卡是否可用

1
2
3
if ( Environment.MEDIA_MOUNTED.equals( Environment.getExternalStorageState() ) ) {
Toast.makeText(getApplicationContext(), "SD卡可用", 1).show();
}

获取SD卡的可用空间

1
2
3
4
5
6
7
File file = Envirinment.getExternalStorageDirectory();
long total = file.getTotalSpace(); // 总空间(单位:byte)
long usable = file.getUsableSpace(); // 可用空间(单位:byte)

//转换数据格式为可视化文本
String Total = Formatter.formatFileSize(this, total); // this 是当前的 MainActivity
String Usable = Formatter.formatFileSize(this, usable); // 每大于900则除以1024

文件权限

四种模式

  • MODE_PRIVATE — 值为0
  • MODE_APPEND —
  • MODE_WORLD_READABLE r–
  • MODE_WORLD_WRITEABLE -w-

Linux文件权限表示:十位数

  • 第一位:文件的类型 ( 例如:文件夹 d )
  • 2-4位:用户的权限
  • 5-7位:用户所在的组的权限
  • 8-10位:其他用户的权限

r w x

  • r 可读
  • w 可写
  • x 可执行

rw- <=> 110 <=> 6

改变文件权限

Linux下的chhmod命令:
chmod 764 a.txt // 文件权限为:rwx rw- r–
​ 111 110 100 == 7 6 4

XML

保存设置文件 SharedPreferences

(必须得会)

Sharedpreferences setting = getSharedPreferences(name, mode);
name:任意 mode:同上,4种模式
会自动生成:/data/data/包名/shared_prefs/name.xml 文件

1
2
3
4
5
6
7
8
9
10
11
SharedPreferences sp = new SharedPreferences("config", 0); // 文件名为 config.xml  模式为 private 0
// 若改成"config.txt", 则会生成 config.txt.xml 文件

// 获取 sp 的编辑器
Editor edit = sp.edit();
edit.putString("username", username); // key, value
edit.putInt("Age", age); // edit.putXXX 有多种类型可选
edit.putFloat("PI", 3.14);

// 记得关闭 edit (提交)
edit.commit();

SharedPreferences.getString(key, defaultValue /*默认的,找不到时*/ );

1
2
SharedPreferences sp = getSharedPreferences("config", 0);
sp.getString("username", "");

对应的 xml 内容

1
2
3
4
5
6
<?xml version="1.0" encodeing="UTF-8" standalone="true"?>
<map>
<string name="username">myname</string>
<string name="Age">20</string>
...
</map>

手动构建 xml 文件

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
public void build_xml()
{

// 声明数组(其中 Sms 是自定义 class)
List<Sms> smsLists = new ArrayList<Sms>();

//创建 sb 对象
StringBuffer sb = new StringBuffer();

// 开始组拼 xml 文件头(注意转移双引号)
sb.append("<?xml version=\"1.0\" encodeing=\"UTF-8\" ?>");

// 开始组拼 xml 根节点
sb.append("<smss>");

// 开始组建 xml 节点 (任意循环增加)
for (Sms sms : smsLists)
{
sb.append("<num>");
sb.append("110");
sb.append("</num>");

sb.append("<people>");
sb.append("小明");
sb.append("</people>");
}

sb.append("</smss>");

// 保存到 SD卡 上
try
{
File file = new File(Environment.getExternalStorageDirectory().getPath(), "backup.xml");
FileOutputStream fos = new FileOutputStream(file);
fos.write(sb, .toString().getBytes());
fos.close(); // 关闭流
// 注意写入到SD卡权限:WRITE_EXTERNAL_STORAGE
}
catch ( Exception e)
{
e.printStackTrace();
}
}

xml序列化器 xmlSerializer

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
try
{
// 通过 Xml 类获取 XmlSerializer 类的实例
XmlSerializer sl = Xml.newSerializer();
// 设置参数:XML文件路径
File file = new File(Environment.getExternalStorageDirectory().getPath(), "backup2.xml");
FileOutputStream fos = new FileOutputStream(file);
sl.setOutput(fos, "utf-8"); // 文件流与编码

// 开始写 xml 文档开头
sl.startDocument("utf-8", true/*standalone, 表示是一个独立的xml文件,没有其他文件的约束*/);

// xml 的根节点,namespace = null
sl.startTag(null, "smss");

// 循环写xml数据
for (Sms sms : smsLists)
{
sl.startTag(null, "sms");

sl.startTag(null, "num");
sl.text("110");
sl.endTag(null, "num");

sl.startTag(null, "people");
sl.text("小明");
sl.endTag(null, "people");

sl.endTag(null, "sms");
}

sl.endTag(numll, "smss");

// 写文档结尾
sl.endDocument();
fos.close(); // 关闭流
}

xml的解析

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 目标 xml 文件 -->
<weather>
<channel id="1">
<city>背景</cith>
<temp>25</temp>
</channel>

<channel>
...
</channel>

...
</weather>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 调用:
try
{

// 获取 Assets 的文件
InputStream inputStream = getAssets().open("weather.xml");

List<Channel> weatherlists = WeatherParser.parserXml(inputstream);

StringBuffer sb = new StringBuffer();
for (Channel channel : weatherlists)
{
sb.append(channel.toString());
}

System.out.println(sb.toString());
}
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
// WeatherParser类的parserXml方法:
public static List<Channel> parserXml(InputStream in)
{

try
{
List<Channel> weather: ists = null;
Channel channel = null;

// 获取解析的实例
XmlPullParser parser = Xml.newPullParser();
// 设置解析参数(in是输入流)
parser.setInput(in, "utf-8");
// 获取事件类型
int type = parser.getEventType();
while (type != XmlPullParser.END_DOCUMENT /*==1*/)
{
// 具体判断解析到哪个节点
switch (type)
{
// 具体判断一下 解析到哪个开始标志
case XmlPullParser.START_TAG : // 解析开始标志
if ("weather".equals(parser.getName()))
{
// 创建一个集合对象
weatherLists = new ArrayList<Channel>();
}
else if ("channel".equals(parser.getName()))
{
// 创建Channel对象
channel = new Channel();

// 获取ID:第1个数,下标0
String id = parser.getAttributeValue(0)
channel.setId(id); // 自定义类的方法
}
else if ("city".equals(parser.getName()))
{
// 获取city的数据
String city = parser.getText();
channel.setCity(city);
}
else if ("temp".equals(parser.getName()))
{
// 获取temp的数据
String temp = parser.getText();
channel.setTemp(temp);
}

break;
case XmlPullParser.END_TAG : // 解析结束标志
// 判断要解析的结束标签
if ("channel".equals(parser.getName()))
{
// 把 Javabean 对象存到集合中
weatherLists.add(channel);

}

break;
}

// 不停地向下解析
parser.next();
}

return weatherLists;
}
catch ( Exception e ) { }

}

数据库

sqlite 数据库初始化

SQLiteOpenHelper

SQLiteOpenHelper类 用来管理数据库的创建 ,创建子类来继承它

db.execSQL(string sql) 执行SQL语句

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
import android.content.Context;
import android.database.sqlite.SQLiteDababase;
import android.database.sqlite.SQLiteOpenHelper;

public class MyOpenHelper extends SQLiteOpenHelper {

// 需要添加无参构造方法
public MyOpenHelper(Context context) {
// 创建数据库,上下文、名字、cursor对象(结果集/游标)、数据库版本(从1开始)
super(context, "test.db", null, 1);
}

// 数据库第一次创建的时候用(仅一次),特别适合做表结构的初始化
@Override
public void onCreate(SQLiteDatabase db) {
// 创建表的结构
db.execSQL("create table info( _id integer primary key autoincrement, name varchar(20) )");
// SQLite数据库底层不区分数据类型,但是SQL语句还是得照写声明类别(所有SQL语句都一样)
// integer 也是用 string 来存储,varchar(20) 可以存更长的 string
}

// 数据库升级的时候调用(不能降级,会出错)
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
//例如:升级 info表 添加 phone字段
db.execSQL("alter table info add phone varchar(20)");
}
}

MainActivity部分

数据库路径:/data/data/包名/databases/数据库名字.db

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MainActivity extends Activity
{
private MyOpenHelp myOpenHelper;

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

// 创建数据库(包名),触发 构造方法、onCreate() 事件
myOpenHelper = new MyOpenHelper(getApplicationContext());
// 创建(第一次)或打开数据库
SQLiteDatabase sqLiteDatabase = myOpenHelper.getWriteableDatabase();
// 创建(第一次)或打开数据库;如果磁盘已满则返回只读
// SQLiteDatabase readableDatabase = myOpenHelper.getReadableDatabase();
}
}

执行SQL语句

execSQL

void execSQL(sql, bindArgs); // 修改语句,无返回值

SQLiteDatabase execSQL(sql); // 查找语句,有返回值

lite
1
2
3
4
insert into info(name, phone) values("Tom", "110"); -- 两个斜杠表示注释
delete from info where name = "Tom"l
update info set phone = "120" where name = "Tom";
select name, phone from info;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 执行 SQL语句
public void onClick(View v)
{
// 获取数据库对象
SQLiteDatabase db = myOpenHelper.getWritableDatabase();

// 执行增加一条的SQL语句
db.execSQL("insert into info (name, phone) values(?, ?)", new Object[] {"Tim", "119"});
// 执行删除语句
db.execSQL("delete from info where name=?", new Object[] {"Tom"}/*bindArgs*/);

// 数据库用完需关闭(官方建议关闭,但是不关可提升效率)
db.close();
}

Cursor

  • int getCount() 行数 getColumnCount() 列数
  • String getString(int columnIndex)
  • boolean moveToNext() moveToFirst() moveToLast() moveToPosition(int position) moveToPrevious()
  • String[] getColumnNames() 所有列的名字
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void onClick(View v)
{
SQLiteDatabase db = myOpenHelper.getReadableDatabase(); // 只读就行了

Cursor cursor = db.rawQuery("select * from info", null);
if (cursor != null && cursor.getCount() > 0)
{
while (cursor.moveToNext())
{
String name = cursor.getString(1); // 这一行第二列的值
System.out.println("name:" + name);
}
}
}

SQL优缺点

缺点

  • SQL语句容易写错
  • 没有返回值,结果不容易判断

优点

  • 多表查询(自由、功能强)

sqlite3

命令行SQL语句

开启shell adb shell

定位到目录 # cd /data/data/包名/databases

打开数据库 # sqlite3 数据库名字.db

sqlite> SQL语句 (不包括前面的sqlite>)

改变DOS编码

如果中文乱码,点击cmd属性,看编码是否为UTF-8(默认GBK)

改成GBK chcp 936

改成UTF-8 chcp 65001

chcp: char change page

谷歌封装好的数据库API

原理是组拼SQL语句

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
public void onClick(View v)
{
SQLiteDatabase db = myOpenHelper.getWritableDatabase();

ContentValues values = new ContentValues();
values.put("name", "Tom");
values.put("phone", "110");

// 添加记录:表名、null、map数组:名字=>值。返回 long -1失败 或 行号(1开始)
long num = db.insert("info", null, values);

// 删除记录:表名、条件、条件值。返回 int 影响的行数, 0 为没有删除
int num = db.delete("info", "name=?", new String[] {"Tom"});

// 更新记录:表名、map数组、条件、条件值。 返回 int 影响的行数
int num = db.update("info", values, "name=?", new String[] {"Tom"});

// 查询:表名、查询的列String[](null为全部列)、条件、条件值String[]、分组、过滤条件、排序
// 选择查询的列(参数二):new String[]{"name", "phone"}
Cursor cursor = db.query("info", null, "name=?", new String[] {"Tom"}, null, null, null);
if (cursor != null && cursor.getCount() > 0)
{
while(cursor.moveToNext())
{
String name = cursor.getString(0); // 第一列(name)
String phone = cursor.getString(1); // 第二列(phone)
System.out.println("name=" + name + " phone=" + phone);
}
}

db.close();
}

优点

  • 写法简单
  • 有返回值

缺点

  • 多张表不容易查询

数据库的事物

例如:转账,需要取出、存入同时成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void onClick(Veiw v){
SQLiteDatabase db = myOpenHelper.getReadableDatabase();

// 使用事物进行操作
db.beginTransaction();
try{
// 实现逻辑,即SQL语句
db.execSQL("update ac_mon set money = money - 100 where name = ?", new String[]{"userA"});
/* int i = 10 / 0; // 运行中断 */
db.execSQL("update ac_mon set money = money + 100 where name = ?", new String[]{"userB"});

// 给当前事物设置一个成功的标记
db.setTansactionSuccessful();
} catch (Exception e) {
Toast.makeText(getApplicationContext(), "服务器忙,请稍后再试", 1).show();
} finally {
db.endTransaction(); // 关闭事物
}
}

ListView

使用方法

1、布局:activity_main

1
2
3
4
5
6
<ListView
android:id="@+id/lv"
android:layout_witdh="match_parent"
android:layout_height="match_parent"
android:fastScrollEnabled="true"> <!--快速滚动条-->
</ListView>

2、设置Adapter

连接到数据适配器:setAdapter()

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle saveInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// 找到控件
ListView lv = (ListView) findViewById(R.id.lv);

// 显示数据(数据来源于数据适配器)
lv.setAdapter(new MyListAdapter());
}
}

3、实现BaseAdapter方法

BaseAdapter

可以放到上面的 MainActivity 类的代码里面,作为类中类

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
private class MyListAdapter extends BaseAdapter {

// 项目数量
@Override
public int getCount() {
return 4; /*item数量,自定义*/
}

// 对应的对象
@Override
public Object getItem(int position) {
return null;
}

// 对应的ID
@Override
public long getItemID(int position) {
return 0;
}

//获取一个view,用来显示每个item的数据
@Override
public View getView(int position, View convertView, ViewGroup parent) {
TextView tv = new TextView(MainActivity.this/*当前类*/);
tv.setText("索引:" + position);
return tv; // return null 的话会报错
}
}

错误提示

Attempt to invoke virtual method 'int android.view.View.getImportantForAccessibility()' on a null object reference

原因是 getView() 返回值为空……

结果示例

getCount()控制条目数量。当数量=4时:

1
public int getCount() { return 4; }

结果:

1
索引:0
1
索引:1
1
索引:2
1
索引:3

每个item都是一个TextView,其由getView()方法决定

显示数据的原理

MVC

Javaweb:

  • m : mode 数据
  • v : view 视图 jsp
  • c : controller 控制 sevlet

Android:

  • m : mode 数据(javabean)
  • v : listview
  • c : adapter

ListView优化

getView() 方法说明

只有在屏幕上显示(包括只显示一点点)的item才会触发getView()方法。
滚动时新显示的条目也会触发。
快速滚动加载大量View可能会导致内存溢出(Out Of Memory)。

convertView 缓存机制

item并不是滚动完就销毁。
使用 convertView 历史缓存可避免内存溢出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public View getView(int position, View convertView, ViewGroup parent)
{

TextView tv;

if (convertView == null)
{
// 缓存对象为空,创建新的 view 对象
tv = new TextView(MainActivity.this);
}
else
{
// 使用历史缓存对象
tv = (TextView) convertView;
}

tv.setText("索引:" + position);

return tv;
}

复用历史缓存对象:一开始时是真的创建可见范围内的对象,滚动上去后,新出现(底部)的对象实际上是复用已经加载好但是看不见了的(顶部)的对象。根据 position 调用 getView() 方法。

例如,快速滚动时加载100个item,不用缓存需要创建100个TextView。使用 convertView 后,只需要设置TextView的值就好了。

List高度设置

能否设置成 layout_height="wrap_content"

1
2
3
<ListView
android:layout_height="match_parent"> <!--这是正常的-->
</ListView>

改成android:layout_height="wrap_content"后,无法确定高度,校验很多次,重复调用 getView() 方法,严重影响性能。

复杂的ListView

┍━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┑
│ ┌───────┐ 这里是项目标题,字…
│ │ Image │ 这个部分是项目相对应
│ └───────┘ 的信息内容
┕━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┙

1、Layout:activity_main

1
2
3
4
5
<ListView
android:id="@+id/lv"
android:layout_witdh="match_parent"
android:layout_height="match_parent">
</ListView>

2、Layout : item(xml)

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
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_witdh="match_parent"
android:layout_height="match_parent" >

<ImageView
android:id="@+id/iv_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_launcher" />

<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent" // 左边与父容器右边的距离
android:layout_height="wrap_content"
android:layout_toRightOf="@id/iv_icon" // 图片右边
android:layout_marginTop="3dp" // 顶部留空
android:singleLine="true" // 单行显示
android:ellipsize="end" // 显示开头,末尾三个点。start值相反
androi:textColor="#000000"
android:textsize="20sp"
android:text="这个里项目标题,字体加粗加黑" />

<TextView
android:id="@+id/tv_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/iv_icon" // 图片右边
android:layout_below="@id/tv_text" // 标题下面
android:singleLine="true"
android:ellipsize="end"
androi:textColor="#666666"
android:textsize="15sp"
android:text="这个里项目标题,字体加粗加黑" />
</RelativeLayout>

3、BaseAdapter:打气筒

View View.inflate(context, resource, root) 方法(打气筒)
介绍:创建新的View对象,把布局资源(xml 文件)折换成一个 View 对象,放到父容器中,并返回这个 View 对象

  • context : getApplicationContext(),或 this
  • resource : 定义的布局文件 R.id.xxx
  • root : null,或者待放入的容器(ViewGroup)
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
public class MainActivity extends Activity
{
@Override
protected void onCreate(Bundle saveInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// 找到控件
ListView lv = (ListView) findViewById(R.id.lv);

// 显示数据(数据来源于数据适配器)
lv.setAdapter(new MyListAdapter);
}

private class MyListAdapter extends BaseAdapter
{

// 项目数量
@Override
public int getCount()
{
return 4; /*item数量,自定义*/
}

// 对应的对象
@Override
public Object getItem(int position)
{
return null;
}

// 对应的ID
@Override
public long getItemID(int position)
{
return 0;
}

//获取一个view,用来显示每个item的数据
@Override
public View getView(int position, View convertView, ViewGroup parent)
{

View view;
if (convertView == null)
{
// 获取打气筒服务
view = View.inflate(getApplicationContext, R.id.item, null);
}
else
{
// 复用历史缓存对象
view = (TextView) convertView;
}
//根据列表设置控件数据
//TextView tv = (TextView) view.findViewById(R.id.tv);
//view.setText(xxLists.get(position).getXx());
return view;
}
}
}

获取 XML 布局里面的控件:view.findViewById(),不能直接 findViewById()

打气筒的三种写法

打气筒写法一:View.inflate()

1
view = View.inflate(getApplicationContext, R.id.item, null); // 用法见上

打气筒写法二:LayoutInflater.from()

1
view = LayoutInflater.from(getApplicationContext()).inflate(R.layout.item, null);

打气筒写法三:getSystemService()
据说实际上用得比较多

1
2
LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
view = inflater.inflate(R.layout.item, null);

其他写法:不常见

ArrayAdapter

布局:activity_main

1
2
3
4
<ListView
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</ListView>

布局:item

1
2
3
4
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</TextView>

MainActivity

ArraryAdapter<String>(context, resource, objects)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import android.R.array;

public class MainAcitivity extends Activity
{

String names[] = { "111", "222", "333", "444" };

@Override
protected void onCreat(Bundle savedInstanceState)
{

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// 找到控件
ListView lv = (ListView) findViewById(R.id.lv);

// 创建 ArrayAdapter : context, resource(只有TextView的xml文件), 数据数组
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.id.item, names);

// 设置数据适配器
lv.setAdapter(adapter);
}
}

结果

1
2
3
4
111
222
333
444

重载的四个参数的ArrayAdapter用法:

ArraryAdapter<String>(context, resource, textViewResourceId, objects)

textViewResourceId 为 resource 布局文件里面的某个特定的 TextView 的 ID

1
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.id.item, R.id.tv, names);
1
2
3
4
<RelativeLayout ...>
<TextView
android:id="@+id/tv" ... />
</RelativeLayout>

SimpleAdapter

┍━━━━━━━━━━━━━━━━━━━━━┑
│┌───────┐┌───────┐│
││ name │ │ phone ││
│└───────┘└───────┘│
┕━━━━━━━━━━━━━━━━━━━━━┙

布局:activity_main

1
2
3
<RelativeLayout ...>
<ListView ... />
</RelativeLayout>

布局:item

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<LinearLayout xmlns="..."
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizental">

<TextView
android:id="@+id/tv_name"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content" />

<TextView
android:id="@+id/tv_phone"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content" />

</LinearLayout>

代码:MainActivity

SimpleAdapter adapter = new SimapleAdapter(context, data, resource, from, to); // 参数长的那个

  • dataList<Map<String, String>> 类型数据
  • resource 是 布局文件(上面的 item)
  • from 是 String[] 类型,表示 Map<String, String> 的键
  • to 是 int[] 类型数组,与from 对应的布局文件中的 TextView 的 ID
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 找到控件
ListView lv = (ListView) findViewById(R.id.lv);

// 准备数据
List<Map<String, String>> data = new ArrayList<Map<String, String>>();

Map<String, String> map1 = new HashMap<String, String>();
map1.put("name", "Tom");
map1.put("phone", "110");

Map<String, String> map2 = new HashMap<String, String>();
map2.put("name", "Alice");
map2.put("phone", "120");

data.add(map1);
data.add(map2);

// 设置数据适配器
SimpleAdapter adapter = new SimapleAdapter(getApplicationContext(), data, R.id.item,
new String[]{ "name", "phone" }, new String[]{ R.id.tv_name, R.id.tv_phone });
lv.setAdapter(adapter);

用ListView展示数据库的数据

┍━━━━━━━━━━━━━━━━━━━━━┑
│┌───────┐┌───────┐│
││ name │ │ phone ││
│└───────┘└───────┘│
┕━━━━━━━━━━━━━━━━━━━━━┙

布局:activity_main

1

布局:item

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<LinearLayout xmlns="..."
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizental">

<TextView
android:id="@+id/tv_name"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content" />

<TextView
android:id="@+id/tv_phone"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content" />

</LinearLayout>

类:Person

1
2
String name, phone;

MainActivity

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
public class MainAcitivity extends Activity
{

// 找到控件
ListView lv = (ListView) findViewById(R.id.lv);

// 整理数据
List<Person> lists = new ArrayList<Person>();

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


public void onClick(View v)
{

SQLiteDatabase db = myOpenHelper.getWritableDatabase();

// 查询:表名、查询的列String[](null为全部列)、条件、条件值String[]、分组、过滤条件、排序
//Cursor cursor = db.query("info", null, "name=?", new String[]{"Tom"}, null, null, null);
Cursor cursor = db.query("info", null, null, null, null, null, null); // 查询所有
if (cursor != null && cursor.getCount() > 0) // 如果有数据
{
while(cursor.moveToNext())
{
String name = cursor.getString(0); // 第一列(name)
String phone = cursor.getString(1); // 第二列(phone)

Person person = new Person(name, phone);
lists.add(person);
}

// 设置数据适配器
lv.setAdapter(new MyListAdapter);
}
db.close();
}


private class MyListAdapter extends BaseAdapter
{

// 项目数量
@Override
public int getCount()
{
return lists.size(); // 返回列表(数据库)中的数据数量
}

// 对应的对象
@Override
public Object getItem(int position)
{
return null;
}

// 对应的ID
@Override
public long getItemID(int position)
{
return 0;
}

//获取一个view,用来显示每个item的数据
@Override
public View getView(int position, View convertView, ViewGroup parent)
{

TextView view;
if (convertView == null)
{
// 缓存对象为空,创建新的 view 对象
view = View.inflate(getApplicationContext, R.id.item, null);
}
else
{
// 使用历史缓存对象
view = (TextView) convertView;
}

Person person = lists.get(position); // 获取从数据库中取出的列表对象
// findViewById() 从当前 activity 中获取 ID,无法直接取到 layout/item 里面的 ID,
// 所以需要先用 inflate() 获取到 R.id.item 的 view,再通过 view 获取 ID
TextView tv_name = (TextView) view.findViewById(R.id.tv_name);
TextView tv_phone = (TextView) view.findViewById(R.id.tv_phone);
tv_name.setText(person.getName()); // 设置名字
tv_phone.setText(person.getPhone()); // 设置电话

return view;
}
}
}

网络

网页源码查看器

联网权限

android.permission.INTERNET

HttpUrlConnection 类

用于发送或者接受数据,取网页源代码

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
// 设置网址
String path = "http://www.baidu.com";
URL url = new URL(path);

try
{
// HttpURLConnection对象,用于发送或者接受数据
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();

// 设置发送 get 请求(默认),注意要求【大写】
urlConnection.setRequestMethod("GET");
// 设置请求超时
urlConnection.setConnectTimeout(5000); // 5 秒
// 获取服务器返回的状态码,例如 404
int code = urlConnection.getResponseCode();

if (code == 200)
{
// 获取服务器返回的数据流
InputStream in = urlConnection.getInputStream();

// 数据流转换成文本,最终的网页源代码
String content = StreamTools.readStream(in);
}
}


// 流转文本的方法
public static class StreamTools
{
public static String readStream(InputStream in)
{
int len = -1;
byte[] buffer = new byte[1024]; // 1 kb
// 定义一个内存输出流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ( ( len = in.read(buffer) ) != -1 )
{
baos.write(buffer, 0, len);
}
in.close();
String content = new String(baos.toByteArray());

return content;
}
}

ScrollView

scrollview 只能有一个子控件

布局

1
2
3
4
5
6
7
8
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/tv"/>

</ScrollView>

如果要多个子控件,可以将这些子控件包裹在 <LinearLayout> 里面,orientation="vertical"

消息机制的写法

主线程(UI线程)

ANR Application not response 应用无响应

耗时的操作放到子线程当中

Android 4.0 之后,谷歌强制要求连接网络不能再主线程进行访问

只有主线程(UI线程)才可以更新UI

Handler 使用步骤

1、在主线程定义一个 Handler

1
private Handler handler = new Handler();

2、使用 Handler,重写里面的 handlerMessage 方法

1
public void handleMessage(android.os.Message msg) { }

3、用 Handler 去子线程发消息

1
handler.sendMessage(msg);

4、handleMessage 方法就会执行,更新UI

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
public class MainActivity extends Activity
{

TextView tv = (TextView) findVIewById(R.id.tv);

// 在主线程中定义一个Handler,类后跟{}表示重写类的方法
private Handler handler = new Handler()
{

// 收到消息。这个方法是在主线程里执行的,可以更新 UI
public void handleMessage(android.os.Message msg)
{
String content = (String) msg.obj;
tv.setText(content);
};

}

public void click(View v)
{
// 创建一个子线程
new Thread()
{
public void run()
{
try
{
// 设置网址
String path = "http://www.baidu.com";
URL url = new URL(path);

// 发送、接收数据的对象
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
// 设置GET请求
urlConnection.setRequestMethod("GET");
// 设置超时时间
urlConnection.setConnectTimeout(5000);
// 获取服务器返回的状态码
int code = urlConnection.getResponseCode();

if (code == 200)
{
// 输入流
InputStream in = conn.getInputStream();
// 流转字符串
String content = StreamTools.readStream(in);

// 创建 Message 对象
Message msg = new Message();
msg.obj = content;
// Handler 发送msg(包含数据)于更新UI
handler.sendMessage(msg);
} //if

} //try

}
} .start(); //thread run

} //function

}

消息机制的原理

消息队列,有一个Looper不断读取队列,然后 handlerMessage(msg)

Handler 完善:what

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
protected final int REQUESTSUCESS = 0;
protected final int REQUESTNOTFOUND = 1;
protected final int REQUESTEXCEPTION = 2;

public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case REQUESTSUCESS : // 代表请求成功
String content = (String) msg.obj;
tv.setText(content);
break;
case REQUESTNOTFOUND : // 代表请求失败
Toast.makeText(getApplicationContext(), "请求资源不存在", 0).show();
break;
case REQUESTEXCEPTION : // 代表产生异常
Toast.makeText(getApplicationContext(), "服务器忙,请稍后再访问", 0).show();
break;
}
}

try{
//...
if (code == 200)
{
Mseeage msg = new Message();
msg.what = REQUESTSUCESS; // 代表哪条消息
handler.sendMessage(msg);
}
else
{
Mseeage msg = new Message();
msg.what = REQUESTNOTFOUND; // 代表哪条消息
handler.sendMessage(msg);
}
} catch (Exception e) {
Mseeage msg = new Message();
msg.what = REQUESTEXCEPTION; // 代表哪条消息
handler.sendMessage(msg);
}

程序运行错误,尽量提示用户:服务器忙,请稍后访问

而不是:程序运行错误,让用户担忧

图片查看器

ImageView 显示网络图片

1
2
3
<manifest>
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
1
2
3
4
5
6
<EditView />
<Button />
<ImageView
android:id="@+id/iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
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
private Handler handler = new Handler()
{
//收到消息,设置图片
public void handleMessage(android.os.Message msg)
{
bitmap bitmap = (Bitmap) msg.obj;
iv.setImageBitmap(bitmap);
}
}

public void click(View v)
{

new Thread()
{
public void run()
{
try
{
// 使用路径创建 URL 对象
String path = ev.getText().toString().trim();
URL url = new URL(path);
// 获取 HttpURLConnection(用户发送或者接收数据)
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
// 设置请求的方式、超时时间
conn.setRequestMethond("GET");
conn.setConnectTimeout(5000);
int code = conn.getResponseCode();
if (code == 200)
{
// 获取图片的数据(流 的形式)
InputStream in = conn.getInputStream();
// 通过位图工厂 获取 bitmap
Bitmap bitmap = BitmapFactory.decodeStream(in);
// 发送消息用于更新UI
Message msg = Message.obtain(); // 使用 Message 的静态方法 可以减少对象的创建
msg.obj = bitmap;
handler.sendMessage(msg);
}
else
{
// ...
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
} .start();

}

使用 Message 的静态方法 可以减少对象的创建

Message msg = Message.obtain();

效果等同于:Message msg = new Message() 但是效率快

图片缓存到本地

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (code == 2) {
//图片的数据流
InputStream in = conn.getInputStream();

File file = new File(getCacheDir(), "test.png"); // 谷歌提供的缓存目录
FileOutputStream fos = new FileOutputStream(file);
int len = -1;
byte[] buffer = new byte[1024]; // 1kb
while ((len = in.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
fos.close();
in.close();
}

使用缓存图片

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
new Thread()
{
public void run()
{
try
{
// 谷歌提供的缓存目录:/data/data/包名/cache/ + test.png
File file = new File(getCacheDir(), "test.png");
if (file.exists() && file.length() > 0)
{
// 使用缓存的图片
Bitmap cacheBitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
// 把 cacheBitmap 显示到 ImageView 上
Message msg = Message.obtain();
msg.obj = cacheBitmap;
handler.sendMessage(msg);
}
else
{
// 第一次访问,联网获取数据
String path = ev.getText().toString().trim();
URL url = new URL(path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethond("GET");
conn.setConnectTimeout(5000);
int code = conn.getResponseCode();

if (code == 200)
{
InputStream in = conn.getInputStream();

FileOutputStream fos = new FileOutputStream(file);
int len = -1;
byte[] buffer = new byte[1024]; // 1kb
while ((len = in.read(buffer)) != -1)
{
fos.write(buffer, 0, len);
}
fos.close();
in.close();
}
}
}
}
}

对图片进行加密

Base64加密: Base64.encodeToString(byte[] input, int flags);

1
File file = new File(getCacheDir(), Base64.encodeToString(path.getBytes[], Base64.DEFAULT));

上述代码针对文件路径进行加密,文件名为URL路径加密后的密文

cache 和 filedir 区别

写出文件示例

1
2
3
4
5
6
7
8
9
10
11
try {
// firledir:/data/data/包名/files/info.txt
FileOutputStream fos = openFileOutput("info.txt", 0);

// cache:/data/data/包名/cache/info.txt
File file = new File(getCacheDir(), "info.txt");
FileOutputStream fos = new FileOutputStream(file);

fos.write("haha".getByte());
fos.close();
}

Thread API

runOnUiThread 写法

runOnUiThread(Runable action)

不管在什么位置,里面运行的语句都运行在 UI 线程上。

1
2
3
4
5
6
7
8
9
new Thread() { public void run() {
// ...
runOnUiThread(new Runnable() {
public void run() {
tv.setText("HHH");
}
});
// ...
} }

Handler API

延迟线程 new Handler().postDelayed(Runnable r, int delayMillis); 效果等同于 Sleep()

这个方法执行在 UI 线程里,可更新 UI

1
2
3
4
5
6
7
8
9
10
11
public void f() {

// 2秒后执行 run 方法
new Handler().postDelayed( new Runnable(){
@Override
public run() {
// ...code...
}
}, 2000);

}

定时器 Timer

Timer.schedule(TimerTask task, long delay); 延迟后执行

Timer.schedule(TimerTask task, long delay, long period); 指定的延迟后进行重复的固定延迟执行

还有其它重载的 schedule 函数

相当于子线程,不能用来更新 UI。可以用 runOnUiThread 来执行 UI 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void click() {

Timer timer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
// ... code...
// 这相当于子线程,不能更新 UI
}
}

timer.schedule(task, 5000);

}

Timer.cancel(); Task.cancel(); 销毁一个执行

1
2
3
4
5
6
// 当 Activity 销毁时 会执行这个方法
protected void onDestroy() {
timer.cancel();
task.cancel();
super.onDestroy();
}

网络基础

网络图片 SmartImageView

1
2
3
<com.loopj.android.image.SmartImageView
android:id="@+id/siv"
... />
1
2
3
4
5
6
7
8
imageUrl = "...";
view = View.inflate(...);
SmartImageView svi = (SmartImageView) view.findViewById(R.id.siv);
siv.setImageUrl(imageUrl); // 设置加载网络图片
siv.setImageUrl(String imageUrl, Integer fallbackResource); // 参数2为加载失败的内容 R.drawable.xxx
siv.setImageView(String imageUrl, Integer OnCompleteListener completeListener); // 结束后回调事件
siv.setImageView(String imageUrl, Integer fallbackResource, OnCompleteListener completeListener);
siv.setImageView(String imageUrl, Integer fallbackResource, Integer loadingResource, OnCompleteListener completeListener);

GitHub 开源项目

网址:github.com

导入开源项目:下载 zip 后,把解压出来后的 com 文件夹(源码包)复制到项目的 src 中

源码:在 com 文件夹内,.java 后缀名的

布局:用的时候,需要用开源项目的完整包名

HttpURLConnextion

联网的基类。

如果连接要求不是很高,只是用来发发数据的话,可以用下面的开源项目。

封装 toast 方法

通用方法,获取数据后立即弹出提示

1
2
3
4
5
6
7
8
public void showToast(final String content) {
runOnUiThread(enw Runnable() {
@Override
public void run() {
Toast.makeText(getApplicationContext(), content, 1).show();
}
})
}

GET 和 POST 的区别

  1. URL路径不同:GET 拼接 网址 和 数据
  2. POST 通过请求体(流)的形式把数据发送给服务器
  3. POST 比 GET 多了两个头信息:content-length 和 content-type

使用 POST

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
String path = "http://xxx";
String data = "name=" + name + "&pass=" + pass;
URL url = new URL(path);
HttpURLConnextion conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethond("POST");
conn.setConnextTimeout(5000);
//比GET方式多的两个头信息
conn.setRequestProperty("Content-Type", "application/x-www-form-url");
conn.setRequestProperty("Content-Length", data.length()+""); // 参数二要求字符串,故 数字+空字符串
//以流的形式提交数据
con.setDoOutput(true); // 设置一个标记,允许输出
conn.getOutputStream().write(data.getBytes());
//判断结果
int code = conn.getReponseCode();
if (code == 200) {
// 获取返回的数据流 -> 转化成字符串 -> 操作字符串数据
InputStream inputStream = conn.getInputStream();
String content = StreamTools.readStream(inputStream);
showToast(content);
}

乱码问题

Android 编码:UTF-8

服务器编码:本地服务器的编码,一般为 iso-859-1 (类似 GBK)

改变服务器编码(Java):

返回中文乱码 System.out.prrintln("ans : " + new String(ans.getBytes("utf-8")));

发送中文乱码:先以 iso-8859-1 编码,再以 UTF-8 解码

`System.out.prrintln("ans : " + new String(ans.getBytes("iso-8859-1"), "utf-8"));`
改变 Android 编码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class StreamTools
{
//把输入流转换成字符串
public static String readStream(InputStream in) throws Exception
{
//定义一个内存输出流
ByteArrayOutputStream baos = new ByteArrayOutStream();
int len = -1;
byte[] buffer = new byte[1024]; // 1 kb
while ( (len = in.read(buffer)) != -1 )
{
baos.write(buffer, 0, len);
}
in.close();
String content = new String(baos.toByteArray(), "gbk"); // 指定 GBK 编码
return content;
}
}
URLEncode 类

encode方法: URLEncode.encode(String s, String charseName);

path = path + "?username=" + URLEncode(name, "utf-8") + "&password=" + URLEncode(pass, "utf-8");

HttpClient 方式提交数据

HttpClient 是一个接口,而不是类。(没有人用这个,了解)

GET

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
new Thread()
{
public void run()
{
try
{
String path = "http://xxx?id=" + URLEncoder.encode(id);

// 获取 httpclient 实例
DefaultHttpClient client = new DefaultHttpClient();
// 准备 get 请求,定义一个 httpget 实现
HttpGet get = new HttpGet(path);
// 执行一个 get 请求
HttpResponse response = client.execute(get);
// 获取服务器返回的状态码
int code = response.getStatusLine().getStatusCode();
if (code == 200)
{
// 获取服务器返回的数据,以流的形式
InputStream inputstream = response.getEntity().getContent();
// 把流转换成字符串
String content = StreamTools.readStream(inputStream);
showToast(content);
}
}
}
} .start();

POST

Entity、BasicNameValuePair 等也都是接口

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
String name = "...", pwd = "...";
String path = "...";
DefaultHttpClient client = new DefaulthttpClient();
HttpPost post = new HttpPost(path);

// 准备 parameters 集合
List<NameValuePair> lists = new ArrayList<NameValuePair>();
// 准备 NameValuePair,键值对数据
BasicNameValuePair nameValuePair = new BasicNameValuePair("username", name);
BasicNameValuePair pwdValuePair = new BasicNameValuePair("password", pwd);
// 把 name 和 pwd 加入到集合中
lists.add(nameValuePair);
lists.add(pwdValuePair);
// 准备 entity
UrlEncodeFormEntity entity = new UrlEncodeFormEntity(parameters);
// 准备 post,以实体 Entity 的形式
post.setEntity(entity);

HttpResponse response = client.execute(post);

int code = response.getStatusLine().getStatusCode();
if (code == 200) {
InputStream inputStream = response.getEntity().getContent();
String content = StreamTools.readStream(inputStream);
showToast(content);
}

asyncHttpClient 开源项目

GitHub 上下载,把 com 包复制到 src 中。

GET

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
String path = "...";
AsyncHttpClient client = new AsyncHttpClient();

client.get(path, new AcynchttpResponseHandler()
{
// 请求成功的回调方法0
/*@Override
public void onSuccess(String response) {
showToast(response);
}*/

// 请求成功的回调方法
@Override
public onSuccess(int statusCode, Header[] headers, byte[] responseBody)
{
showToast(new String(responseBody, "gbk"));
}

// 请求失败的回调方法
@Override
public void onFailure(int statusCode, Header[] headers, byte[] responseBody, Throwable error)
{
// ...code...
}
});

POST

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
String path = "...";
AsyncHttpClient client = new AsyncHttpClient();

// 准备请求体的内容
RequestParams params = new RequestParams();
params.put("username", "admin");
params.put("password", "123");
params.put("email", "my@email.com");
params.put("profile_picture", new FIle("pic.jpg")); // 上传文件
params.put("profile_picture2", someInputStream); // 上传输入流
params.put("profile_picture", new ByteArrayInputStream(someBytes)); // 上传字节集

client.post(path, params, new AcynchttpResponseHandler() {

// 请求成功的回调方法
@Override
public onSuccess(int statusCode, Header[] headers, byte[] responseBody) {
showToast(new String(responseBody, "gbk"));
}

// 请求失败的回调方法
@Override
public void onFailure(int statusCode, Header[] headers, byte[] responseBody, Throwable error){
// ...code...
}
});

多线程下载

原理

分段下载

服务器没有限速的话,线程并不是越多越好

获取一部分文件,返回的状态码是206,而不是200

RandomAccessFile 类

随机读取和写入文件,使用见下方代码

代码

test.java

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
import java.io.*;
import java.net.*;

class test
{
private static String path = "http://download.dcloud.net.cn/HBuilder.9.0.1.windows.zip";
private static int threadCount = 3; // 线程数

public static void main(String[] args)
{
try
{
URL url = new URL(path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
int code = conn.getResponseCode();
if (code == 200)
{
// 获取服务器文件的大小(B)
int length = conn.getContentLength();

// 创建一个和下载文件一样大的文件,提前申请空间
RandomAccessFile raf = new RandomAccessFile(getFileName(path), "rw");
raf.setLength(length);

// 算出每个线程下载的大小
int blockSize = length / threadCount;

// 计算每个线程开始位置和结束位置
for (int i = 0; i < threadCount; i++)
{
int startIndex = i * blockSize; // 开始位置
int endIndex = (i + 1) * blockSize - 1;
if (i == threadCount - 1) // 最后一个线程
{
endIndex = length - 1;
}

// 开启线程去下载
DownLoadThread dlt = new DownLoadThread(path, startIndex, endIndex, i);
dlt.start();
}
}
}
catch (Exception e)
{
e.printStackTrace();
}
}

public static String getFileName(String path)
{
int start = path.lastIndexOf("/") + 1;
return path.substring(start);
}
}

DownLoadThread.java

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
import java.io.*;
import java.net.*;

// 定义下载文件的线程
public class DownLoadThread extends Thread
{
private String path;
private int startIndex;
private int endIndex;
private int threadId;

DownLoadThread(String path, int startIndex, int endIndex, int threadId)
{
this.path = path;
this.startIndex = startIndex;
this.endIndex = endIndex;
this.threadId = threadId;
}

@Override
public void run()
{
try
{
URL url = new URL(path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
// 设置一个请求头Range,获取每个线程下载的开始位置和结束位置
conn.setRequestProperty("Range", "bytes=" + startIndex + "-" + endIndex);
int code = conn.getResponseCode();
// 状态码 206 表示请求部分资源成功(一部分文件)
if (code == 206)
{
// 创建随机读写文件对象
RandomAccessFile raf = new RandomAccessFile(getFileName(path), "rw");
// 每个线程从自己的位置开始写
raf.seek(startIndex);

InputStream in = conn.getInputStream();

int len = -1;
byte[] buffer = new byte[1024];
while ((len = in.read()) != -1)
{
raf.write(buffer, 0, len);
}

System.out.println("下载完毕");
raf.close();
}
}
catch (Exception e)
{
e.printStackTrace();
}

}

// 获取文件名
public static String getFileName(String path)
{
int start = path.lastIndexOf("/")+1;
return path.substring(start);
}
}

断点续传

下载过程中保存每个线程的下载位置到文件

保存的间隔越短(缓冲区越小)则越卡顿

1
2
// 正在运行的线程数
runningThread = threadCount;

下载类:

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
/*...code...*/

// 读取上次断开的位置继续下载
File file = new File(threadId + ".txt");
if (file.exists() && file.length() > 0)
{
FileInputStream fis = new FileInputStream(file);
BufferedReader bufr = new BufferedReader(new InputStream(fis));
String lastPositions = bufr.readLine(); // 读出来的是上次下载的位置
int lastPosition = Integer.parseInt(lastPositions);

// 改变一下 startIndex 的位置
startIndex = lastPosition;
fis.close();
}

if (code == 206)
{
// 创建随机读写文件对象
RandomAccessFile raf = new RandomAccessFile("file.exe", "rw");
// 每个线程从自己的位置开始写
raf.seek(startIndex);

InputStream in = conn.getInputStream();

int len = -1;
byte[] buffer = new byte[1024 * 1024]; // 缓冲区每 1M 保存一次文件
int total = 0; // 当前线程已下载的大小

while ((len = in.read) != -1)
{
raf.write(buffer, 0, len);

// 保存下载位置到文件
total += len;
int currentThreadPosition = startIndex + total;
RandomAccessFile raff = new RandomAccessFile(threadId + ".txt", "rwd"); // rwd 同步写入到底层文件中
raff.write(String.valueOf(currentThreadPosition).getBytes());
raff.close();
}
raf.close();

System.out.println("下载完毕");

// 下载完毕后判断并删除保存下载位置的文件
synchronized (DownLoadThread.class) // 加锁?
{
runningThread--;
if (runningThread == 0) // 全部下载完毕
{
for (int i = 0; i < threadCount; i++)
{
File file = new File(threadId + ".txt")
file.delete(); // 删除文件
}
}
}

}

安卓动态添加进度条

进度条布局

1
2
3
4
5
<progressBar xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/progressBar1"
style="?android:attr/progressBarStyleHorizontal
android:layout_width="match_parent"
android:layout_height="wrap_content" />

动态添加进度条

1
2
3
4
5
6
7
8
9
10
11
12
13
14
List<ProgressBar> pbLists = new ArrayList<ProgressBar>(); // 进度条集合,用来取对象
barLayout = (LinearLayout) findViewById(R.id.barLayout); // 进度条线性布局

barLayout.removeAllViews(); // 先移除上次的进度条
pbLists.clear(); // 清除上次的进度条实例

// 添加线程数量的进度条
int threadCount = Integer.parseInt(threadCount);
for (int i = 0; i < threadCount; i++)
{
View pbView = View.inflate(getApplicationContext(), R.id.barItem, null)
pbLists.add(pbView);
barLayout.addView(pbView);
}

安卓多线程下载

与进度相关的控件都可以在子线程更新UI

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
private int PbMax; // 线程下载的最大值
private int pbPos; // 当前线程的进度
pbMax = endIndex - startIndex;
pbPos = 0; // 进度条当前进度,如果中断过,则要从上次的位置开始


// 每个线程设置对应进度条的进度
if (code == 206)
{
// 创建随机读写文件对象
RandomAccessFile raf = new RandomAccessFile(getFileName(path), "rw");
// 每个线程从自己的位置开始写
raf.seek(startIndex);

InputStream in = conn.getInputStream();

int len = -1;
byte[] buffer = new byte[1024];
while ((len = in.read()) != -1)
{
raf.write(buffer, 0, len);
}

System.out.println("下载完毕");
raf.close();

// 设置进度条进度(注:进度条可以直接在子线程中更新UI)
pbLists.get(threadId).setMax(pbMax);
pbLists.get(threadId).setProgress(pbPos);
}

多线程下载开源项目

xUtils

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
// 点击按钮开始下载
public void click(View v)
{
String path = "...";
HttpUtils http = new HttpUtils();
//下载路径, 目标路径, 是否支持断点续传, 回调);
http.download(path, "/mnt/sdcard/haha.exe", true, new RequstCallBack<File>()
{
// 下载成功
@Override
public void onSucess(RequestInfo<File> responseInfo)
{
Toast.makeText(getApplicationContext(), "下载成功", 1).show();
}

// 下载失败
public void onFailure(HttpException error, String msg)
{
;
}

// 下载进度
@Override
public void onLoading(long total, long current, boolean isUploading)
{
pb.setMax((int) total);
pb.setProgress((int) current);
}
})
}

Android 四大组件

Activity

创建新的 Activity

创建新的 Class 继承 Activity

新的 Activity

1
2
3
4
5
6
7
8
9
10
public class TestActivity extends Activity
{
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
// 加载布局
setContentView(R.layout.activity_test)
}
}

清单文件 AndroidManifest

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
<!-- 代表当前应用 -->
<!-- 两个 Activity 入口 -->
<application ... >

<activity
android:name="com.包名.MainActivity"
android:label="第一个页面"
android:icon="@drawable/icon1" />

<!-- main 主入口 -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<!-- 配置入口 -->
<activity
android:name="com.包名.TestActivity" />
android:label="第二个页面"
android:icon="@drawable/icon2" />
<!-- main 主入口 -->
<intent-filter>
<!-- 第二个入口(要 .MAIN 和 .LAUNCHER) -->
<action android:name="android.intent.action.MAIN" />
<!-- 如果是 .category.DEFAULT 则不会有入口 -->
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

</application>
  • Android 四大组件都要在清单文件里面配置
  • 如果要多个入口(启动图标),则 intent-filter 必须要 .MAIN 和 .LAUNCHER。(一般就一个启动图标)
  • Activity 下的 label 和 icon 属性可以和 Application 节点的属性不一样默认使用 Application 节点下的属性。

隐式意图

1
2
3
4
5
6
7
8
// 创建 意图对象
Intent intent = new Intent();
// 设置 跳转的动作
intent.setAction("com.包名.活动名");
// 设置 category
intent.addCategory("android.intent.category.DEFALT");
// 开启 Activity
startActivity(intent);

意图过滤器

1
2
3
4
5
<intent-filter>
<!-- 传递的数据 、 约束 -->
<data android:mimetype="audio/mp4"
android:scheme="wxy"/>
</intent-filter>

可以配置多个 filter,只要匹配到完整的一个(setData 和 .setType)就行
mimetype 和 scheme 都可以不写

mimetype
Java 代码中设置 Date 也为 wxy(只要和约束相同就行)
intent.setData(Uri.parse("wxy:" + 110));
不设置约束,其实效果相同。

scheme
设置 setType 会自动把 setData 方法的数据清除。
反之亦然……
intent.setType("audio/mp4");
谷歌自定义了很多数据类型,开发者也可以用自己定义的例如aa/bb

两个要一起使用时,可以用下面的方法:
intent.setDataAndType(data, type);
例如:intent.setDataAndType(Uri.parse("wxy:" + 110), "audio/mp4");

####显式意图

通过制定具体的包名和类名来切换窗口

1
2
3
4
5
6
7
public void click(View v)
{
Intent intent = new Intent();
//参数:packageName 包名, className 类名
intent.setClassName("com.wxy.hello", "com.wxy.hello.HelloActivity2");
startActivity(intent);
}
1
2
// 更简洁的 切换窗口
startActivity(new Intent(this, HelloActivity.class));

开启自己应用的界面用显示意图,开启其他应用用隐式意图。

显式意图更加安全一些(仅自己能调用)

####传递数据
切换 Activity 时传递数据

获取传递过来的数据 Uri getIntent();

1
Uri data = intent.getData(); // 例如 wxy:110

Intent.putExtra(name, value); value 支持Android八大类型的数据

存入数据

1
2
3
Intent intent = new Intent(this, SecondActivity.class);
intent.putExtra("name", "hhh");
intent.putExtra("sex", 0);

取出数据

1
2
3
4
Intent intent = getIntent();            // 获取开启此Activity放入意图对象
String name = intent.getStringExtra("name");
int sex = intent.getIntExtra("sex", 0); // 参数二为默认值,可省
textView.setTex(name);

####人品计算器

1
2
3
4
5
6
7
8
9
10
11
12
13
byte[] bytes = NULL;
int total = 0;
if (sex == 1) bytes = name.getBytes("GBK");
else bytes = name.getBytes("UTF-8");
for (byte b : bytes) { // 有多少个1
int number = b & 0xff; // 1111 1111
total += number;
}
int score = Math.abs(total) % 100;
if (score > 90) {
toast("您的人品爆棚!");
break;
}

#####RadioGroup

1
2
3
4
5
<RadioGroup ... >
<Radio ... />
<Radio
android:checked="checked" />
</RadioGroup>
1
2
3
RadioGroup rg = (RadioGroup) findViewByUd(R.id.rg);
int res = rg.getCheckedRadioButtonId(); // 获取选中的单选框ID(可不选)
if (res == NULL) { ; }

短信发送器

制作一个能够发送短信模板的小应用。

####简单的 ListView

1
2
3
4
String msgs[] = {"短信1", "短信2", "短信3", ...};
ListView lv = (ListView) findViewById(R.id.lv);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.layout.item, msg); // layout/item 里面就只有一个 TextView
lv.setAdapter(adapter);

####设置 ListView 事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 给 ListView 设置事件
lv.setonItemClickListener(new onItemClickListener(){
// 点击列表发送短信
// 参数一:<?>ListView;参数二:每个条目(这里是TextView)
@Override
public void onItemClickListener(AdapterView<?> parent, View view, int position, long id)
{
// 条目内容
String content = msgs[position];

// 跳转到短信发送页面
Intent intent = new Intent();
intent.setAction("android.intent.action.SEND");
intent.addCategory("android.intent.category.DEFAULT");
intent.setType("text/plain");
intent.putExtra("sms_body", content); // 设置短信内容
startActivity(intent);
}
});

####请求码和结果码

自定义短信模板页面并插入

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
// 跳转到发送短信模板页面
public void addTemplate(View v)
{
Intent intent = new Intent(this, SmsActivity.class);
startActivityForResult(intent, 1); // 请求码 = 2
}


// 当开启的 Activity 页面关闭的时候,调用这个方法
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
if (requestCode == 1) // 请求码
{
; // 代表请求的是哪个页面的数据
}

if (resultCode == 10) // 结果码
{
// 说明数据是从短信模板页面返回的
String msg = data.getStringExtra("msg");

edit_num.setText("msg");
}

super.onActivityResult(requestCode, resultCode, data);
}

选择模板页面

1
2
3
4
5
6
7
8
9
10
11
12
13
lv.setOnItemClickListener(newOnItemClickListener(){
@Overridepublic void onItemClickListener(AdapterView<?> parent, View view, int position, long id)
{
String msg = msgs.get(position).getPhone();

Intent intent - new Intent();
intent.putExtra("msg", msg);
setResult(10, intent); // 返回的结果码 = 10

// 关闭当前页面
finish();
}
})

####发送短信

SmsManager 类
添加权限 android.permission.SEND_SMS

1
2
3
4
String number, content;
SmsManager sm = SmsManager.getDefault(); // 静态方法
// 参数二:服务中心号码,NULL 则使用默认;参数四:广播(成功失败的结果)
sm.sendTextMessage(number, null, content, NULL);

注:这个API有限制,英文70字符,中文140。太多不会发送,需要划分成多个片段。

###Activity 生命周期

onCreate
​ onResuart
​ onStart 变成可视界面时
​ onResume 允许获取焦点(按钮能点击等)
​ onPause 不允许获取焦点
​ onStop 界面不可见时
onDestroy

打开其他 Activity:onPause onStart
返回当前 Activity:onRestart onStart onResume

####设置横屏
横竖屏切换时,会Destroy再Create。
竖屏:portrait
横屏:landscape
<activity android:screenOrientation="portrait">

###任务栈

任务栈和 Activity 有关

进栈:打开一个 Activity
出栈:关闭一个 Activity
操作的 Activity 永远是栈顶的

任务栈是用来维护操作体验的

应用程序退出完是任务栈清空了

一般情况一个应用程序对应一个任务栈

###Activity 的四种启动模式

<activity android:launchMode="stander" ... >

  • stander 每创建一次 Activity 就一个任务栈
  • singletop 若Activity在栈顶则复用原Activity,否则创建Activity
  • singletask 一个 Activity 单一任务栈。浏览器。
  • singleinstance 有专属的任务栈,仅一个实例,不会重复创建。任务栈和任务栈之间再形成顺序。来电页面。

广播

广播接收

类名一般是 xxReceiver 格式
退出程序后,进程还在,广播接受器还能收到广播

IP拨号器

(加特定前缀能省话费)

权限:android.permission.OUTGONG_CALL

清单文件配置

1
2
3
4
5
6
7
8
9
<application>
......
<receiver android:name="com.包名.OutGoingCallReceiver">
<intent-filter>
<!-- 要接受的广播频道 -->
<action android:name="android.intent.action.NEW_OUTGONG_CALL" />
</intent-filter>
</receiver>
</application>

定义广播接受者(java类)
若没有界面的话,可以不用编辑 MainActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import android.content.BroadcastReceiver;

public class OutGoingCallReceiver extends BroadcastReceiver {

// 当进行外拨电话的时候调用
@Override
public void onReceiver(Context context, Intent intent) {
// 使用参数里的上下文来读取设置
SharedPreferences sp = context.getSharedPreferences("config", 0);
String IPnumber = sp.getString("IPnumber", "");

// 获取当前拨打的电话号码
String number = getResultData();

// 判断当前号码是否是长途,在当前的号码前面加上前缀
if (currentNumber.startsWith("0")) {
setResultData(IPnumber + number); // 一般是能变更的前缀
}
}
}

主函数 设置用户手动输入的 IPnumber

1
2
3
4
5
6
7
public void click(View v) {
EditText et = (EditText) findViewById(R.id.et);
String IPnumber = et.getText().toString().trim(); // 用户编辑的前缀好吗
SharedPreferences sp = getSharedPreferences("cinfig", 0);
sp.edit().putString("IPnumber", IPnumber).commit();
Toast.makeText(getApplicationContext, "保存成功", 1).show();
}

SD卡状态的监听

1
2
3
4
5
6
7
8
<receiver android:name="包名.AdcardStateReceiver">
<intent-filter>
<action android:name="android.intent.action.MEDIA_MOUNTED" />
<action android:name="android.intent.action.MEDIA_UNMOUNTED" />
<!-- 必须指定一个叫 file 的约束,否则不生效 -->
<data android:scheme="file" />
</intent-filter>
</receiver>

小细节:SD卡里面存的数据类型是 file,所以需要 file 约束。另一个要配置data的广播事件是 安装应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import android.content.BroadcastReceiver;

public class SdcardStateReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// 获取当前广播的事件类型
Strning action = intent.getAction();

if ("android.intent.action.MEDIA_MOUNTED".equals(action)) {
// SD卡挂在
}
else if ("android.intent-filter.action.MEDIA_UNMOUNTED".equals(action)) {
// SD卡卸载
}
}
}

短信监听器

权限:android.permission.RECEIVE_SMS

1
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void onReceive(Context context, Intent intent) {
// 获取发送的号码和内容
Object[] objects = (Objectp[]) intent.getExtras().get("pdus");
for (Object obj : objects) {
// 获取 smsmessage 实例:这是调用静态方法而不是 new 出来的
SmsMessage smsMessage = SmsMessage.createFromPdu((byte[]) boj);
// 获取短信内容
String messageBody = smsMessage.getMessageBody();
String address = smsMessage.getOriginatingAddress();
}
}

卸载安装实例

这个也要配置 data

1
2
3
4
<action android:name="android.intent.action.PACKAGE_INSTALL" />
<action android:name="android.intent.action.PACKAGE_ADDED" />
<action android:name="android.intent.action.PACKAGE_REMOVED" />
<data android.scheme="package" />
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();

if ("android.intent.action.PACKAGE_INSTALL".equals(action)) {
// 应用安装(预留的字符串,并没有什么用)
}
else if ("android.intent.action.PACKAGE_ADDED".equals(action)) {
// 应用安装了
String packageName = intent.getData(); // 应用包名
}
else if ("android.intent.action.PACKAGE_REMOVED".equals(action)) {
// 应用卸载了
}
}

手机重启实例

不能在广播接收者里面开启 activity
需要添加一个任务栈的标记

权限:anroid.permission.RECEIVE_BOOT_COMPLETED

1
<action android:name="android.intent.action.BOOT_COMPLETED" />
1
2
3
4
5
6
7
8
9
10
11
12
13
public class BootReceiver extends BroadcastReceiver {

// 当手机重新启动的时候调用
@Override
public void onReceive(Context context, Intent intent) {
// 在这个方法里面开启 activity
Intent intent2 = new Intent(context, MainActivity.class);
intent2.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

// 开启 activity
context.startActivity(intent2);
}
}
1
2
3
4
5
// Activity 里屏蔽返回键
@Override
public void onBackPressed() {
; // 覆盖父类返回方法
}

广播类型

有序广播:按照一定的优先级进行接收,在接收的过程中可以被修改或者终止

无序广播:不可以被修改或者终止

特殊广播:不能在清单文件中注册,需要动态注册

无序广播

发送无序广播

1
2
3
4
5
6
7
8
9
public void click(View v) {
Intent intent = new Intent();

intent.setAction("com.HHH");

intent.putExtra("name", "HHH");

sendBroadcast(intent); // 发送无序广播
}

接收无序广播

1
<action name="com.HHH" />
1
2
3
4
// 当接收到自定义广播
public onReceive(Context context, Intent intent) {
String context = intent.getStringExtra("name"); // 广播内容
}

有序广播

发送有序广播

1
2
// 参数:Intent, 权限,最终的receiver,scheduler,初始码,初始化数据,额外的数据
sendOrderedBroadcast(intent, null, null, null, 1, "这是广播", null);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<application
... >
<activity> ... </activity>

<!-- 配置广播接受优先级 范围 -1000~1000 -->
<receiver android:name="包名.FirstReceiver">
<intent-filter android:priority="1000">
<action android:name="myReceiver" />
</intent-filter>
</receiver>

<receiver android:name="包名.SecondReceiver">
<intent-filter android:priority="0">
<action android:name="myReceiver" />
</intent-filter>
</receiver>

<receiver android:name="包名.LastReceiver">
<intent-filter android:priority="-200">
<action android:name="myReceiver" />
</intent-filter>
</receiver>

</application>

按顺序接收广播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class FirstReceiver extends BroadcastReceiver {
@Override
public onReceive(Context context, Intent intent) {
String content = getResultData();
setResultData("哈哈哈"); // 修改广播内容,后面接收到的就是这个内容
}
}

public class SecondReceiver extends BroadcastReceiver {
@Override
public onReceive(Context context, Intent intent) {
abortBroadcast(); // 直接终止广播,下面接收不到了
}
}

public class LastReceiver extends BroadcastReceiver {
@Override
public onReceive(Context context, Intent intent) {

}
}

特殊广播接收者

操作特别频繁的广播事件,比如 屏幕的锁屏和解锁、电池电量的变化,这种事件的广播在清单文件里面是注册无效的,可以动态注册广播

注册广播接受者的两种方式:

  1. 在清单文件通过 receiver tag 节点
  2. 动态注册:代码方式(即清单方式注册无效,并且无需注册)

类似于android.intent.action.SCREEN_ONandroid.intent.action.SCREEN_OFF 这种,注册是无效的

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
public class MainActivity extends Activity {

// 动态的去注册广播接收者,设为全局方便销毁
ScreenReceiver screenreceiver = new ScreenReceiver();

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



// 创建 Intent-Filter 对象
IntentFilter filter = new IntentFilter();
// 动态注册广播(可以添加多个)
filter.addAction("android.intent.action.SCREEN_OFF");
filter.addAction("android.intent.action.SCREEN_ON");

registerReceiver(screenReceiver, filter);
}

@Override
protected void onDestory() {
// activity 销毁时要取消注册广播

unregisterReceiver(screenreceiver);
}
}

public class ScreenReceiver extends BroadcastReceiver {
@Override
public onReceive(Context context, Intent intent) {

}

样式和主题

样式

样式一般作用在控件上

style 文件

1
2
3
4
5
<style name="myStyle">
<item name="android:layout_height">match_parent</item>
<item name="android:textSize">20SP</item>
<item name="android:textColor">#ff000</item>
</style>

layout 文件

1
2
3
<TextView
style="@style/mystyle"
android:text="哈哈哈" />

主题

样式一般作用在 Activity 或 Application 节点下

style 文件

1
2
3
<style name="myStyle">
<item name="android:background">#ff0000</item>
</style>

manifest 文件

1
2
<application
android:theme="@style/mystyle">

区别

两者定义的方式是一样的。

样式的作用范围比较窄,主题比较大。

不一定要在 style.xml 上定义,其他类似 txt 上也行。

Android 国际化

国际化:i18n

类似于android:text="你好世界"这种硬编码的字符串,一般都是放到string.xml里面

多国语言文件:res目录下的values-语言代码文件夹里面的string.xml (固定写法)
语言代码:en英语,zh中文简体,zh-Hant中文繁体……
例如:/res/values-zh/string.xml 表示中文资源

对话框

常见对话框

  • Toast
  • 普通对话框
  • 单选对话框
  • 多选对话框
  • 进度条对话框

普通对话框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 通过 builder 构建器来构造
AlertDialog.Builder builder = new Builder(this); // 不能用 getApplicationContext()
builder.setTitle("提示");
builder.setMessage("这里是提示内容");
builder.setPositiveButton("确定", new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
System.out.println("点击了确定按钮");
}
});
builder.setNegativeButton("取消", new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
System.out.println("点击了取消按钮");
}
});
builder.show(); // 一定要 show 出来

如果报错,则把new OnClickListener(){} 改成new DialogInterface.OnClickListener(){}

两种上下文的区别
  • this (即:类名.this)
  • getApplicationContext() 返回的是 Context 对象

getApplicationContext() 获取的是整个应用上下文,this 是其子类,子类有的父类不一定有。

this 多了token(令牌)。

就对话框来说,必须要用 this,否则会报错。

单选对话框

1
2
3
4
5
6
7
8
9
10
11
12
13
AlertDialog.Builder builder = new Builder(this);
builder.setTitle("请选择");
String items[] = { "item1", "item2"};
// 参数二:checked 选中索引,-1 为没有选中
builder.setSingleChoiceItems(items, -1, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
System.out.println("您选择了" + items[which]);
// 关闭对话框:点一下就关掉,不需要确定按钮
dialog.dismiss();
}
});
builder.show();

多选对话框

注意:listener 是OnMultiChoiceClickListener

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
AlertDialog.Builder builder = new Builder(this);
String items[] = {"item1", "item2", "item3"};
Boolean[] checkedItems = {true, false, true};
builder.setMultiChoiceItems(items, checkedItems, new OnMultiChoiceClickListener() {
@Override
public void onClick(DialogInterface dialog, int which, boolean isChecked) {
System.out.println("您点击了" + items[which]);
// 关闭对话框:点一下就关掉,不需要确定按钮
dialog.dismiss();
}
});
builder.setPositiveButton("确定", new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// 把选中的条目数据取出来
StringBuffer sb = new StringBuffer();
for (int i = 0; i < checkedItems.length; i++)
{
if (checkedItems[i])
{
sb.append(items[i] + " ");
}
}
Toast.makeText(getApplicationContext(), sb.toString(), 1).show();
// 关闭对话框
dialog.dismiss();
}
});
builder.show();

进度条对话框

1
2
3
4
5
ProgressDialog dialog = new ProgressDialog(this);
dialog.setTitle("正在加载中");
// 设置进度条样式
dialog.setProgressStyle(style);
dialog.show();

style 属性有:

ProgressDialog.STYLE_HORIZONTAL 横向进度条,0%

和进度相关的控件,都能在子线程中更新UI

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
ProgressDialog dialog = new ProgressDialog(this);
dialog.setTitle("正在加载中");
// 设置进度条样式
dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
dialog.show();

// 创建一个子线程
new Thread() {
public void run() {
// 设置最大值
dialog.setMax(100);

// 耗时操作,设置进度
for (int i = 0; i < 100; i++)
{
// 睡眠 50 ms
SystemClock.sleep(50);

// 设置当前进度
dialog.setProgress(i);
}

// 关闭对话框
dialog.dismiss();
}
}.start();

帧动画

Android 中动画

  • 帧动画
  • View 动画(补间动画)
  • 属性动画

帧动画:加载一系列的图片资源

xml 文件在res/drawable/directory目录下,标签是<animation-list>

res/drawable/myanim.xml 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<animation-list xmlns:android="..."
androidLoneshot="true">
<!-- true 表示只执行一次,false 循环播放 -->

<item
android:drawable="@drawable/pic_1"
android:duration="200" />
<item
android:drawable="@drawable/pic_2"
android:duration="200" />
<item
android:drawable="@drawable/pic_3"
android:duration="200" />
<item
android:drawable="@drawable/pic_4"
android:duration="200" />
</animation-list>

java文件

1
2
3
4
5
6
7
8
9
10
ImageView rocketImage = (ImageView) findViewById(R.id.rocketImage);

// 设置背景资源
rocketImage.setBackgroundResource(R.drawable.myanim);

// 获取 AnimationDrawable 类型
AnimationDrawable rocketAnimation = (AnimationDrawable) rocketImage.getBackground();

// 开启动画
rocketAnimation.start();

兼容低版本 2.3

因为数据并未准备好,这个 API 是 2.3 之后出的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ImageView rocketImage = (ImageView) findViewById(R.id.rocketImage);

// 设置背景资源
rocketImage.setBackgroundResource(R.drawable.myanim);

// 兼容低版本的写法
new Thread(){
public void run() {
SystemClock.sleep(100);

// 获取 AnimationDrawable 类型
AnimationDrawable rocketAnimation = (AnimationDrawable) rocketImage.getBackground();

// 开启动画
rocketAnimation.start();
}
}.start();

服务

服务在后台运行,没有界面

Service extends CpntextWrapper implements ...

进程

  1. 前台进程
  2. 可视进程
  3. 服务进程
  4. 后台进程
  5. 空进程

start方式开启服务

清单文件

1
2
3
4
<application ...>
<activity ...> ... </activity>
<service android:name="包名.服务名"></service>
</application>

MyService.java

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
public class MyService extends Service {

@Override
public Ibinder onBind(Intent intent) {
return null;
}

// 服务第一次创建的时候调用
@Override
public void onCreate() {
super.onCreate();
}

// 服务运行
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.StartCommand(intent, flags, startId);
}

// 服务销毁
@Override
public void onDestroy() {
super.onDestroy;
}
}

MainActivity.java

1
2
3
4
5
// 开启服务,一直后台运行直到用户手动关闭
startService(new Intent(this, MyService.class));

// 结束服务
stopService(new Intent(this, MyService.class));

start方式开启服务的特点

  • 定义四大组件的方式是一样的
  • 定义一个类继承Service
  • 第一次开启服务,会执行 onCreate 和 onStart 方案
  • 第二次开启服务,会执行 onStart 方法
  • 服务一定开启,就会长期后台运行,直到用户手动停止

电话监听器

  1. 定义一个服务,开启服务。(记得在清单文件中配置服务)
  2. 在服务的 onCreate 里面获取 TelephonyManager
  3. 注册电话的监听
  4. 定义一个类用来判断电话的状态

TelephoneManager 监视电话的状态的改变

权限:<uses-permission android:name="android.permission.READ_PHONE_STATE" />

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
public class PhoneService extends Service {
@Override Ibinder onBind(Intent intent) {
return null;
}

@Override
public void onCreate() {
// 获取 telephonemanager 的实例
TelephonyManager tm = (TelephonyManager)getSystemService(TELEPHONY_SERVICE);

// 注册电话的监听
tm.listen(new MyPhoneStateListener(), PhoneStateListener.LISTEN_CALL_STAT)

super.onCreate();

}

@Override
public void onDestroy() {
super.onDestroy();
}

private class MyPhoneStateListener extends PhoneStateListener {
// 当电话设备状态发生改变的时候调用
@Override
public void onCallStateChanged(int state, String incomingNumber) {
// 具体判断一下电话的状态
switch (state) {
case TelephonyManager.CALL_STATE_IDLE: // 空闲
// 停止录音

break;
case TelephonyManager.CALL_STATE_OFFHOOK: // 接听
// 开始录音

break;
case TelephonyManager.CALL_STATE_RINGING: // 响铃
// 准备一个录音机

break;
}
}

} {}
}
录音机

录音需要权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />

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
// 创建实例,全局
MediaRecorder recorder = new MediaRecorder();


// 设置音频来源(麦克风MIC、外置声音VOICE_CALL)
recorder.setAudioSource(MediaRecorder.AudioSource.MIC); // 外国违法
// 设置输出格式 3GP(THREE_GPP)、MPEG_4 等
recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
// 设置音频编码方式
recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
// 设置存放路径
recorder.setOutputFile("/mnt/sdcard/luyin.3gp");
// 准备录
try {
recorder.prepare();
}


// 开始录
recorder.start();


// 停止录
if (recorder != null)
{
recorder.stop();
recorder.reset();
recorder.release();
}

使用服务注册特殊的广播接收者

  1. 定义广播接收者
  2. 写个服务用来注册广播接收者
  3. 在MainActivity里面开启服务
  4. 一定要记得配置服务

MainActivity.java

1
2
3
4
onCreate() {
Intent intent = newe Intent(this, ScreenService.class);
startService(intent);
}

ScreenService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 动态注册和销毁广播
private ScreenReceiver receiver = new ScreenReceiver();

public void onCreate() {
// 服务创建时注册广播接收者
IntentFilter filter = new IntentFilter();
filter.addAction("android.intent.action.SCREEN_OFF");
filter.addAction("android.intent.action.SCREEN_ON");

registerReceiver(receiver, filter)

super.onCreate();
}

onDestroy() {
// 服务销毁的时候取消广播接收者
unregisterReceiver(receiver);

super.onDestroy();
}

ScreenReceiver.java

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ScreenReceiver extends BroadcastReceiver
{
@Override
public void onReceive(Context context, Intent intent)
{
// 获取当前的事件类型
String acation = intent.getAction();
if ("android.intent.action.SCREEN_OFF".equals(action))
; // 锁屏
else if ("android.intent.action.SCREEN_ON".equals(action))
; // 解锁
}
}

bind 方式开启服务

bindService(service, conn, flags);

点击按钮绑定服务

MainActivity.java

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
MyConn conn;

public void click(View v)
{
// 或者放到 onCreate 方法里面
Intent intent = new Intent(this, MyService.class);
conn = new MyConn();
bindService(intent, conn, BIND_AUTI_CREATE);// 只能绑定一次
}

// 定义一个类,用来监视服务的状态
private void MyConn extends ServiceConnection
{
// 当服务连接成功
@Override
public void onServiceConnected(ComponentName name, IBinder serviec) {
;
}

// 失去连接
@Override
public void onServiceDisconnected(ComponentName name) {
;
}
}

// 服务和活动绑定,销毁的时候必须手动解绑(同样不可以多次解绑)
protected void onDestroy() {
unbindService(conn);

super.onDestroy();
}

MyService.java

1
2
3
4
5
6
// 当绑定服务的时候
public IBinder onBinder(Intent intent) {

// 当这个方法返回值为null的时候,onServiceConnected方法一不执行的
// return null;
}

bind方法开启服务的特点

  • 点击按钮(或执行操作)后,会此项服务的onCreate方法和onBind方法
  • 当onBind方法返回null的时候,onServiceConnected方法是不执行的
  • 第二次点击按钮,服务没有响应
  • activity和service之间,不求同时生,但求同时死……
  • 服务多次解绑会报异常
  • 设置页面里面不能找到bind方式开启的服务,相当于隐形的服务

为什么要引入 bindService

在 Activity 里面调用 Service 的方法

Binder 是一个接口。

MyService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public IBinder onbind(Intent intent) {
return new MyBinder;
}

// 要调用的方法
public void f() {
;
}

public class MyBinder extends Binder() {
public void callF() {
// 调用 f 的方法
f();
}
}

MainActivity.java

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
MyConn conn;
private MyBinder myBinder; // (中间对象)

public void onCreate(...)
{
Intent intent = new Intent(this, MyService.class);
conn = new MyConn();
bindService(intent, conn, BIND_AUTI_CREATE);// 只能绑定一次
}

// 定义一个类,用来监视服务的状态
private void MyConn extends ServiceConnection
{
// 当服务连接成功
@Override
public void onServiceConnected(ComponentName name, IBinder serviec) {
myBinder = (MyBinder) service; // 拿到中间对象,从而可以调用服务里的某个方法
}

// 失去连接
@Override
public void onServiceDisconnected(ComponentName name) {
;
}
}

public void click() {
myBinder.callF(); // 间接调用服务的方法
}

// 服务和活动绑定,销毁的时候必须手动解绑(同样不可以多次解绑)
protected void onDestroy() {
unbindService(conn);

super.onDestroy();
}

通过接口调用服务里面的方法

接口可以隐藏代码内部的细节,让程序员只暴露自己想暴露的方法

  1. 把想暴露的方法都定义在接口里面
  2. Binder对象实现我们定义的方法
  3. 把获取到的IBinder对象转换成暴露部分方法的接口

Iservice.java (I 开头表示是一个接口)

1
2
3
4
public interface Iservice {
// 只暴露一个 f1,没有 f2
public void callF1();
}

MyService.java

1
2
3
4
5
6
7
8
9
10
public void f1() { }

public void f2() { }

// 把 public 改成 private
private class MyBinder extends Binder implements Iservice {
public void callF1() { }

public void callF2() { }
}

MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private MyConn conn;
private Iservice myBinder; // 中间对象

private calss MyConn implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
// 类型转换成 Iservice
myBinder = (Iservice) service;
}
}

public void click(View v) {
myBinder.callF1(); // 可以

// myBinder.callF2(); // 不可以
}

混合方式开启服务

既能在后台长期运行,又能调用服务里面的方法

  1. start 方法开启服务
  2. 调用 bindService 获取中间对象
  3. 调用 unbindService 解绑服务
  4. 彻底退出时 stopService

百度音乐盒框架

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
/* ==== MainActivity.java ==== */

public void MainActivity extends Activity {
private MyConn conn;
private Iservice iservice; // 定义的 Binder 对象

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

// 混合方式开启服务

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

conn = new MyConn();

bindService(intent, conn, BIND_AUTO_CREATE);
}

// 监听服务状态
private class MyConn {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
// 获取定义的 Binder 对象
iservice = (Iservice) service;
}

}

public void playButton(View v) {
// 播放音乐
iservice.callPlayMusic();
}

public void pauseButton(View v) {
// 暂停音乐
iservice.callPauseMusic();
}

public void rePlayButton(View v) {
// 继续播放音乐
iservice.callRePlayMusic();
}


@Override
protected void onDestroy() {
unbindService(conn);

super.onDestroy();
}
}



/* ==== MusicService.java ==== */

public class MusicService extends Service {

@Override
public Ibinder onBind(Intent intent) {
return null;
}

// 服务第一次创建的时候调用
@Override
public void onCreate() {
super.onCreate();
}

// 服务运行
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.StartCommand(intent, flags, startId);
}

// 服务销毁
@Override
public void onDestroy() {
super.onDestroy;
}

// 播放音乐
public void playMusic() {
;
}

// 暂停音乐
@Override
public void pauseMusic() {
;
}

// 继续播放
@Override
public void rePlayMusic() {
;
}

// 在服务内部定义一个 IBinder 类的实现
private class MyBinder extends Binder implements Iservice{
@Override
public void callPlayMusic() {
playMusic();
}

@Override
public void callPauseMusic() {
pauseMusic();
}

@Override
public void callRePlayMusic() {
rePlayMusic();
}
}
}


/* ==== Iservice.java ==== */

public interface Iservice {
// 想暴露的方法
public void callPlayMusic();
public void callPauseMusic();
public void callRePlayMusic();
}

AIDL 介绍

  • 本地服务:运行在自己应用里面的服务
  • 远程服务:运行在其他应用里面的服务

实现进程间通讯(IPC)

AIDL:安卓接口定义语言,专门用来解决进程间通讯

  1. 每个应用都要配置服务。其中一个定义 IBinder
  2. 把 Iservice.java 文件重命名为 Iservice.aidl。
  3. aidl 不认识 public,所以这个关键词要去掉
  4. 会自动生成Iservice.java,里面有个类叫 Stub
  5. MyBinder 直接继承 Stub
  6. 两个应用的 aidl 文件包名相同
  7. Stub.asInterface(service);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Iservice iservice;

onCreate() {
Intent intent = new Intent();
Intent.setAction("目标应用的包名");
MyConn conn = new MyConn();
bindService(intent, conn, BIND_QUTO_CREATE);
}

private class MyConn implements ServiceConnection {

@Override
public void onServiceConnected(ComponentName name, IBinder service) {
iservice = Stub.asInterface(service);
}
}

public void click(View v) {
iservice.callF();
}
1
2
3
4
5
6
7
private class MyBinder extends Stub {
// ...

public void callF() {
f();
}
}

Iservice.aidl

1
2
3
interface Iservice {
void f();
}

AIDL 的应用场景

类似调用支付宝进行支付

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
import android.os.Bundle;

public class MainActivity extends Activity {

private MyCon conn;
private Iservice iservice;

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

Intent intent = new Intent();
intent.setAction("com.test.pay");

conn = new MyConn();

bindService(intent, conn, BIND_AUTO_CREATE);
}


public void payClick() {
try {
boolean result = iservice.callPay("abc", "123", 1000);
if (result) {
Toast.makeText(getApplicationContext(), "支付成功", 1).show();
} else {
Toast.makeText(getApplicationContext(), "支付失败", 1).show();
}
} catch (RemoteException e) {
e.printStackTrace();
}
}


private class MyConn implements ServiceConnection {

@Override
public void onServiceConnected(ComponentName name, IBinder service) {
iservice = Stub.asInterface(service);
}

@Override
public void onServiceDisconnected(ComponentName name) {
;
}
}


@Override
protected void onDestroy() {
unbinderService(conn);

super.onDestroy()
}
}


// =================PayService.java===================

import android.app.Service;

public class PayService extends Service {

@Override
publci IBinder onBind(Intent intent) {
return new MyBinder();
}

// 支付的方法
publci void pay(String username, String pwd, int money) {
// 检查账号密码安全性等
if ("abc" . equals(username) && "123" . equals(pwd) && money <= 5000) {
return true;
}
else {
return false;
}
}
}

private class MyBinder extends Stub /*Binder implements Iservice*/ {
@Override
public boolean callPay(String username, String pwd, int money) {
return pay(username, pwd, money);
}
}



// ==============Iservice.aidl===================
// 用来支付的应用也要 Iservice.aidl

/*public*/ interface Iservice {

/*public*/ boolean callPay(String username, String pwd, int money) {
pay();
}
}



// ====================Manifest==================

<service android:name="com.test.pay.PayService">
<intent-filter>
<action android:name="com.test.pay" />
</intent-filter>
</service>

内容提供者(contentProvider)

作用:在app2中读取app1的数据库(暴露私有的数据库)

使用:

  1. 建一个类,继承 contentProvider
  2. 在清单文件中配置
  3. 添加静态代码块
  4. 暴露你想暴露的方法(增删改查)
  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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import android.content.ContentProvider;

public class MyProvider extends ContentProvider {

// 定义一个 UriMather 路径匹配器
private static final UriMather sURIMather = new UriMather(UriMather.NO_MATH);
private static final int QUERYSUCCESS = 0;
private MyOpenHelper myOpenHelper;

// 定义静态代码块,添加匹配规则
static {
// sURIMather.addURI(authority/*清单文件里的自定义字符串*/, path, code/*int常量*/)
// URI 路径:content://mystr/query
sURIMather.addURI("mystr", "query", QUERYSUCCESS);
}

@Override
public boolean onCreate() {
myOpenHelper = new MyOpenHelper(getContext());
return false;
}

@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
int code = sURIMather.math(uri);
if (code == QUERYSUCCESS) { // 路径匹配成功。不成功返回-1
// 对数据库进行查询的操作
SQLiteDatabase db = myOpenHelper.getReadableDatabase();

// db.query(table, columns/*别人能访问的列*/, selection, selectionArgs, groupBy, having, orderBy)
Cursor cursor = db.query("info", projection, selection, selectionArgs, null, null. sortOrder);
// 注意:cursor 不能管

return cursor;
} else { // 路径不匹配
throw new IlleagalArgumentException("路径不匹配");
}
}

@Override
public String getType(Uri uri) {
return null;
}

@Override
public Uri insert(Uri uri, ContentValues values) {
return null;
}
}
1
2
3
<provider android:name="com.app1.MyProvider"
android:authorities="mystr">
</provider>

app2读取app1的数据库

由于app1里面的私有数据库已经通过内容提供者的方式暴露出来了,所以app2可以直接通过内容的解析这进行访问

1
2
3
4
5
6
7
8
9
10
11
12
13
// 拿到内容的解析者
// getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder)
Uri uri = Uri.parse("content://mystr/query"); // 和app1定义的路径一样
// Cursor cursor = db.query("info", null, null, null, null, null, null, null);
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
if (cursor != null && cursor.getCount() > 0) {
while (cursor.moveToNext()) {
String name = sursor.getString(1);
String phone = cursor.getString(2);

System.out.println("app2 name:"+name+";phone:"+phone);
}
}

暴露增删改查

app1

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
import android.content.ContentProvider;

public class MyProvider extends ContentProvider {

// 定义一个 UriMather 路径匹配器
private static final UriMather sURIMather = new UriMather(UriMather.NO_MATH);
private static final int QUERYSUCCESS = 0;
private static final int INSERTSUCCESS = 1;
private static final int UPDATESUCCESS = 2;
private static final int DELETESUCCESS = 3;
private MyOpenHelper myOpenHelper;

// 定义静态代码块,添加匹配规则
static {
// sURIMather.addURI(authority/*清单文件里的自定义字符串*/, path, code/*int常量*/)
// URI 路径:content://mystr/query
sURIMather.addURI("mystr", "query", QUERYSUCCESS);
sURIMather.addURI("mystr", "insert", INSERTSUCCESS);
sURIMather.addURI("mystr", "update", UPDATESUCCESS);
sURIMather.addURI("mystr", "delete", DELETESUCCESS);
}

@Override
public boolean onCreate() {
myOpenHelper = new MyOpenHelper(getContext());
return false;
}

@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
int code = sURIMather.math(uri);
if (code == QUERYSUCCESS) { // 路径匹配成功。不成功返回-1
// 对数据库进行查询的操作
SQLiteDatabase db = myOpenHelper.getReadableDatabase();

// db.query(table, columns/*别人能访问的列*/, selection, selectionArgs, groupBy, having, orderBy)
Cursor cursor = db.query("info", projection, selection, selectionArgs, null, null. sortOrder);
// 注意:cursor 不能管

return cursor;
} else { // 路径不匹配
throw new IlleagalArgumentException("路径不匹配");
}
}

@Override
public String getType(Uri uri) {
return null;
}

@Override
public Uri insert(Uri uri, ContentValues values) {
int code = sURIMather.match(uri);
if (code == INSERTSUCCESS) {
// 路径匹配成功,操作数据库
SQLiteDatabase db = myOpenHelper.getReadableDatabase();
long ins = db.insert("info"/*表名*/, null, values/*map<String, String>类型*/); // 返回插入的行数ID

Uri uri2 = Uri.parse("插入的行号:" + ins);
return uri2;
} else { // 路径不匹配
throw new IlleagalArgumentException("路径不匹配");
}
return null;
}

@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
int code = sURIMather.match(uri);
if (code == DELETESUCCESS) {
// 路径匹配成功,操作数据库
SQLiteDatabase db = myOpenHelper.getReadableDatabase();
int del = db.delete("info", selection, selectionArgs); // 返回删除的行数

return del; // 返回的类型就是 int
} else { // 路径不匹配
throw new IlleagalArgumentException("路径不匹配");
}
return null;
}

@Override
public int delete(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
int code = sURIMather.match(uri);
if (code == DELETESUCCESS) {
// 路径匹配成功,操作数据库
SQLiteDatabase db = myOpenHelper.getReadableDatabase();
int upd = db.delete("info", values, selection, selectionArgs); // 返回影响的行数

return upd; // 返回的类型就是 int
} else { // 路径不匹配
throw new IlleagalArgumentException("路径不匹配");
}
return null;
}
}

app2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void toInsert() {
Uri uri = Uri.parse("cintent://mystr/insert");
ContentValues values = new ContentValues();
values.put("name", "123");
values.put("money", "100");
Uri ins = getContentResolver().insert(uri, values);
System.out.println("insert:"+ins);
}

public void toDelete() {
Uri uri = Uri.parse("cintent://mystr/delete");
int del = getContentResolver().insert(uri, "name=?", new String[]{"asd"});
Toast.makeText(getApplicationContext(), "delete:"+del, 1).show();
}

public void toUpdate() {
Uri uri = Uri.parse("cintent://mystr/insert");
ContentValues values = new ContentValues();
values.put("money", "100");
int upd = getContentResolver().update(uri, values, "nname=?", new String[]{"asd"});
Toast.makeText(getApplicationContext(), "update:"+upd, 1).show();
}

备份短信

权限:WRITE_SMS READ_SMS READ_EXTERNAL_STORAGE

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
// 短信数据库已经通过内容提供者暴露出来了

public sms_backup() {

// 获取 xml 序列化实例
XmlSerializer serializer = Xml.newSerializer();
// 设置序列化参数
File file = new File(Environment.getExternalStorageDirectory().getPath(), "smsBackup.xml");
FileOutputStream fos = new FileOutputStream(file);
serializer.setOutput(fos, "utf-8");

// 开始写 xml 文档的开头
serializer.startDocument("utf-8", true);
{
// 开始写根节点
serializer.startTag(null, "smss");
{
Uri uri = Uri.parse("content://sms/") // 不写后面的参数表示查询所有
Cursor cursor = getContentResolver().query(uri, new String[]{"address", "date", "body"}, null, null, null);
while (cursor.moveToNext()) {
String address = cursor.getString(0);
String date = cursor.getString(1);
String body = cursor.getString(2);

// 写 sms 节点
serializer.startTag(null, "sms");
{
// 写 address 节点
serializer.startTag(null, "address");
serializer.text(address);
serializer.endTag(null, "address");

// 写 body 节点
serializer.startTag(null, "body");
serializer.text(body);
serializer.endTag(null, "body");

// 写 date 节点
serializer.startTag(null, "date");
serializer.text(date);
serializer.endTag(null, "date");
}
serializer.endTag(null, "sms");
}
}
serializer.endTag(null, "smss");
}
serializer.endDocument();
}

插入短信

通过内容提供者在短信的私有数据库中插入一条短信

权限:WRITE_SMS READ_SMS

1
2
3
4
5
6
Uri uri = Uri.parse("content://sms");
ContentValues values = new ContentValues();
values.put("address", "110");
values.put("body", "报警");
values.put("date", System.currentTimeMillis());
getContentResolver().insert(uri, values);