《第一行代码》读书笔记

第01章 启程#

Android系统架构#

  • Linux内核层(Linux Kernel)

  • 系统运行层(Libraries,Android Runtime)

  • 应用框架层(Application Framework)

  • 应用层(Application)

Android提供的东西#

  • 四大组件

  • Activity 活动

  • Service 服务

  • Broadcast Receiver 广播接收器

  • Content Provider 内容提供者

  • 丰富的系统控件

  • SQLite数据库

  • 地理位置定位

  • 强大的多媒体

  • 传感器

项目目录结构#

  • src:放代码

  • gen:存放自动生成的内容,R.java文件在这

  • assets:存放随应用打包的文件

  • bin:包含编译时自动产生的文件,包括没有签名的应用安装文件**.apk

  • libs:存放第三方的jar包 

  • res:存放项目中用到的图片(Drawable),布局(Layout),字符串(String),颜色(Color)等资源

  • AndroidManifest.xml:整个Android项目的配置文件,作用如下

      - 注册应用中添加的四大组件

      - 声明应用所需的权限

      - 指定应用最低兼容版本和目标版本

  • project.properties:通过一行代码指定编译应用时用的SDK版本

日志工具Log#

  • Log.v():verbose,所有琐碎的信息

  • Log.d():debug,调试信息

  • Log.i():info,比较重要的信息

  • log.w():warn,警告信息

  • log.e():error,错误信息

第02章 活动Activity#

一种包含用户界面的组件,主要用于和用户进行交互

手动创建Activity#

  • 添加一个继承了Activity类的class文件,并重写其onCreate()方法

  • 创建和加载布局

  • 在AndroidManifest.xml文件中注册

使用Intent在Activity间跳转#

  • 显示Intent

  • 隐式Intent:指定抽象的action和category,交由系统去找出合适的活动来启动

  • 向下一个活动传递数据:intent.putExtra(“extra_data”, data);

  intent.getStringExtra(“extra_data”)

  • 返回数据给上一个活动:startActivityForResult()

活动的生命周期#

  • 返回栈 Back Stack

  • 活动状态

  • 运行状态:活动处在栈顶

  • 暂停状态:活动不处于栈顶,但仍然可见

  • 停止状态:活动不处于栈顶,且完全不可见

  • 销毁状态:活动从栈中移除

  • 活动的生存期

  • onCreate()

  • onStart()

  • onResume()

  • onPause()

  • onStop()

  • onDestroy()

  • onRestart()

  • onSaveInstanceState() 使用该方法在活动实例被回收前保存数据

活动的启动模式#

启动模式一共有四种:standard,singleTop,singleTask,singleInstance,可以在AndroidManifest.xml中通过给标签指定android:launchMode属性来指定启动模式

standard#

standard是活动默认的启动方式,采用standard启动方式的Activity在每次启动时都会创建一个新的实例在返回栈中入栈,即使这个活动已经有实例在栈中了。

singleTop#

当活动的启动模式指定为singleTop时,在启动活动时如果发现返回栈顶已经是该活动,则认为可以直接使用它,而不会再创建新的活动实例。

singleTask#

当活动的启动模式被指定为singleTask,当活动被启动的时候,系统会检测返回栈中是否存在该活动,如果存在,则直接使用它并将该活动之上的所有活动通通弹出返回栈,假如没有,则将创建新的实例。

singleInstance#

实现较为复杂。指定为singleInstance的活动会启用一个新的返回栈来管理这个活动。

技巧#

隐藏标题栏#

在加载布局前加入代码 requestWIndowsFeature(Windows.FEATURE_NO_TITLE)

知晓当前是在哪一个活动#

在项目中创建一个继承自Activity的BaseActivity,然后重写BaseActivity的onCreate()方法,在其中加入一行“Log.d(“BaseActivity”, getClass().getSimpleName());”。

然后让项目中所有其他的Activity都继承自BaseActivity。

随时随地地退出程序#

新建一个ActivityCollector类作为活动管理器,代码如下

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

public class ActivityCollector {



public static List<Activity> activities = new ArrayList<Activity>();



public static void addActivity(Activity activity) {

activities.add(activity);

}



public static void removeActivity(Activity activity) {

activities.remove(activity);

}



public static void finishAll() {

for (Activity activity : activities) {

if (!activity.isFinishing()) {

activity.finish();

}

}

}

}

创建继承自Activity的BaseActivity类(可以喝上一条中的BaseActivity一起写),如下所示

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

public class BaseActivity extends Activity {



@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

Log.d("BaseActivity", getClass().getSimpleName());

ActivityCollector.addActivity(this);

}

@Override

protected void onDestroy() {

super.onDestroy();

ActivityCollector.removeActivity(this);

}

}

以后不管在什么地方想退出程序只要调用ActivityCollector.finishAll()方法就可以了。

启动活动的最佳写法#

在SecondActivity中加入如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13

public static void actionStart(Context context, String data1, String data2) {

Intent intent = new Intent(context, SecondActivity.class);

intent.putExtra("param1", data1);

intent.putExtra("param2", data2);

context.startActivity(intent);

}

这样,在其他活动中只要输入如下一行代码就可以启动SecondActivity: 

SecondActivity.actionStart(FirstActivity.this, "data1", "data2");

第03章 UI#

控件#

  • TextVIew

  • Button

(Android开发之onClick事件的三种写法)

  • EditText

  • ImageView

  • ProgressBar

  • AlertDialog

  • ProgressDialog

  • ListView

基本布局#

  • LinearLayout

(巧妙的使用android:layout_weight属性可以有效的布局控件。)

  • RelativeLayout

  • FrameLayout

  • TableLayout

自定义控件#

引入布局#

在Layout目录下新建一个xml布局文件,在新建布局中写好自定义的布局。比如我们新建了一个名为title.xml的布局,现在我们只要在其他布局中输入代码 即可引入title.xml中的布局。

创建自定义控件#

新建一个继承自LinearLayout的类TitleLayout.class,并重写构造函数。在构造函数中调用 LayoutInflater.from(context).inflate(R.layout.title, this);这一行代码,引入自定义控件的布局。在xml布局中调用该自定义控件的方法和调用其他系统空间一样,但要在自定义控件的类名前加完整的包名,比如

1
2
3
4
5
6
7
8
9

<com.example.uicustomviews.TitleLayout

        android:layout_width="match_parent"

        android:layout_height="wrap_content">

</com.example.uicustomviews.TitleLayout>

难用的ListView#

简单用法#

  1. 在布局中加入控件

  2. 在Activity中新建一个ArrayAdapter适配器

  3. 使用listView.setAdapter()方法来为ListView设定适配器

定制ListView的界面#

  1. 定义实体类Fruit,Fruit类中有两个字段:name,imageId

  2. 在layout目录下新建fruit_item.xml,布局中包含一个ImageVIew和TextView

  3. 创建一个自定义的适配器,这个适配器继承自ArrayAdapter,并将泛型指定为Fruit类,新建类FruitAdapter。public class FruitAdapter extends ArrayAdapter{}

  4. 重写自定义适配器FruitAdapter的一组构造函数以及getView()方法

  5. 在代码中使用setAdapter()方法来使用自定义的适配器

提升ListView的效率#

  1. convertView是之前加载好的布局的缓存,使用它可以减少ListView加载布局的次数

  2. 使用一个自定义的类ViewHolder来防止ListView重复获取空间的实例

代码如下:

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

@Override

public View getView(int position, View convertView, ViewGroup parent) {

Fruit fruit = getItem(position);

View view;

ViewHolder viewHolder;

if (convertView == null) {

view = LayoutInflater.from(getContext()).inflate(resourceId, null);

viewHolder = new ViewHolder();

viewHolder.fruitImage = (ImageView) view.findViewById(R.id.fruit_image);

viewHolder.fruitName = (TextView) view.findViewById(R.id.fruit_name);

view.setTag(viewHolder);

} else {

view = convertView;

viewHolder = (ViewHolder) view.getTag();

}

viewHolder.fruitImage.setImageResource(fruit.getImageId());

viewHolder.fruitName.setText(fruit.getName());

return view;

}

ListView的点击事件#

单位和尺寸#

Android中的密度就是屏幕每英寸包含的像素数,通常以dpi为单位。比如一个手机的屏幕的宽是2英寸,长是3英寸,如果它的分辨率是320*480,那么这个屏幕的密度就是160dpi。

要获取当前屏幕的密度的代码为:

1
2
3
4
5
6
7
8
9

float xdpi = getResources().getDisplayMetrics().xdpi;

float ydpi = getResources().getDisplayMetrics().ydpi;

Log.d("MainActivity", "xdpi is " + xdpi);

Log.d("MainActivity", "ydpi is " + ydpi);

根据Android规定,在160dpi的屏幕上,1dp = 1px。

制作Nine-Patch图片#

第04章 碎片Fragment#

碎片是一种可以嵌入在活动中的UI片段,它能让程序更加合理和充分地利用大屏幕的空间,因为在平板上应用非常广泛。

第05章 广播Broadcast#

Android提供一套完整的API,允许应用程序自由的发送和接收广播,发送广播是借助Intent,而接收广播,则是靠“广播接收器Broadcast Receiver”。

Android中的广播分为两种:标准广播和有序广播。

标准广播是异步的,所有接收器会同一时间收到,所以无法被拦截。

有序广播则是一种同步执行的广播,广播发出后,同一时间只有一个广播接收器收到这条广播消息,当这个广播接收器中的逻辑执行完成后才回继续传递。优先级高的接收器可以拦截有序广播。

接收系统广播#

注册广播的方式有两种,在代码中注册和在AndroidManifest.xml中注册,前者被称为动态注册,后者被称为静态注册。

动态注册#

如何创建一个广播接收器:新建一个类,让它继承自BroadcastReceiver并重写父类的onReceive()方法就行了。

动态注册的代码如下:

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

@Override

protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_main);



    intentFilter = new IntentFilter();

    intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");

    networkChangeReceiver = new NetworkChangeReceiver();

    registerReceiver(networkChangeReceiver, intentFilter);

}





class NetworkChangeReceiver extends BroadcastReceiver {

    @Override

    public void onReceive(Context context, Intent intent) {

        ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);

        NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();

        if ((networkInfo != null) && (networkInfo.isAvailable())) {

            Toast.makeText(context, "Network is available.", Toast.LENGTH_SHORT).show();

        } else {

            Toast.makeText(context, "Network is unavailable.", Toast.LENGTH_SHORT).show();

        }



    }

}

静态注册#

1
2
3
4
5
6
7
8
9
10
11

 <receiver android:name=".BootCompleteReceiver" >

    <intent-filter>

        <action android:name="android.intent.action.BOOT_COMPLETED" />

    </intent-filter>

</receiver>

发送自定义广播#

发送标准广播#

1
2
3
4
5

Intent intent = new Intent("com.jeff.broadcasttest.MY_BROADCAST");

sendBroadcast(intent);

发送有序广播#

1
2
3
4
5

Intent intent = new Intent("com.jeff.broadcasttest.MY_BROADCAST");

sendOrderedBroadcast(intent, 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
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

public class MainActivity extends Activity {



private IntentFilter intentFilter;



private LocalReceiver localReceiver;



private LocalBroadcastManager localBroadcastManager;



@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

localBroadcastManager = LocalBroadcastManager.getInstance(this);

Button button = (Button) findViewById(R.id.button);

button.setOnClickListener(new OnClickListener() {

@Override

public void onClick(View v) {

Intent intent = new Intent(

"com.example.broadcasttest.LOCAL_BROADCAST");

localBroadcastManager.sendBroadcast(intent);

}

});

intentFilter = new IntentFilter();

intentFilter.addAction("com.example.broadcasttest.LOCAL_BROADCAST");

localReceiver = new LocalReceiver();

localBroadcastManager.registerReceiver(localReceiver, intentFilter);

}



@Override

protected void onDestroy() {

super.onDestroy();

localBroadcastManager.unregisterReceiver(localReceiver);

}



class LocalReceiver extends BroadcastReceiver {



@Override

public void onReceive(Context context, Intent intent) {

Toast.makeText(context, "received local broadcast",

Toast.LENGTH_SHORT).show();

}



}

}

第06章 数据持久化#

文件存储#

将数据存储到文件中#

Context类中提供了一个openFileOutput()方法,可用于将数据存储到指定文件中。这个方法有两个参数,

第一个参数是文件名,注意这个指定的文件名不能包含路径,因为所有文件默认都保存在/data/data//files/中

第二个参数是文件的操作模式主要有两种模式可选

MODE_PRIVATE:默认的操作模式,表示当指定同样文件名的时候,所写入的内容会覆盖原文件中的内容

MODE_APPEND:如果已存在同文件名文件则将写入内容追加到该文件后面,如果不存在,则创建新文件。

这个类返回的是FileOutputStream对象

示例代码如下:

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

public void save(String data) {

    FileOutputStream out = null;

    BufferedWriter writer = null;



    try {

        out = openFileOutput("data", Context.MODE_PRIVATE);

        writer = new BufferedWriter(new OutputStreamWriter(out));

        writer.write(data);

    } catch (IOException e) {

        e.printStackTrace();

    } finally {

        try {

            if (writer != null) {

                writer.close();

            }

        } catch (IOException e) {

            e.printStackTrace();

        }

    }

}

从文件中读取数据#

Context提供了一个openFileInput()方法,可以从文件中读取数据,它自由一个参数,就是要读取的文件名,返回的是一个FileInputStream对象

示例代码如下:

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 String load() {

    FileInputStream in = null;

    BufferedReader reader = null;

    StringBuilder content = new StringBuilder();

    try {

        in = openFileInput("data");

        reader = new BufferedReader(new InputStreamReader(in));

        String line = "";

        while ((line = reader.readLine()) != null) {

            content.append(line);

        }

    } catch (IOException e) {

        e.printStackTrace();

    } finally {

        if (reader != null) {

            try {

                reader.close();

            } catch (IOException e) {

                e.printStackTrace();

            }

        }

    }

    return content.toString();

}

SharedPreferences存储#

将数据存储到SharePreferences中#

Android中主要提供了三种方法用于得到SharedPreferences对象。

  • Context类中的getSharePreferences()方法

  • Activity类中的getPreference()方法

  • PreferenceManager类中的getDefaultSharedPreferences()方法

主要可以分为三步实现

  1. 调用SharedPreferences对象的edit()方法来获取一个SharedPreferences.Editor对象。

  2. 向SharedPreferences.Editor对象中添加数据,比如一个布尔型数据就用putBoolean()方法,以此类推

  3. 调用commit()方法将添加的数据提交,从而完成数据存储操作

示例如下:

    public void onClickSaveData(View view) {

        SharedPreferences.Editor editor = getSharedPreferences(“data”, MODE_PRIVATE).edit();

        editor.putString(“name”, “Tome”);

        editor.putInt(“Age”, 28);

        editor.putBoolean(“Married”, false);

        editor.commit();

    }

  • 从SharedPreferences中读取数据

比较简单,示例代码如下:

    public void onClickRestoreData(View view) {

        SharedPreferences pref = getSharedPreferences(“data”, MODE_PRIVATE);

        String name = pref.getString(“Name”, “”);

        int age = pref.getInt(“Age”, 0);

        boolean maried = pref.getBoolean(“Married”, false);

        Log.d(“MainActivity”, “name is “ + name);

        Log.d(“MainActivity”, “age is “ + age);

        Log.d(“MainActivity”, “married is “ + maried);

    }

  • SQLite数据库存储

  • 调试说明

在File Explorer中时无法查看数据库的具体内容的,所以我们要使用adb来帮助我们调试

首先我们打开命令行工具中,然后可以使用如下命令

ab shell :进入设备的控制台了,这时候我们可以用cd命令来进入到/data/data//databases/目录下,然后可以查看数据库文件

sqlite3 数据库名:打开数据库,并可以在里面使用一些命令来查看操作数据库

.table:查看有哪些表

.schema:查看表的建表语句

.quit/.exit:退出数据库的编辑

select等SQL通用命令,这些命令最后记得添加分号;

exit:退出设备控制台

  • 创建数据库

Android中专门提供了一个SQLiteOpenHelper帮助类,借助这个类,我们可以非常简单的对数据库进行创建和升级。

SQLiteOpenHelper是一个抽象类,所以我们要使用它的时候必须自己创建一个帮助类去继承它,而且我们必须在自己的帮助类里面重写SQLiteOpenHelper的两个抽象方法:onCreate()和onUpgrade()。

SQLiteOpenHelper还有两个非常重要的示例方法,分别是getReadableDatabase()和getWritableDatabase()。这两个方法都能创建或打开一个数据库并返回一个可对数据库进行读写的对象。不同的是,当数据库不可写入(磁盘满了)getReadableDatabase()方法返回的对象将以只读的方式去打开数据库,而getWritableDatabase()方法则将出现异常。

SQLiteOpenHelper中有两个构造方法可以重写,一般使用参数少点的那个构造方法即可。这个构造函数接收四个参数:

  1. Context

2.  数据库名

3.  第三个参数允许我们在查询数据库时返回一个自定义的Cursor,一般传入null

  1. 当前数据库的版本号

示例代码如下:

public class MyDatabaseHelper extends SQLiteOpenHelper {

    public static final String CREATE_BOOK = “create table Book (“

            + “id integer primary key autoincrement,”

            + “author text,”

            + “price real,”

            + “pages integer,”

            + “name text)”;

    public static final String CREATE_CATEGORY = “create table Category (“

            + “id integer primary key autoincrement,”

            + “category_name text,”

            + “category_code integer)”;

    private Context mContext;

    public MyDatabaseHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {

        super(context, name, factory, version);

        mContext = context;

    }

    @Override

    public void onCreate(SQLiteDatabase db) {

        db.execSQL(CREATE_BOOK);

        db.execSQL(CREATE_CATEGORY);

        Toast.makeText(mContext, “Create succeeded”, Toast.LENGTH_SHORT).show();

    }

    @Override

    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

        db.execSQL(“drop table if exists Book”);

        db.execSQL(“drop table if exists Category”);

        onCreate(db);

    }

}

然后在MainActivity中写入:

    public void onClickCreateDatabase(View view) {

        dbHelper.getWritableDatabase();

    }

  • 升级数据库

参见上一条的示例代码

  • 添加数据

Android特有方法:

    public void onClickAddData(View view) {

        SQLiteDatabase db = dbHelper.getWritableDatabase();

        ContentValues values = new ContentValues();

        values.put(“name”, “The Da Vinci Code”);

        values.put(“author”, “Dan Brown”);

        values.put(“pages”, 454);

        values.put(“price”, 16.96);

        db.insert(“Book”, null, values);

        values.clear();

        values.put(“name”, “The Lost Symbol”);

        values.put(“author”, “TDan Brown”);

        values.put(“pages”, 510);

        values.put(“price”, 19.95);

        db.insert(“Book”, null, values);

    }

SQL语句:

db.execSQL(“insert into Book (name, author, pages, price) values(?, ?, ?, ?)”,

                    new String[]{“Game of Thrones”, “George Martin”, “720”, “20.85”});

  • 更新数据

Android特有方法:

public void onClickUpdateData(View view) {

        SQLiteDatabase db = dbHelper.getWritableDatabase();

        ContentValues values = new ContentValues();

        values.put(“price”, 10.99);

        db.update(“Book”, values, “name = ?”, new String[]{“The Da Vinci Code”});

    }

SQL语句:

db.execSQL(“update Book set price = ? where name = ?”, new String[]{“10.99”, “The Da Vinci Code”});

  • 删除数据

Android特有方法:

public void onClickDeleteData(View view) {

        SQLiteDatabase db = dbHelper.getWritableDatabase();

        db.delete(“book”, “pages < ?”, new String[]{“500”});

    }

SQL语句:

    db.execSQL(“delete from Book where pages < ?”, new String[]{“500”});

  • 查询数据

查询是最为复杂的,SQLiteDatabase实例提供的querry()方法有整整七个参数,其具体情况如下所示:

table:指定查询的表名 from table_name

columns:指定查询的列名 select column1,column2

selection:指定where的hue条件 where column = value

shectionArgs:为where中的占位符提供具体的值 -

groupBy:指定需要group by的列 group by column

having:对group by后的结果进一步约束 having column = value

orderBy:指定查询结果的排序方式 order by column1,column2

简单例子的示例代码如下:

public void onClickQueryData(View view) {

        SQLiteDatabase db = dbHelper.getWritableDatabase();

        Cursor cursor = db.query(“Book”, null, null, null, null, null, null);

        if (cursor.moveToFirst()) {

            do {

                String name = cursor.getString(cursor.getColumnIndex(“name”));

                String author = cursor.getString(cursor.getColumnIndex(“author”));

                int pages = cursor.getInt(cursor.getColumnIndex(“pages”));

                double price = cursor.getDouble(cursor.getColumnIndex(“price”));

                Log.d(“MainActivity”, “Book’s name is “ + name);

                Log.d(“MainActivity”, “Book’s author is “ + author);

                Log.d(“MainActivity”, “Book’s pages are “ + pages);

                Log.d(“MainActivity”, “Book’s price is “ + price);

            } while (cursor.moveToNext());

        }

        cursor.close();

    }

SQL语句:

    db.rawQuery(“select * from Book”, null);

  • 技巧

  • 使用事务

使用事务能能保证一系列任务能全部完成或者都不完成。

    public void onClickReplaceData(View view) {

        SQLiteDatabase db = dbHelper.getWritableDatabase();

        db.beginTransaction();

        try {

            db.execSQL(“delete from Book”);

            /*if (true) {

                throw new NullPointerException();

            }*/

            db.execSQL(“insert into Book (name, author, pages, price) values(?, ?, ?, ?)”,

                    new String[]{“Game of Thrones”, “George Martin”, “720”, “20.85”});

            db.setTransactionSuccessful();

        } catch (Exception e) {

            e.printStackTrace();

        } finally {

            db.endTransaction();

        }

    }

  • 升级数据库的最佳写法

详细见P263页,摘要代码如下:

@Override

public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

        switch (oldVersion) {

            case 1:

                db.execSQL(“CREATE_CATEGORY”);

            case 2:

                db.execSQL(“alter table Book add column category_id integer”);

            default:

  • 技巧

  • TextUtils.isEmpty( String str)

TextUtils.isEmpty()方法能一次进行两次控制判断,当传入的字符串等于null或者空字符串时,这个方法都会返回true。

  • 第07章 内容提供器Content Provider

  • 访问其他程序中的数据

  • ContentResolver

Android中是借用ContentResolver类来访问内容提供器中的数据,可以通过Context中的getContentResolver()的方法来获取该类的实例。

ContentResolver中提供了如下方法用于对数据的CRUD操作。

public Uri insert(Uri uri, ContentValues values) 返回增加的数据的Uri地址

public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) 返回更新的数据的ID

public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) 

public int delete(Uri uri, String selection, String[] selectionArgs) 返回删除的数据的ID

  • URI

内容URI给内容提供器中的数据建立的唯一的标识符,例如:”content://com.example.appname.provider/table1” 它只要它主要由如下三个部分组成

  • 协议声明

 

      “content://“

  • 权限

 

      “com.jeff.databasetest.provider” 用作对不同程序的区分,一般都是使用程序的包名来命名

  • 路径

 

      “/table1” 用于对同一程序中的不同的表作区分

利用Uri.parse()方法可以使内容URI字符转化成Uri对象

Uri uri = Uri.parse(“content://com.example.appname.provider/table1”);

  • 创建内容提供器

内容过于琐碎,这里只贴一个数据库联系程序中内容提供者的例子代码:

public class DatabaseProvider extends ContentProvider {

    public static final int BOOK_DIR = 0;

    public static final int BOOK_ITEM = 1;

    public static final int CATEGORY_DIR = 2;

    public static final int CATEGORY_ITEM = 3;

    public static final String AUTHORITY = “com.jeff.databasetest.provider”;

    private static UriMatcher uriMatcher;

    private MyDatabaseHelper dbHelper;

    static {

        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

        uriMatcher.addURI(AUTHORITY, “book”, BOOK_DIR);

        uriMatcher.addURI(AUTHORITY, “book/#”, BOOK_ITEM);

        uriMatcher.addURI(AUTHORITY, “category”, CATEGORY_DIR);

        uriMatcher.addURI(AUTHORITY, “category/#”, CATEGORY_ITEM);

    }

    @Override

    public boolean onCreate() {

        dbHelper = new MyDatabaseHelper(getContext(), “BookStore.db”, null, 2);

        return true;

    }

    @Override

    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {

        SQLiteDatabase db = dbHelper.getWritableDatabase();

        Cursor cursor = null;

        switch (uriMatcher.match(uri)) {

            case BOOK_DIR:

                cursor = db.query(“Book”, projection, selection, selectionArgs, null, null, sortOrder);

                break;

            case BOOK_ITEM:

                String bookId = uri.getPathSegments().get(1);

                cursor = db.query(“Book”, projection, “id = ?”, new String[]{“bookId”}, null, null, sortOrder);

                break;

            case CATEGORY_DIR:

                cursor = db.query(“Category”, projection, selection, selectionArgs, null, null, sortOrder);

                break;

            case CATEGORY_ITEM:

                String categoryId = uri.getPathSegments().get(1);

                cursor = db.query(“Category”, projection, “id = ?”, new String[]{categoryId}, null, null, sortOrder);

                break;

            default:

                break;

        }

        return cursor;

    }

    @Override

    public String getType(Uri uri) {

        switch (uriMatcher.match(uri)) {

            case BOOK_DIR:

            case CATEGORY_DIR:

                return “vnd.android.cursor.dir/vnd.” + AUTHORITY + “.” + uri.getPathSegments().get(0);

            case BOOK_ITEM:

            case CATEGORY_ITEM:

                return “vnd.android.cursor.item/vnd.” + AUTHORITY + “.” + uri.getPathSegments().get(0);

        }

        return null;

    }

    @Override

    public Uri insert(Uri uri, ContentValues values) {

        SQLiteDatabase db = dbHelper.getWritableDatabase();

        Uri uriReturn = null;

        switch (uriMatcher.match(uri)) {

            case BOOK_DIR:

            case BOOK_ITEM:

                long newBookId = db.insert(“Book”, null, values);

                uriReturn = Uri.parse(“content://“ + AUTHORITY + “/book/“ + newBookId);

                break;

            case CATEGORY_DIR:

            case CATEGORY_ITEM:

                long newCategoryId = db.insert(“Category”, null, values);

                uriReturn = Uri.parse(“content://“ + AUTHORITY + “/category/“ + newCategoryId);

                break;

            default:

        }

        return uriReturn;

    }

    @Override

    public int delete(Uri uri, String selection, String[] selectionArgs) {

        SQLiteDatabase db = dbHelper.getWritableDatabase();

        int deletedRows = 0;

        switch (uriMatcher.match(uri)) {

            case BOOK_DIR:

                deletedRows = db.delete(“Book”, selection, selectionArgs);

                break;

            case BOOK_ITEM:

                String bookId = uri.getPathSegments().get(1);

                deletedRows = db.delete(“Book”, “id = ?”, new String[]{bookId});

                break;

            case CATEGORY_DIR:

                deletedRows = db.delete(“Category”, selection, selectionArgs);

                break;

            case CATEGORY_ITEM:

                String categoryId = uri.getPathSegments().get(1);

                deletedRows = db.delete(“Category”, “id = ?”, new String[]{categoryId});

                break;

            default:

                break;

        }

        return deletedRows;

    }

    @Override

    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {

        SQLiteDatabase db = dbHelper.getWritableDatabase();

        int updatedRaws = 0;

        switch (uriMatcher.match(uri)) {

            case BOOK_DIR:

                updatedRaws = db.update(“Book”, values, selection, selectionArgs);

                break;

            case BOOK_ITEM:

                String bookId = uri.getPathSegments().get(1);

                updatedRaws = db.update(“Book”, values, “id = ?”, new String[] {bookId});

                break;

            case CATEGORY_DIR:

                updatedRaws = db.update(“Category”, values, selection, selectionArgs);

                break;

            case CATEGORY_ITEM:

                String categoryId = uri.getPathSegments().get(1);

                updatedRaws = db.update(“Category”, values, “id = ?”, new String[]{categoryId});

                break;

            default:

                break;

        }

        return updatedRaws;

    }

}

同时,还得在AndroidManifes.xml中为自建的内容提供器注册,并表明外部程序是否可以访问(android:exported):

<provider

            android:authorities=”com.jeff.databasetest.provider”

            android:name=”.DatabaseProvider”

            android:exported=”true” />

  • 第08章 手机多媒体

  • 使用通知

  • 创建通知的步骤

  1. 获取NotificationManager的实例

  2. 创建一个Notification对象

  3. 对通知的布局进行设置

  4. 对通知的提醒方式(声音,震动,led灯)进行设置

  5. 设置好点击通知的动作(比如跳转到某个Activity)

  6. 调用NotificationManager的notify()方法让通知显示出来

  • 具体代码

// 创建NotificationManager和Notification的实例

NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

Notification notification = new Notification(R.mipmap.ic_launcher,

          “This is ticker text”, System.currentTimeMillis());

// 设置通知铃声

Uri soundUri = Uri.fromFile(new File(“/system/media/audio/notifications/Adara.ogg”));

notification.sound = soundUri;

// 设置震动

long[] vibrates = {0, 1000, 1000, 1000};

notification.vibrate = vibrates;

//设置LED灯

notification.ledARGB = Color.GREEN;

notification.ledOnMS = 1000;

notification.flags = Notification.FLAG_SHOW_LIGHTS;

//设置为系统默认方式

notification.defaults = Notification.DEFAULT_ALL;

//设置点击进行跳转

Intent intent = new Intent(this, NotificationActivity.class);

PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);

notification.setLatestEventInfo(this, “This is content title”, “This is content text”, pendingIntent);

//发送通知

manager.notify(1, notification);

  • 接收和发送短信

当手机收到一条短信的时候就会发出一个值为”android.provider.Telephony.SMS_RECEIVED”的有序广播,这条广播里带着与短信有关的所有数据,所有应用都可以在广播接收器里对它进行监听,收到广播时再从中解析出短信的内容,甚至可以拦截短信。

  • 接收短信

    class MsgReceiver extends BroadcastReceiver {

        @Override

        public void onReceive(Context context, Intent intent) {

            Bundle bundle = intent.getExtras();

    // 使用pdu密钥来提取一个 SMS pdus数组,其中每一个pdu字节都表示一条短信消息

            Object[] pdus = (Object[]) bundle.get(“pdus”);

            SmsMessage[] messages = new SmsMessage[pdus.length];

            for (int i = 0; i < messages.length; i++) {

                messages[i] = SmsMessage.createFromPdu((byte[])pdus[i]);

            }

// smsMessage的getOriginatingAddress()方法可以获取短信的发送方的地址

            String address = messages[0].getOriginatingAddress();

            String fullMessage = “”;

            for (SmsMessage message : messages) {

                fullMessage += message.getMessageBody();

            }

            sender.setText(address);

            content.setText(fullMessage);

// 拦截短信,不让短信广播继续向优先级低的广播传播

            abortBroadcast();

        }

    }

  • 发送短信

@Override

    protected void onCreate(Bundle savedInstanceState) {

to = (EditText) findViewById(R.id.to);

        msgInput = (EditText) findViewById(R.id.msg_input);

        send = (Button) findViewById(R.id.send);

        sendFilter = new IntentFilter();

        sendFilter.addAction(“SENT_SMS_ACTION”);

        sendStatusReceiver = new SendStatusReceiver();

        registerReceiver(sendStatusReceiver, sendFilter);

        send.setOnClickListener(new View.OnClickListener() {

            @Override

            public void onClick(View v) {

                SmsManager smsManager = SmsManager.getDefault();

                Intent sentIntent = new Intent(“SENT_SMS_ACTION”);

                PendingIntent pendingIntent = PendingIntent.getBroadcast(MainActivity.this, 0, sentIntent, 0);

                smsManager.sendTextMessage(to.getText().toString(), null,

                msgInput.getText().toString(), pendingIntent, null);

            }

        });

}

// 确认短信已经发出

    class SendStatusReceiver extends BroadcastReceiver {

        @Override

        public void onReceive(Context context, Intent intent) {

            if (getResultCode() == RESULT_OK) {

                Toast.makeText(context, “Send succeeded”, Toast.LENGTH_SHORT).show();

            } else {

                Toast.makeText(context, “Send failed”, Toast.LENGTH_SHORT).show();

            }

        }

    }

  • 调用摄像头和相册

    public static final int TAKE_PHOTO = 0;

    public static final int CROP_PHOTO = 1;

    private Button btnTakePhoto;

    private Button btnChooseFromAlbum;

    private ImageView picture;

    private Uri imageUri;

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        btnTakePhoto = (Button) findViewById(R.id.btn_take_photo);

        btnChooseFromAlbum = (Button) findViewById(R.id.btn_choose_from_album);

        picture = (ImageView) findViewById(R.id.picture);

// 调用摄像头拍取照片

        btnTakePhoto.setOnClickListener(new View.OnClickListener() {

            @Override

            public void onClick(View v) {

                File outputImage = new File(Environment.getExternalStorageDirectory(), “output_image.jpg”);

                try {

                    if (outputImage.exists()) {

                        outputImage.delete();

                    }

                    outputImage.createNewFile();

                } catch (IOException e) {

                    e.printStackTrace();

                }

                imageUri = Uri.fromFile(outputImage);

                Intent intent = new Intent(“android.media.action.IMAGE_CAPTURE”);

                intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);

                startActivityForResult(intent, TAKE_PHOTO);

            }

        });

// 从相册中挑选照片

        btnChooseFromAlbum.setOnClickListener(new View.OnClickListener() {

            @Override

            public void onClick(View v) {

                File outputImage = new File(Environment.getExternalStorageDirectory(), “output_image.jpg”);

                try {

                    if (outputImage.exists()) {

                        outputImage.delete();

                    }

                    outputImage.createNewFile();

                } catch (IOException e) {

                    e.printStackTrace();

                }

                imageUri = Uri.fromFile(outputImage);

                Intent intent = new Intent(“android.intent.action.GET_CONTENT”);

                intent.setType(“image/*”);

                intent.putExtra(“crop”, true);

                intent.putExtra(“scale”, true);

                intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);

                startActivityForResult(intent, CROP_PHOTO);

            }

        });

    }

    @Override

    protected void onActivityResult(int requestCode, int resultCode, Intent data) {

        switch (requestCode) {

//拍照后对拍摄的照片进行剪裁

            case TAKE_PHOTO:

                if (resultCode == RESULT_OK) {

                    Intent intent = new Intent(“com.android.camera.action.CROP”);

                    intent.setDataAndType(imageUri, “image/*”);

                    intent.putExtra(“scale”, true);

                    intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);

                    startActivityForResult(intent, CROP_PHOTO);

                }

                break;

// 剪裁完后的图片会被设为ImageView的资源

            case CROP_PHOTO:

                if (resultCode == RESULT_OK) {

                    try {

                        Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(imageUri));

                        picture.setImageBitmap(bitmap);

                    } catch (FileNotFoundException e) {

                        e.printStackTrace();

                    }

                }

                break;

            default:

                break;

        }

    }

  • 播放多媒体文件

  • 播放音频

Android中播放音频一般都是使用MediaPlayer类来实现的,MediaPlayer提供了一些较为常用的控制方法。

  • 播放视频

Android中系统提供了VideoView来播放少数几个格式的视频。

  • 第09章 服务Service

服务(Service)是Android中实现程序后台运行的解决方案,它非常适合去执行那些不需要和用户交互还要求长期执行的任务。

但是,服务不是运行在一个独立的进程中的,而是依赖于创建服务时,所在的应用程序进程,当某个应用进程被杀掉时,所偶依赖于该进程的服务也会停止运行。

另外也不要被服务的后台属性给迷惑,实际上服务是不会自己自动开启线程的,所有的代码其实还是运行在主线程中的。也就是说,我们要在服务里手动创建线程去完成任务,否则很可能会造成主线程的堵塞

  • Android的多线程编程

  • 线程的三种创建方式

  • 新建一个类去继承Thread类,并重写父类的run()方法

代码如下所示:

class MyThread extends Thread {

    @Override

    public void run(){

        //  处理具体的逻辑

    }

}

然后在要启动线程的时候写下如下代码:

new MyThread().start()

  • 实现Runnable接口

让一个类实现Runnable接口:

class MyThread implements Runnable {

    @Override

    public void run(){

        // 处理具体的逻辑

    }

}

启动线程的方法:

MyThread myThread = new MyThread();

newThread(myThread).start();

  • 匿名类(最为常见)

代码如下,记得不要漏了后面的.start()

new Thread(new Runnable(){

    @Override

    public void run(){

        // 处理具体的逻辑

    }

}).start();

PS:在public void run(){}中写入stopSelf()后,线程会在执行完任务后自动销毁。

  • 在子线程中更新UI

Android的UI是线程不安全的,想要更新应用程序中的UI元素,则必须在主线程中进行,否则就会出现异常。要想在子线程中对UI进行改变,可以采取异步消息处理的方法或者AsyncTask

  • 异步消息处理的方法

  • 例子

public class MainActivity extends Activity implements OnClickListener {

public static final int UPDATE_TEXT = 1;

private TextView text;

private Button changeText;

private Handler handler = new Handler() {

public void handleMessage(Message msg) {

switch (msg.what) {

case UPDATE_TEXT:

text.setText(“Nice to meet you”);

break;

default:

break;

}

}

};

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

text = (TextView) findViewById(R.id.text);

changeText = (Button) findViewById(R.id.change_text);

changeText.setOnClickListener(this);

}

@Override

public void onClick(View v) {

switch (v.getId()) {

case R.id.change_text:

new Thread(new Runnable() {

@Override

public void run() {

Message message = new Message();

message.what = UPDATE_TEXT;

handler.sendMessage(message);

}

}).start();

break;

default:

break;

}

}

}

  • 异步消息处理机制

Android中的异步消息处理主要由四个部分组成

  • Message

Message是线程之间传递的消息,它可以再内部携带少量的信息,用于不同线程之间的交换数据。在上面的例子中我们使用到了 Message 的 what 字段, 除此之外还可以使用 arg1 和 arg2 字段来携带一些整形数据,使用 obj 字段来携带一个 Object 对象。

  • Handler

Handler 顾名思义也就是处理者的意思,它主’用于发送和处理消息的,发送消息一般使用 Handler 的 sendMessage() 方法, 发出的消息经过一系列的辗转处理后,最终会传递到 Handler 的 HandlerMessage() 方法中。

  • MessageQueue

MessageQueue 是消息队列,它用来存放所有通过Handler发送的消息。这些消息会一直呆在 MessageQueue中等待被处理。每一个线程中只会有一个MessageQueue对象。

  • Looper

Looper 是每个线程中 MessageQueue 的管家,调用Looper的loop()方法后,就会进入到一个无限循环中,然后每当发现 MessageQueue中存在消息,就会将它取出,并传递到 Handler 的 handlerMessage() 方法中。每个线程也只会有一个Looper

  • AsyncTask

  • 定义一个服务

新建一个类继承Service, 并重写父类的 onBind(Intent intent)方法具体代码如下

public class MyService extends Service {

@Override

public IBinder onBind(Intent intent) {

return null;

}

@Override

public void onCreate() {

super.onCreate();

Log.d(“MyService”, “onCreate executed”);

}

@Override

public int onStartCommand(Intent intent, int flags, int startId) {

Log.d(“MyService”, “onStartCommand executed”);

return super.onStartCommand(intent, flags, startId);

}

@Override

public void onDestroy() {

super.onDestroy();

Log.d(“MyService”, “onDestroy executed”);

}

}

在上面的代码中重写了服务中最常用的三个方法:onCreate(), onStartCommand(), onDestory()。其中onCreate()会在服务被创建的时候被调用,onStartCommand()会在服务启动时被调用,而onDestory()则是在服务被销毁的时候被调用。其中,服务第一次启动后假如没有执行onDestory()那么再次启动它的时候不会再调用onCreate()  方法,但是还是会调用onStartCommand()方法,而无论调用多少次 onStartCommand()  方法, 服务都只会存在一个实例。

每一个服务都要在AndroidManifest.xml中注册才行

  • 启动和停止服务

case R.id.start_service:

Intent startIntent = new Intent(this, MyService.class);

startService(startIntent);

break;

case R.id.stop_service:

Intent stopIntent = new Intent(this, MyService.class);

stopService(stopIntent);

break;

  • 活动和服务进行通信

活动和服务的通信可以通过用Binder来将活动和服务绑定来解决

在Service中的代码如下:

public class MyService extends Service {

private DownloadBinder mBinder = new DownloadBinder();

class DownloadBinder extends Binder {

public void startDownload() {

new Thread(new Runnable() {

@Override

public void run() {

// start downloading

}

}).start();

Log.d(“MyService”, “startDownload executed”);

}

public int getProgress() {

Log.d(“MyService”, “getProgress executed”);

return 0;

}

}

@Override

public IBinder onBind(Intent intent) {

Log.d(“MyService”, “onBind executed”);

return mBinder;

}

}

在启动服务代码所在的活动中写代码如下:

private MyService.DownloadBinder downloadBinder;

private ServiceConnection connection = new ServiceConnection() {

@Override

public void onServiceDisconnected(ComponentName name) {

}

@Override

public void onServiceConnected(ComponentName name, IBinder service) {

downloadBinder = (MyService.DownloadBinder) service;

downloadBinder.startDownload();

downloadBinder.getProgress();

}

};

@Override

public void onClick(View v) {

switch (v.getId()) {

case R.id.bind_service:

Intent bindIntent = new Intent(this, MyService.class);

bindService(bindIntent, connection, BIND_AUTO_CREATE); 

break;

case R.id.unbind_service:

unbindService(connection);

break;

default:

break;

}

}

  • 使用前台服务

前台服务和后台服务最大的区别在于,它会一直有个正在运行的图标显示在系统的状态栏,下拉后可以看到详细信息,非常类似通知,比如天气应用显示在通知栏的天气信息就是前台服务。创建前台服务的其实挺简单的,示例代码如下:

public class MyService extends Service {

@Override

public void onCreate() {

super.onCreate();

Notification notification = new Notification(R.drawable.ic_launcher,

“Notification comes”, System.currentTimeMillis());

Intent notificationIntent = new Intent(this, MainActivity.class);

PendingIntent pendingIntent = PendingIntent.getActivity(this, 0,

notificationIntent, 0);

notification.setLatestEventInfo(this, “This is title”, “This is content”,

pendingIntent);

startForeground(1, notification);

Log.d(“MyService”, “onCreate executed”);

}

}

可以看出,这个过程和创建通知几乎一模一样,就是最后不是使用NotificationManager来将通知显示出来,而是调用的 startForeground()方法,这个方法的第一个参数是通知的id。调用startForeground()方法就能让MyService变成一个前台服务,并在系统状态栏中显示出来

  • 使用IntentService

使用Android提供的IntentService类可以简单地创建一个异步的,会自动停止的服务

-  第10章 网络技术

  • WebView

  • 使用Http协议访问网络

  • HttpURLConnection

  • HttpClient

  • 解析XML格式数据

  • Pull解析方式

  • SAX解析方式

  • 解析JSON格式数据

  • 使用JSONObject

  • 使用GSON

  • 使用 Java 的回调机制来解决在线程里使用Http协议访问网络无返回结果的问题

  • 疑惑

这里记录一些我在看书过程中产生的疑问,我会抽时间去搜寻并了解他们,最后把我的解读放在下面

  • Context到底是什么?

  • PendingIntent是什么?