Skip to content

Latest commit

 

History

History
1914 lines (1395 loc) · 76.4 KB

第一行代码(第二版).md

File metadata and controls

1914 lines (1395 loc) · 76.4 KB

一、开始启程—你的第一行Android代码

2003年10月,Andy Rubin等人创办了Android公司,2005年8月谷歌收购了这家仅仅成立了22个月的公司,并让Andy Rubin继续负责Android项目。2008年谷歌推出了Android系统的第一个版本。

由于谷歌的开发政策,任何手机厂商和个人都能免费获取到Android操作系统的源码,并且可以自由地使用和定制。

了解全貌—Android王国简介

Android 系统架构

Android大致可以分为四层架构:Linux内核层、系统运行库层、应用框架层和应用层。

1.Linux内核层

Android系统是基于Linux内核的,这一层为Android设备的各种硬件提供了底层的驱动,如显示驱动、音频驱动、照相机驱动、蓝牙驱动、Wi-Fi驱动、电源管理等。

2.系统运行库层

这一层通过一些C/C++库来为Android系统提供了主要的特性支持。如SQLite库提供了数据库的支持,OpenGl|ES库提供了3D绘图的支持,Webkit库提供了浏览器内核的支持等。

同样这一层还有Android运行时库,它主要提供了一些核心库,能够允许开发者使用Java语言来编写Android应用。另外,Android运行库中还包含了Dalvik虚拟机(5.0系统之后改为ART运行环境),它使得每一个Android应用都能运行在独立的进程中,并且拥有一个自己的Dalvik虚拟机示例。相较于Java虚拟机,Dalvik是专门为移动设备定制的,它针对手机内存、CPU性能有限等情况做了优化处理。

3.应用框架层

这一层主要提供了构建应用程序时可能用到的各种API,Andorid自带的一些核心应用就是使用这些API完成的,开发者也可以通过使用这些API来构建自己的应用程序。

4.应用层

所有安装在手机上的应用程序都是属于这一层的,比如系统自带的联系人、短信等程序,或者是你从Google Play上下载的小游戏,当然还包括你自己开发的程序。

image

Android 已发布的版本

2008年9月,谷歌正式发布了Android 1.0系统。2011年2月,谷歌发布了Android 3.0系统,这个系统是专门为平板电脑设计的,但也是Android为数不多的比较失败的版本,同年10月,谷歌又发布了Android 4.0系统。2014年Google I/O大会上,谷歌推出了号称史上版本改动最大的Android 5.0系统,其中使用了ART运行环境替代了Dalvik虚拟机,大大提升了应用的运行速度,还提出了Material Design的概念来优化应用的界面设计。除此之外,还推出了Android Wear、Android Auto、Android TV系统,从而进军可穿戴设备、汽车、电视等全新领域。2015年Google I/O大会推出了Android 6.0系统,加入运行时权限功能。2016年Google I/O大会上推出了Android 7.0系统,加入多窗口模式功能。

Android应用开发特色

Android系统提供了一些东西供我们开发出优秀的应用程序。

1.四大组件

Android系统四大组件分别是活动(Activity)、服务(Service)、广播接收器(Broadcast Receiver)、内容提供器(Content Provider)。其中活动是所以Android应用程序的门面,凡是在应用程序中你看得到的东西,都是放在活动中的。而服务就比较低调了,你无法看到它,但它会一直在后台默默地运行,即使用户退出了应用,服务仍然是可以继续运行的。广播接收器允许你的应用接收来自各处的广播消息,比如电话、短信等,你也可以向外发出广播信息。内容提供器为应用程序之间提供共享数据,比如可以用它读取系统电话簿中的联系人。

2.丰富的系统控件

Android系统提供了丰富的系统控件,并且你也可以进行自定义控件。

3.SQLite数据库

Android系统还自带了这种轻量级、运行速度极快的嵌入式关系型数据库。它不仅支持标准的SQL语法,还可以通过Android封装好的API进行操作,让存储和读取数据变得非常方便。

4.强大的多媒体

如音乐、视频、录音、拍照、闹铃等等,这一切你都可以在程序中通过代码进行控制。

5.地理位置定位

现在的Android手机都内置有GPS,如果结合功能强大的地图功能,LBS这一领域潜力无限。

手把手带你搭建开发环境

准备所需要的工具

  • JDK。JDK是Java语言的软件开发工具包,它包含了Java的运行环境、工具集合、基础类库等内容。
  • Android SDK。Android SDK是谷歌提供的Android开发工具包,在开发Android程序时,需引入它使用Android相关API。
  • Android Studio。它是谷歌在2013年推出的官方IDE工具,比起在Eclipse上安装ADT插件来开发Android程序,Android Studio则是专业的Android开发IDE。

搭建开发环境

直接到Android官网下载最新开发工具即可,如果没翻墙,可以去这个地址下载: 百度云下载

安装Android Studio时只需一直Next,在INstall Type时选择Standard类型,再一直Next,点击Finish,配置就完成了。最后等待Android Studio联网下载更新完成就会进入Android Studio的欢迎界面。

创建你的第一个Android项目

创建HellowWorld项目

在欢迎界面点击Start a new Android Studio project,会打开一个创建新项目的界面。对应的属性信息如下:

  • Application name。应用名称。
  • Company Domain。公司域名。
  • Package name。项目的包名。Android系统通过包名来区分程序,需保证其唯一性。Android Studio会根据应用名称和公司域名来自动生成合适的包名,你也可以点击右侧的Edit自行修改。
  • Project location。项目代码存放的位置。

接下来点击Next即对项目的最低兼容版本进行配置。

  • Minimum SDK。最小SDK版本,一般指定为API 19即可,想要兼容更低的设备,指定为API 15。
  • Wear、TV、Android Auto。分别对应可穿戴设备、电视、汽车程序。

接下来点击Next选择内置的模板-Empty Activity即可。

继续点击Next,可以给创建的活动和布局命名。

  • Activity Name。活动名称,命名规范为HelloWorldActivity。
  • Layout Name。布局名称,命名规范为activity_hello_world。

最后,点击Finish,项目创建成功。

启动模拟器

观察Android Studio顶部工具栏中的图标,找到名称为AVD Mangaer的手机图标区域(右下方有个安卓机器人),点击会弹出虚拟设备列表弹框,在下方点击Create Virtual Device安卓即可进入Select HardWare界面,这里有很多设备可供我们选择,除了创建手机模拟器,还可以创建平板、手表、电视模拟器。选择你想要的设备和最新的Android系统版本,指定或不指定其它的信息(模拟器名字、分辨率、横竖屏等等)之后,点Finish即可在模拟器列表中看到一个创建好的模拟器设备了,点击三角形启动按钮即可启动模拟器。

运行HelloWorld

观察Android Studio顶部工具栏中的图标,如下图所示。其中左边的锤子是用来编译项目的,中间的下拉列表是用来选择运行哪一个项目的,通常app就是当前的主项目,右边的三角形按钮时用来运行项目的。

image

点击中间按钮,选择刚刚刚刚创建好的虚拟设备即可运行该应用。

分析你的第一个Android程序

项目的结构列表位于最左边,刚创建的新项目默认使用Android模式的项目结构,但这并不是真实的目录结构,是为了快速开发而设置的,点击结构列表上面的下拉列表,选择Proejct,即看到真实的目录结构。

image

1..gradle和.idea

自动生成的文件,不管它。

2.app

项目的代码、资源目录,开发工作基本在此目录下进行。

3.build

主要包含了一些在编译时自动生成的文件。

4.gradle

目录下包含了gralde wrapper的配置文件,使用gradle wrapper的方式不需要提前将gradle下载好,而是会自动根据本地的缓存情况决定是否需要联网下载gradle。Android Studio默认不启动gradle wrapper的方式,可以点击AS导航栏->File->Settings->Build,Execution,Deployment->Gradle进行配置更改。

5..gitignore

将指定的目录或文件排除在版本控制之外。

6.build.gradle

项目全局的gradle构建脚本。

7.gradle.properties

这个文件时全局的gradle配置文件,配置的属性会影响项目所有的gradle编译脚本。

8.gradle和gradlew.bat

用来在命令行界面中执行gradle命令,其中gradlew是在Linux或Mac系统中使用的,gradlew.bat是在Windows系统中使用的。

9.HelloWorld.iml

iml文件是所有IntelliJ IDEA项目都会自动生成的一个文件(Android Studio基于IntelliJ IDEA),用于标识这是一个IntelliJ IDEA项目。

10.local.properties

用于指定本机中的SDK和NDK路径,自动生成,如果SDK/NDK位置发生改变,改成新位置即可。

11.settings.gradle

用于指定项目中所有引入的模块。一般都是自动引入。

接下来详细分析app目录,因为大部分的开发工作在此目录下。

image

1.build

与外层的build目录类型,包含编译时自动生成的文件。

2.libs

用于存放第三方jar包,其会被自动添加到构建路径中去。

3.androidTest

用来编写Android Test测试用例的,可以对项目进行一些自动化测试。

4.java

放置Java代码的地方。

5.res

用于存放图片、布局、字符串等资源。比如说,图片放在drawable目录下,布局放在layout目录下,字符串放在values目录下。

6.AndroidManifest.xml

整个项目的配置文件,用于定义四大组件和添加静态权限声明。

7.test

用来编写Unit Test测试用例。

8..gitignore

和外层.gitignore文件类似。

9.app.iml

和外层app.iml文件类似。

10.build.gradle

app模块的gradle构建脚本,其中会指定很多项目构建相关的配置。

11.proguard-rules.pro

用于指定代码的混淆规则,混淆会使破解者难以阅读反编译的代码。

接下来分析项目如何运行起来。

image

这段代码表示对SplashActivity这个活动进行注册,没有在AndroidManifest.xml里注册的活动是不能使用的。其中intent-filter里的两行代码非常重要:

<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />

这表示了SplashActivity是这个项目的主活动,即应用程序的入口。

打开HelloWorldActivity,它是继承自AppCompatActivity的,这是一种向下兼容的Acitivity,可以将Activity在各个系统版本中增加的特性和功能最低兼容到Android 2.1系统。Activity是系统提供的一个活动基类,必须继承它或者它的子类才能拥有活动的特性(AppCompatActivity是Activity的子类)。

Android程序设计为逻辑和视图分离,因此使用setContentView()的方式将对应的布局引进来。

详解项目中的资源

  • drawable。所有以drawable开头的文件夹用来存放图片。
  • mipmap。所有mipmap开头的文件夹用来存放应用图标。
  • values。所有已values开头的文件夹用来存放字符串、样式、颜色等配置。
  • layout。用来存放布局文件。

其中mipmap会有hdpi、xhdpi、xxhdpi等后缀,是为了适配屏幕的分辨率,从而更好地兼容各种设备,一般放在mipmap-xxhdpi下就可以了。

res/values/strings.xml文件下

<string name="app_name">HelloWorld</string>

引用该字符串有两种方式:

  • 代码中使用R.string.helloworld。
  • XML中使用@string/hello_world。

drawble、mipmap、layout的引用与其类似。

打开AndroidManifest.xml,其中:

android:icon="..."
android:label="..."

可以看到就是使用上面的第二种引用形式来指定了项目的应用图标和应用的名称。

详解build.gradle文件

Gradle基于Groovy的领域特定语言(DSL)来声明项目设置。

打开工程目录下的build.gradle,其中两处repositories的闭包中都声明了jcenter()这行配置,它是一个代码托管仓库,声明它以后,就可以在项目中轻松引用任何jcenter上的开源项目了。

接下来,dependencies闭包使用了classpath声明了一个Gradle插件。声明它,AS才能使用Gradle构建Android项目,最后面试版本号,最新为3.2.0。

下面再来分析一下app目录下的build.gradle文件,第一行有两种值可选:

  • com.android.application。表示这是一个应用程序模块。
  • com.android.library。表示这是一个库模块。

区别为:应用程序可以直接运行,库模块智能作为代码块依附在别的应用程序模块来运行。

接下来是一个大的android闭包,用于配置项目构建的各种属性。

  • compileSdkVersion。用于指定项目的编译版本。
  • buildToolsVersion。用于指定项目构建工具的版本。

然后,在andorid闭包中又嵌套了一个defaultConfig闭包。

  • applicaitonId。指定项目的包名。
  • minSdkVersion。指定项目最低兼容的Android系统版本。
  • tartgetSdkVersion。指定的值表示你在该目标版本上已经做了充分的测试,系统将会为你的应用程序启动一些最新的功能和特性。
  • versionCode。指定项目的版本号。
  • versionName。指定项目的版本名。

接下来,看buildTypes闭包。

  • debug子闭包。指定生成测试版安装文件的配置。
  • release子闭包。指定生成正式版安装文件的配置。

子闭包中。

  • minifyEnabled。指定是否对项目的代码进行混淆。

  • proguardFiles。指定混淆时使用的规则文件,这里指定了两个文件。

    1.proguard-android.txt。在Android SDK下,所有项目通用的混淆规则。 2.proguard-rules.pro。在当前项目的根目录下,用于编写特有的混淆规则。

注意:直接运行项目生产的都是测试版安装文件。

最后,则是dependencies闭包,用于指定当前项目所有的依赖关系。通常有3钟依赖方式:

  • 本地依赖。第一行的implementation fileTree就是一个本地依赖声明,表示将libs目录下的所有.jar后缀的文件都添加到项目的构建路径当中。
  • 远程依赖。第二行的implementation则是远程依赖声明,其中com.android.support是域名部分,用于和其他公司的库做区分;appcompat-v7是组名称,用于和同一个公司中不同的库做区分;27.0.0是版本号,用于和同一个库中不同的版本做区分。声明后,Gradle会在构建项目时会首先检查一下本地是否已经有这个库的缓存,如果没有则会自动联网下载,然后在添加到项目的构建目录当中。
  • 库依赖。新建项目默认没有这个依赖,要使用它,需要创建一个和app模块的同级模块/库,然后在使用格式compile project(':helper'),即为依赖了创建的helper库。

前行必备—掌握日志工具的使用

使用Android的日志工具Log

Android的日志工具类是Log(android.util.log)。

  • Log.v()。打印最为琐碎的信息,对应级别为verbose,级别最低。

  • Log.d()。打印调试信息,对应级别debug,比verbose高一级。

  • Log.i()。打印重要信息,对应级别info,比debug高一级。

  • Log.w()。打印警告信息,对应级别warn,比info高一级。

  • Log.e()。打印错误信息,对应解绑error,比warn高一级。

    Log.d("HelloWorldActivity", "onCreate execute");

Log.d()方法有两个参数,第一个是tag,一般传入当前的类名,主要对打印信息进行过滤;第二个是msg,即想要打印的具体内容。

为什么使用Log而不使用System.out

与Log相比,System.out的缺点如下:

  • 日志打印不可控制。
  • 打印时间无法控制。
  • 不能添加过滤器。
  • 日志没有级别区分。

Log可以使用logd快速生成模块代码,并且,在方法外面输入logt可生成tag。

除了快捷键,logcat中还能轻松添加过滤器。

  • Show only selected application。只显示当前选中程序的日志。
  • Firebase。谷歌的一个分析工具。
  • No Filters。没有过滤器。
  • Edit Filter Configuration。可以自定义过滤器,比如在log Tag一栏中填入data,一次过滤出log Tag为data的数据。

日志级别控制的好处:可以很快地找到你所关心的日志。

最后,其实我用的最多的还是关键字过滤,并且它支持正则表达式。

二、先从看得到的入手—探究活动

活动是什么

活动(Activity)是一种可以包含用户界面的组件,主要和用户进行交互,一个应用程序中可以包含零个或多个活动。

活动的基本用法

创建和加载布局

  • Design切换区域是当前的可视化布局编辑器,可以预览和通过拖放的方式编辑布局。

  • Text切换区域通过XML文件的方式编辑布局。

  • @id/id_name。引用id对应的资源。

  • @+id/id_name。定义该资源的id。

  • 项目中添加的任何资源都会在R文件中生成一个相应的资源id。

在AndroidManifest文件中注册

  • 给主活动指定的label不仅会成为标题栏中的内容,还会成为启动器中应用程序显示的名称。

在活动中使用Menu

public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.main, menu);
    return true;
}
  • inflate()方法将创建的布局R.menu.main添加到系统为我们创建的menu对象中。
  • 返回true允许创建的菜单显示,false菜单则无法显示。

使用Intent在活动之间穿梭

使用隐式Intent

  • 只有和中的内容同时能够匹配上Intent中指定的action和category时,这个活动才能响应该Intent。注意:android.intent.category.DEFAULT是一种默认的category,startActivity()调用时会自动添加。
  • 每个Intent中只能指定一个action,但却能指定多个category。

更多隐式Intent的用法

启动默认浏览器

Intent intent = new Intent(Intent.ACTION_VIEW);//系统内置action,打开默认浏览器
intent.setData(Uri.parse("http://www.baidu.com"));//设置网址
startActivity(intent);

标签

  • android:scheme。指定数据的协议部分,如http。
  • android:host。指定数据的主机名部分,如www.baidu.com。
  • android:port。指定数据的端口部分,跟随主机名后面。
  • android:path。指定主机名和端口后面的部分。
  • android:mimeType。指定处理的数据类型,允许使用通配符。

注意:一般在标签中不会指定过多内容。

指定其他协议

除了http协议外,还可以指定很多其它协议,如geo:地理位置,tel:拨打电话。如下在程序中调用系统拨号界面。

Intent intent = new Intent(Intent.ACTION_DIAL);//系统内置action,打开系统拨号界面
intent.setData(Uri.parse("tel:10086"));//指定号码为10086
startActivity(intent);

活动的生命周期

体验活动的生命周期

<activity android:name=".DialogActivity"
    android:theme="@android:style/Theme.Dialog">
</activity>

指定Activity为对话框样式。

活动被回收了怎么办

当一个活动进入到了停止状态,是有可能被系统回收的。

B->A,A返回,A不会执行onRestart(),会直接执行onCreate(),也就是重新创建一次A。

活动的启动模式

A - B, B - C, 按返回键,C - A?

给B指定LauchMode为singleInstance即可。

image

活动的最佳实践

知晓当前是在哪一个活动

在BaseActivity的onCreate()中添加:

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

杀死当前程序所属的进程

android.os.Process.killProcess(android.os.Process.myPid());

编写界面的最佳实践

制作Nine-Patch图片

它是一个被特殊处理过的png图片,能够指定哪些区域可以被拉伸、哪些区域不可以。

在图片的四个边框绘制一个个的小黑点其中:

  • 上边框和左边框的部分表示当图片需要拉伸时就拉伸黑点标记的区域。
  • 下边框和右边框绘制的部分表示内容会被放置的区域。

使用鼠标在图片的边缘拖动就可以进行绘制了,按住Shift键拖动可以进行擦除。

Git创建代码仓库

Windows系统:打开Git Bash

//配置身份
git config --global user.name "JsonChao"
git config --global user.email "chao.qu521@gmail.com"

配置完成后使用同样的命令来查看是否配置成功,只需将最后的名字和邮箱地址去掉即可。

仓库是用于保存版本管理所需信息的地方。

//初始化当前文件夹为仓库
git init

根目录下隐藏的.git文件夹就是用来记录本地所有的Git操作的,如果想删除本地仓库,删除.git文件夹即可。

提交本地代码

git add build.gradle
git add app
git add .
git commit -m "First commit"

三、数据存储全方案——详解持久化技术

将数据存储到文件中

public void save() {
    String data = "Data to save";
    FileOutputStream out = null;
    BufferedWriter writer = null;
    try {
        //默认保存在/data/data/<packagename>/files/目录下的
        out = openFileOutput("data", Context.MODE_PRIVATE);
        writer = new BufferedWriter(new OutputStreamWriter(out));
        writer.writer(data);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (writer != null) {
                writer.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

从文件中读取数据

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存储

有三种方法用于得到SharedPreferences对象, SharePreferences文件都是存放在/data/data//shared_prefs/目录下的。

  • Context类中的getSharedPreferences()方法,可以任意指定其文件名。
  • Activity类中的getPreferences()方法,使用当前活动的类名作为文件名。
  • PreferenceManager类中的getDefaultSharedPreferences()方法,使用当前应用程序的包名作为前缀来命名文件名。

SQLite数据库存储

创建数据库

Android提供了SQLiteOpenHelper帮助类对数据库进行创建和升级,继承它,重写onCreate()和onUpgrade()方法去实现创建、升级数据库的逻辑。(数据库文件存放在/data/data//databases/目录下)

SQLiteOpenHelper创建或打开一个现有的数据库:

  • getReadableDatabase()
  • getWriteableDatabase()

注意:当书记库不可写入的时候(如磁盘空间已满),getReadableDatabase()方法返回的对象将以只读的方式去打开数据库,而getWritableDatabase()方法则将出现异常。

建表语句:新建一张Book表,表中有id(主键)、作者、价格、页数和书名等列。 SQLite的数据类型很简单,integer表示整形,real表示浮点型,text表示文本类型,并用autoincrement关键字表示id列是自增长的。

create table Book (
    id integer primary key autoincrement,
    author text,
    price real,
    pages integer,
    name text)

在onCreate()方法中执行建表语句:

db.execSQL(CREATE_BOOK);

升级数据库

当我们新添加一张表时,需要升级数据库,在onCreate()方法中多添加一条建表语句:

db.execSQL(CREATE_BOOK);
db.execSQL(CREATE_CATEGORY);

在onUpgrade()方法中添加以下代码:

db.execSQL("drop table if exists Book");
db.execSQL("drop table if exists Category");
onCreate(db);

增加数据库版本号即可回调onUpgrade()方法重新建表:

dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 2);

添加数据

SQLiteDatabase中提供了一个inset()方法,第一个参数是表名,第二个参数用于在未指定添加数据的情况下给某些可为空的列自动赋值NULL,一般我们用不到这个功能,直接传入null即可。第三个参数是ContentValues对象,使用它的put()方法添加数据即可。

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", "Dan Brown");
values.put("pages", 510);
values.put("price", 19.95);
db.insert("Book", null, values);//插入第二条数据

更新数据

同理,update()方法用于对数据进行更新,第一个参数为表名,第二个参数是ContentValues对象,第三、四个参数用于约束更新某一行或某几行中的数据,不指定的话默认就是更新所有行。

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语句的where部分,表示更新所有name等于?的行,而?是一个占位符,可以通过第四个参数提供的一个字符串数组为第三个参数中的每个占位符指定相应的内容。

删除数据

delete()方法用于删除数据,第一参数是表名,第二、三个参数用于约束删除某一行或某几行的数据,不指定的话默认就是删除所有行。

SQLiteDatabase db = dbHelper.getWriteDatabase();
db.delete("Book", "pages > ?", new String[] {"500"});

查询数据

query()方法用于查询数据。最短的方法重载有7个参数,第一个参数是表名,第二个参数用于指定去查询哪几列,如果不指定则默认查询所有列。第三、四个参数用于约束查询某一行或某几行的数据,不指定则默认查询所有行的数据。第五个参数用于指定需要去group by的列,不指定则表示不对查询结果进行group by操作。第六个参数用于对group by之后的数据进行进一步过滤,不指定则表示不进行过滤。第七个参数用于指定查询结果的排序方式,不指定的话则表示使用默认的排序方式。

query()方法参数         对应SQL部分                 描述
  table             from table_name             指定查询的表名
  columns           select column1,column2      指定查询的列名
  selection         where column = value        指定where的约束条件
  selectionArgs     -                           为where中的占位符提供具体的值
  groupBy           group by colum              指定需要group by的列
  having            having column = value       对group by后的结果进一步约束
  orderBy           order by column1,column2   指定查询结果的排序方式
  
SQLiteDatabase db = dbHelper.getWritableDatabase();
//查询Book表中所有的数据
Cursor cursor = db.query("Book", null, null, null, null, null, null);
if (cursor.moveToFirst()) {
    do {
        //遍历Cursor对象
        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"));
    } while (cursor.moveToNext());
}
cursor.close

使用SQL操作数据库

  • 添加数据的方法如下:

    db.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)", new String[] {"The Da Vinci Code", "Dan Brown", "454", "16.96"});

    db.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)", new String[] {"The Lost Symbol", "Dan Brown", "510", "19.95"});

  • 更新数据的方法如下:

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

  • 删除数据的方法如下:

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

  • 查询数据的方法如下:

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

四、跨程序共享数据——探究内容提供器

运行时权限

Android 6.0以上权限分为普通权限和危险权限。危险权限共9组24个。

注意:每个危险权限都属于一个权限组,我们在进行运行时权限处理时使用的是权限名,但是用户一旦同意授权了,那么该权限组所对应的权限中所有的其他权限也会同时被授权。

在程序运行时申请权限

if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.
    permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
    ActivityCompat.requestPermissions(MainActivity.this, new 
        String[]{Manifest.permission.CALL_PHONE}, 1);
} else {
    call();
}

private void call() {
    try {
        Intent intent = new Intent(Intent.ACTION_CALL);
        intent.setData(Uri.parse("tel:10086"));
        startActivity(intent);
    } catch (SecurityException e) {
        e.printStackTrace();
    }
}

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    switch (requestCode) {
        case 1:
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                call();
            } else {
                Toast.makeTest(this, "You denied the permission", Toast.LENGTH_SHORT).show();
            }
            break;
        default:
            break;
    }
}

内容URI对象定义

Uri uri = Uri.parse("content://com.example.app.provider/table1");
//结尾的1为id
Uri uri = Uri.parse("content://com.example.app.provider/table1/1");
//使用通配符
Uri uri = Uri.parse("content://com.example.app.provider/*");
Uri uri = Uri.parse("content://com.example.app.provider/table1/#");

自定义ContentProvider要注意的地方

使用UriMatcher匹配内容URI的功能。

public static final int TABLE1_DIR = 0;

public static final int TABLE1_ITEM = 1;

public static final int TABLE2_DIR = 2;

public static final int TABLE2_ITEM = 3;

private static UriMatcher uirMatcher;

static {
    uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    //addURI接收3个参数,authority、path、自定义代码
    uriMatcher.addURI("com.example.app.provider", "table1", TABLE_DIR);
    uriMatcher.addURI("com.example.app.provider", "table1/#", TABLE1_ITEM);
    uriMatcher.addURI("com.example.app.provider", "table2/#", TABLE2_ITEM);
}

@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
    switch (uriMatcher.match(uri)) {
        case TABLE1_DIR:
            //查询table1表中的所有数据
            break;
        case TABLE1_ITEM:
            //查询table1表中的单条数据
            break;
        case TABLE2_DIR:
            //查询table2表中的所有数据
            break;
        case TABLE2_ITME:
            //查询table2表中的单条数据
            break;
        default:
            break;
    }
}

getType()方法用于获取Uri对象所对应的MIME类型。一个内容URI由3部分组成:

  • 必须以vnd开头

  • 如果内容URI以路径结尾,则后接android.cursor.dir/, 如果内容URI以id结尾,则后接android.cursor.item/。

  • 最后接上vnd..。

    @Override public String getType(Uri uri) { switch (uriMatcher.match(uri)) { case TABLE1_DIR: return "vnd.android.cursor.dir/vnd.com.example.app.provider.table1"; case TABLE1_ITEM: return "vnd.android.cursor.item/vnd.com.example.app.provider.table1"; case TABLE2_DIR: return "vnd.android.cursor.dir/vnd.com.example.app.provider.table2"; case TABLE2_ITEM: return "vnd.android.cursor.item/vnd.com.example.app.provider.table2"; default: break; } return null; }

注意在清单文件注册

<provider
    android:name=".DatabaseProvider"
    android:authorities="com.example.databasetest.provider"
    android:enabled="true"
    android:exported="true">

Git命令

查看文件修改情况

git status

查看更改内容

git diff

撤销未add之前的修改

git checkout 文件

对于add后的文件,先取消添加,再撤回提交。

//取消添加使用reset
git reset HEAD 文件

查看历史提交信息

git log

查看指定提交记录的一行记录

git log 记录id -1
//查看具体修改了什么内容
git log 记录id -1 -p

五、丰富你的程序——运用手机多媒体

通知的进阶技巧

在通知播放的时候播放一段音频

setSound(Uri.fromFile(new File("/system/media/audio/ringtones/luna.ogg")))

设置手机静止和震动的时长

它是一个长整型的数组,以毫秒为单位。

//立即振动1秒,然后静止1秒,再振动1秒
setVibrate(new long[] {0, 1000, 1000, 1000})

控制手机振动需要声明权限

<users-permission android:name="android.permission.VIBRATE" />

实现LED灯不停闪烁的效果

//颜色,LED灯亮起的时长,LED灯暗去的时长,均以毫秒为单位
setLights(Color.GREEN, 1000, 1000)

使用默认设置实现上述效果

setDefaults(NotificationCompat.DEFAULT_ALL)

构建出丰富的通知效果

setStyle()
setPriority()

设置长文本的通知内容

.setStyle(new NotificationCompat.BigTextStyle().bigText("Learn how to build notifications, send and sync data, and use voice action. Get the official Android IDE and developer tools to build apps for Android"))

显示大图片

.setStyle(new NotificaitonCompat.BigPictureStyle().bigPicture(BitmapFactory.decodeResource(getResources(), R.drawable.big_image)))

设置通知的重要程度

  • PRIORITY_DEFAULT表示默认的重要程度,和不设置效果是一样的

  • PRIORITY_MIN表示最低的重要程度,系统可能只会在特定的场景才会显示这条通知,比如用户下拉状态栏的时候

  • PRIORITY_LOW表示较低的重要程度,系统可能会将这类通知缩小,或改变其显示的顺序,将其排在更重要的通知之后

  • PRIORITY_HIGH表示较高的重要程度,系统可能会将这类通知放大,或改变其显示的顺序,将其排在比较靠前的位置

  • PRIORITY_MAX表示最高的重要程度,这类通知消息必须要让用户立刻看到,甚至需要用户做出响应操作

    setPriority(NotificationCompat.PRIORITY_MAX)

使用摄像头和相册

调用摄像头拍照

public void onClick(View v) {
    //创建File对象,用于存储拍照后的图片, getExternalCacheDir()获取的是SDK卡中专门用于存放当前应用缓存数据的位置:/sdcard/Android/data/<package name>/cache
    File outputImage = new File(getExternalCacheDir(), "output_image.jpg");
    try {
        if (outputImage.exists()) {
            outputImage.delete();
        }
        outputImage.createNewFile();
    } catch (IOException e) {
        e.printStackTrace();
    }
    if (Build.VERSION.SDK_INT >= 24) {
        //从Android 7.0系统开始,直接使用本地真实路径的Uri被认为是不安全的,会抛出FileUriExposedException异常。目的:它使用了和内容提供器类似的机制来对数据进行保护,可以选择性地将封装过的Uri共享给外部,提高了应用的安全性
        imageUri = FileProvider.getUriForFile(MainActivity.this, "com.exmaple.cameraalbumtest.fileprovider", outputImage);
    } else {
        imageUri = Uri.fromFile(outputImage);
    }
    //启动相机程序
    Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
    intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
    startActivityForResult(intent, TAKE_PHOTO);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    switch (requestCode) {
        case TAKE_PHOTO:
            if (resultCode == RESULT_OK) {
                try {
                    //将拍摄的照片显示出来
                    Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(imageUri));
                    picture.setImageBitmap(bitmap);
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                }
            }
            break;
        default:
            break;
    }
}

清单文件中注册FileProvider

<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.example.cameraalbumtest.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    //指定共享路径
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

<paths xmlns:android="http://schemas.android.com/apk/res/android">
    //设置空值表示将整个SK卡进行共享
    <external-path name="my_iamges" path="" />
</paths>

//兼容4.4之前需声明权限
<user-permission android:name="android.permission.WRITE_EXTERNAL_STOREAGE" />

从相册中选中照片

public static final int CHOOSE_PHOTO = 2;

Button chooseFromAlbum = (Button) findViewById(R.id.choose_from_album);
chooseFromAlbum.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManagaer.PERMISSION_GRANTED) {
            ActvitiyCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.WRATE_EXTERNAL_STORAGE}, 1);
        } else {
            openAlbum();
        }
    }
}

private void openAlbum() {
    Intent intent = new Intent("android.intent.action.GET_CONTENT");
    intent.setType("image/*");
    startActivityForResult(intent, CHOOSE_PHOTO);//打开相册
}

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    switch (requestCode) {
        case 1:
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                openAlbum();
            } else {
                Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show();
            }
            break;
        default:
            break;
    }
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    switch (requestCode) {
        case CHOOSE_PHOTO:
            if (resultCode == RESULT_OK) {
                //判断手机系统版本号
                if (Build.VERSION.SDK_INT >= 19) {
                    //Android系统从4.4版本开始,选取相册中的图片不在返回图片的真实Uri了,则是一个封装过的Uri,需要对不同类型的Uri进行解析
                    handleImageOnKitKat(data);
                } else {
                    handleImageBeforeKitKat(data);
                }
            }
            break;
        default:
            break;
    }
}

@TargetApi(19)
private void handleImageOnKitKat(Intent data) {
    String iamgePath = null;
    Uri uri = data.getData();
    if (DocumentContract.isDocumentUri(this, uri)) {
        //如果是document类型的Uri,则通过document id处理
        String docId = DocumentsContract.getDocumentId(uri);
        if("com.android.providers.media.documents".equals(uri.getAuthority())) {
            String id = docId.split(":")[1]; //解析出数字格式的id
            String selection = MediaStore.Images.Media._ID + "=" + id;
            imagePath = getImagePath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection);
        } else if ("com.android.providers.downloads.documents".equals(uri.getAuthority())) {
            Uri contentUri = CotnentUris.withAppendedid(Uri.parse("content://downloads/public_downloads"), Long.valuesOf(docId));
            imagePath = getImagePath(contentUri, null);
        }
    } else if ("content".equalsIgnoreCase(uri.getScheme())) {
        //如果是content类型的Uri,则使用普通方式处理
        imagePath = getImagePath(uri, null);
    } else if ("file".equalsIgnoreCase(uri.getScheme())) {
        //如果是file类型的Uri,直接获取图片路径即可
        imagePath = uri.getPath();
    }
    displayImage(imagePath); //根据图片路径显示图片
}

private void handleImageBeforeKitKat(Intent data) {
    Uri uri = data.getData();
    String imagePath = getImagePath(uri, null);
    displayImage(imagePath);
}

private String getImagePath(Uri uri, String selection) {
    String path = null;
    // 通过Uri和selection来获取真实的图片路径
    Cursor cursor = getContentResolver().query(uri, null, selection, null, null);
    if (cursor != null) {
        if (cursor.moveToFirst()) {
            path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
        }
        cursor.close();
    }
    return path;
}

private void displayImage(String imagePath) {
    if (imagePath != null) {
        Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
        picture.setImageBitmap(bitmap);
    } else {
        Toast.makeText(this, "failed to get image", Toast.LENGTH_SHORT).show();
    }
}

六、看看精彩的世界——使用网络技术

WebView

//当需要从一个网页跳转到另一个网页时,我们希望目标网页仍然在当前WebView中显示,而不是打开系统浏览器
webView.setWebViewClient(new WebViewClient());

使用HTTP协议访问网络

POST请求:

connection.setRequestMethod("POST");
DataOutputStream out = new DataOutputStream(connection.getOutputStream());
out.writeBytes("username=admin&password=123456");

解析JSON格式数据

//解析Json数组
List<Person> people = gson.fromJson(jsonData, new TypeToken<List<Person>>(){}.getType());

解析异步消息处理机制

image

七、后台默默的劳动者——探究服务

服务的最佳实践——完整版的下载实例

public interface DownloadListener {
    
    void onProgress(int progress);
    
    void onSuccess();
    
    void onFailed();
    
    void onPaused();
    
    void onCanceled();

}

public class DownloadTask extends AsyncTask<String, Integer, Integer> {
    public static final int TYPE_SUCCESS = 0;
    public static final int TYPE_FAILED = 1;
    public static final int TYPE_PAUSED = 2;
    public static final int TYPE_CANCELED = 3;
    
    private DownloadListener listener;
    
    private boolean isCanceled = false;
    
    private boolean isPaused = false;
    
    private int lastProgress;
    
    public DownloadTask(DownloadListener listener) {
        this.listener = listener;
    }
    
    @Override
    protected Integer doInBackground(String... params) {
        InputStream is = null;
        RandomAccessFile savedFile = null;
        File file = null;
        try {
            long downloadedLength = 0;//记载已下载的文件长度
            String downloadUrl = params[0];
            String fileName = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
            file = new File(directory + fileName);
            if (file.exists()) {
                downloadedLength = file.length();
            }
            long contentLength = getContentLength(downloadUrl);
            if (contentLength == 0) {
                return TYPE_FAILED;
            } else if (contentLength == downloadedLength) {
                //已下载字节和文件总字节相等,说明下载完成了
                return TYPE_SUCCESS;
            }
            OkHttpClient client = new OkHttpClent();
            Request request = new Request.Builder()
                    //断点下载,指定从哪个字节开始下载
                    .addHeader("RANGE", "bytes=" + downloadedLength + "-")
                    .url(downloadUrl)
                    .build();
            Response response = client.newCall(request).execute();
            if (response != null) {
                is = responsel.body().byteStream();
                savedFile = new RandomAccessFile(file, "rw");
                savedFile.seek(downloadedLength);//跳过已下载的字节
                byte[] b = new byte[1024];
                int total = 0;
                int len;
                while ((len = is.read(b)) != -1) {
                    if (isCanceled) {
                        return TYPE_CANCELED;
                    } else if(isPaused) {
                        return TYPE_PAUSED;
                    } else {
                        total += len;
                        savedFile.write(b, 0, len);
                        //计算已下载的百分比
                        int progress = (int) ((total + downloadedLength) * 100 / contentLength);
                        publishProgress(progress);
                    }
                }
                response.body().close();
                return TYPE_SUCCESS;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
                if (savedFile != null) {
                    savedFile.close();
                }
                if (isCanceled && file != null) {
                    file.delete();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return TYPE_FAILED;
    }
    
    @Override
    protected void onProgressUpdate(Integer... values) {
        int progress = values[0];
        if (progress > lastProgress) {
            listener.onProgress(progress);
            lastProgress = progress;
        }
    }
    
    @Override
    protected void onPostExecute(Integer status) {
        switch (status) {
            case TYPE_SUCCESS:
                listener.onSuccess();
                break;
            case TYPE_FAILED:
                listener.onFailed();
                break;
            case TYPE_PAUSED:
                listener.onPaused();
                break
            case TYPE_CANCELED:
                listener.onCanceled();
                break
            default:
                break;
        }
    }
    
    public void pauseDownload() {
        isPaused = true;
    }
    
    public void cancelDownload() {
        isCanceled = true;
    }
    
    private long getContentLength(String downloadUrl) throws IOException {
        OkHttpClient client = new OkHttpClient()
        Request request = new Request.Builder()
                .url(downloadUrl)
                .build();
        Response response = client.newCall(request).execute();
        if (reponse != null && response.isSuccessful()) {
            long contentLength = response.body().contentLength();
            response.close();
            return contentLength;
        }
        return 0;
    }
}

public class DownloadService extends Service {
    
    private DownloadTask downloadTask;
    
    private String downloadUrl;
    
    private DownloadListener listener = new DownloadListener() {
        @Override
        public void onProgress(int progress) {
            getNotificationManager().notify(1, getNotification("Downloading...", progress));
        }
        
        @Override
        public void onSuccess() {
            downloadTask = null;
            //下载成功时将前台服务通知关闭,并创建一个下载成功的通知
            stopForeground(true);
            getNotificationManager().notify(1, getNotification("Download Success", -1);
            Toast.makeText(DownloadService.this, "Download Success", Toast.LENGTH_SHORT).show();
        }
        
        @Override
        public void onFailed() {
            downloadTask = null;
            //下载失败时将前台服务通知关闭,并创建一个下载失败的通知
            stopForeground(true);
            getNotificationmanager().notify(1, getNotification("Download Failed", -1));
            Toast.makeText(DownloadService.this, "Download Failed", Toast.LENGTH_SHORT).show();
        }
        
        @Override
        public void onPaused() {
            downloadTask = null;
            Toast.makeText(DownloadService.this, "Pause", Toast.LENGTH_SHORT).show();
        }
        
        @Override
        public void onCanceled() {
            downloadTask = null;
            stopForeground(true);
            Toast.makeText(DownloadService.this, "Canceled", Toast.LENGTH_SHORT).show();
        }
    };
    
    private DownloadBinder mBinder = new DownloadBinder();
    
    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
    
    class DownloadBinder extends Binder {
        
        public void startDownload(String url) {
            if (downloadTask == null) {
                downloadUrl = null;
                downloadTask = new DownloadTask(listener);
            }
        }
        
        public void pauseDownload() {
            if (downloadTask != null) {
                downloadTask.pauseDownload();
            }
        }
        
        public void cancelDownload() {
            if (downloadTask != null) {
                downloadTask.cnacelDownload();
            } else {
                if (downloadUrl != null) {
                    //取消下载时需将文件删除,并将通知关闭
                    String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
                    String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
                    File file = new File(directory + fileName);
                    if (file.exists()) {
                        file.delete();
                    }
                    getNotificationManager().cancel(1);
                    stopForeground(true);
                    Toast.makeText(DownloadService.this, "Canceled", Toast.LENGTH_SHORT).show();
                }
            }
        }
    }
    
    private NotificationManager getNotificatinoManager() {
        return (NotificationMangaer) getSystemService(NOTIFICATION_SERVICE);
    }
    
    private Notification getNotificatino(String title, int progress) {
        Intent intent = new Intent(this, MainActivity.class);
        PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
        builder.setSmallIcon(R.mipmap.ic_luncher);
        builder.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher));
        builder.setContentIntent(pi);
        builder.setCotentTitle(title);
        if (progress > 0) {
            //当progress大于或等于0时才需显示下载进度
            builder.setContentText(progress + "%");
            builder.setProgress(100, progress, false);
        }
        return builder.build();
    }
}

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    
    private DownloadService.DownloadBinder downloadBinder;
    
    private ServiceConnection connection = new ServiceConnection() {
        
        @Override
        public void onServiceDisconnected(ComponentName name) {
            
        }
        
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            downloadBinder = (DownloadService.DownloadBinder) service;
        }
    };
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceSate);
        setContentView(R.layout.activity_main);
        Button startDownload = (Button) findViewById(R.id.start_download);
        Button pauseDownload = (Button) findViewById(R.id.pause_download);
        Button cancelDownload = (Button) findViewById(R.id.cancel_download);
        startDownload.setOnClickListener(this);
        pauseDownload.setOnClickListener(this);
        cancelDownload.setOnClickListener(this);
        Intent intent = new Intent(this, DownloadService.class);
        startService(intent); //启动服务
        bindeService(intent, connection, BIND_AUTO_CREATE); //绑定服务
        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(MainActivity.this, new String[]{ Manifest.permission.WRITE_EXTERNAL_STORAGE }, 1);
        }
    }
    
    @Override
    public void onClick(View v) {
        if (downloadBinder == null) {
            return;
        }
        switch (v.getId)) {
            case R.id.start_download:
                String url = "https//raw.githubusercontent.com/guolindev/eclipse/master/eclipse_inst_win64.ext";
                downloadBinder.startDownload(url);
                break;
            case R.id.pause_download:
                downloadBinder.pauseDownload();
                break
            case R.id.cancel_download:
                downloadBinder.cancelDownload();
                break
            default:
                break;
        }
    }
    
    @Override
    public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults) {
        switch (requestCode) {
            case 1:
                if(grantResult.length > 0 && grantResult[0] != PackageManager.PERMISSION_GRANTED) {
                    Toast.makeText(this, "拒绝权限将无法使用程序", Toast.LENGTH_SHORT).show();
                    finish();
                }
                break;
            default:
                break;
        }
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbinderService(connection);
    }
    
}

八、Android特色开发——基于位置的服务

使用百度定位

首先配置清单文件:

<users-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<users-permission android:name="android.permission.ACCESS_FINE_STATE"/>
<users-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<users-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<users-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<users-permission android:name="android.permission.READ_PHONE_STATE"/>
<users-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<users-permission android:name="android.permission.INTERNET"/>
<users-permission android:name="android.permission.mount_unmount_FILESYSTEMS"/>
<users-permission android:name="android.permission.WAKE_LOCK"/>

<meta-data
    android:name="com.baidu.lbsapi.API_KEY"
    android:value="i6VD2fHKM3msMfZtIOXAhFSzDiYGFIwL" />
    
<service android:name="com.baidu.location.f" android:enabled="true"
    android:process=":remote">
</service>

public class MainActivity extends AppCompatActivity {
    
    public LocationClient mLocationClient;
    
    private TextView positionText;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mLocationClient = new LocationClient(getApplicationContext());
        mLocationClient.registerLocationListener(new MyLocationListener());
        setContentView(R.layout.activity_main);
        positionText = (TextView) findViewById(R.id.position_text_view);
        List<String> permissionList = new ArrayList<>();
        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            permissionList.add(Manifest.permission.ACCESS_FINE_LOCATION);
        }
        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
            permissionList.add(Manifest.permission.READ_PHONE_STATE);
        }
        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORATE) != PackageManager.PERMISSION_GRANTED) {
            permissionList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
        }
        if (!permissionList.isEmpty()) {
            String[] permissions = permissionList.toArray(new String[permissionList.size()]);
            ActivityCompat.requestPermissions(MainActivity.this, permissions, 1);
        } else {
            requestLocation();
        }
    }

    private void requestLocation() {
        mLocationClent.start();
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        switch (requestCode) {
            case 1:
                if (grantResults.length > 0) {
                    for (int result : grantResults) {
                        if (result != PackageManager.PERMISSION_GRANTED) {
                        Toast.makeText(this, "必须同意所有权限才能使用本程序", Toast.LENGTH_SHORT).show();
                        finish();
                        return;
                        }
                    }
                    requestLocation();
                } else {
                    Toast.makeText(this, "发生未知错误", Toast.LENGTH_SHORT).show();
                    finish();
                }
                break;
            default:
                break;
        }
    }

    public class MyLocationListener implements BDLocationListener {
    
        @Override
        public void onReceiveLocation(BDLocation location) {
            StringBuilder currentPosition = new StringBuilder();
            currentPosition.append("纬度:").append(location.getLatitude()).append("\n");
            currentPostion.appen("经线:").append(location.getLongitude()).append("\n");
            currentPosition.append("定位方式:");
            if (location.getLocType() == BDLocation.TypeGpsLocation) {
                currentPosition.append("GPS");
            } else if (location.getLocType() == BDLocation.TypeNetWorkLocation) {
                currentPosition.append("网络");
            }
            positionText.setText(currentPosition);
        }
    }

}

实时更新当前的位置:

private void requestLocation() {
    initLocation();
    mLocation.start();
}

private void initLocation() {
    LocationClientOption option = new LocationClientOption();
    option.setScanSpan(5000);
    mLocationClent.setLocOption(option);
}

@Override
protected void onDestroy() {
    super.onDestroy();
    mLocationClient.stop();
}

选择定位模式

//选择当前定位模式为传感器模式
option.setLocationMode(LocationClentOption.LocationMode.Device_Sensors);

获取看得懂的位置信息

option.setIsNeedAddress(true);
//设置上行代码即可在定位监听中调用如下api
location.getCountry();
locatin.getProvince();
location.getCity();
location.getDistrict();
location.getStreet();

使用百度地图

让地图显示出来

<com.baidu.mapap.map.MapView
    android:id="@+id/bmapView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_height="match_parent"
    android:clickable="true" />
    
private MapView mapView;

//onCreate()中
SDKInitializer.initialize(getApplicationContext());
setContentView(R.layout.activity_main);
mapView = (MapView) findViewById(R.id.bmapView);

@Override
protected void onResume() {
    super.onResume();
    mapView.onResume();
}

@Override
protected void onPause() {
    super.onPause();
    mapView.onPause();
}

mapView.onDestroy();

移动到我的位置

private BaiduMap baiduMap;

private boolean isFirstLocate = true;

//onCreate()中
baiduMap = mapView.getMap;

private void navigateTo(BDLocation location) {
    if (isFirstLocate) {
        LatLng ll = new LatLng(location.getLatitude(), location.getLongitude());
        MapStatusUpdate update = MapStatusUpdateFactory.newLatLng(ll);
        baiduMap.animateMapStatus(update);
        update = MapStatusUpdateFactory.zoomTo(16f);
        baiduMap.animateMapStatus(update);
        isFirstLocate = false;
    }
}

//定位监听中
if (location.getLocType() == BDLocation.TypeGpsLocation || location.getLocType() == BDLocation.TypeNetWorkLocation) {
    navigateTo(location);
}

让“我”显示在地图上

//onCreate()中
baiduMap.setMyLocationEnabled(true);

//navigateTo()中最后
MyLocationData.Builder locationBuilder = new MyLocationData.Builder();
locationBuilder.latitude(location.getLatitude());
locationBuilder.longitude(location.getLongitude());
MyLocationData locationData = locationBuilder.build();
baiduMap.setMylocationData(locationData);

//onDestroy()中
baiduMap.setMyLocationEnabled(false);

Git时间——版本控制工具的高级用法

Git分支

//查看分支
git branch
//创建分支
git branch version1.0
//切换分支
git checkout version1.0
//合并分支
git checkout master
git merge version1.0
//删除分支
git branch -D version1.0

与远程版本库协作

//将代码下载到本地
git clone https://github.com/example/test.git
//将修改的内容同步到远程版本库上
git push origin master
//将远程版本库上的修改同步到本地
git fetch origin master
//查看远程版本修改的内容
git diff origin/master
//将origin/master分支上的修改合并到主分支上
git merge origin/master
//pull = fetch + merge
git pull origin master

九、最佳的UI体验——Material Design实战

Menu

app:showAsAction指定按钮的显示为止,使用app命名空间同样是为了能够兼容低版本的系统。

  • always表示永远显示在Toolbar中,如果屏幕空间不够则不显示。
  • ifRoom表示屏幕空间足够的情况下显示在Toolbar中,不够的话就显示在菜单栏中。
  • never则表示永远显示在菜单当中。

注意:Toolbar中的action按钮只会显示图标,菜单中的action按钮只会显示文字。

悬浮按钮和可交互提示

  • app:elevation属性给控件指定了一个高度值,高度值越大,投影范围也越大,但是投影效果越淡,高度值越小,投影范围也越小,但是投影效果越淡。
  • Snackbar的make()方法的第一个参数是用来指定Sncakbar是基于哪个View来触发的。

app:layout_scrollFlags属性各个值的作用

  • scroll表示当RecyclerView向上滚动的时候,Toolbar会跟着一起向上滚动并重新隐藏。

  • enterAlways表示当RecyclerView向下滚动的时候,Toolbar会跟着一起向下滚动并重新显示。

  • snap表示当Toolbar还没有完全隐藏或显示的时候,会根据当前滚动的距离,自动选择隐藏还是显示。

    //用来协调CoordinatorLayout的子布局的 app:layout_behavior="@string/appbar_scrolling_view_behavior"

CollapsingToolbarLayout

CollapsingToolbarLayout是不能独立存在的,它在设计的时候就被限定只能作为AppBarLayout的直接子布局来使用。

  • app:contentScrim属性用于指定CollapsingToolbarLayout在趋于折叠状态以及折叠之后的背景色。
  • exitUntilCollapsed表示当CollapsingToolbarLayout随着滚动完成后就保留在界面上,不在移出屏幕。

app:layout_collapseMode属性的各个值的作用

  • pin表示在折叠的过程中位置始终保持不变。

  • parallax表示会在折叠的过程中产生一定的错位偏移

    //将锚点设置为AppBarLayout app:layout_anchor="@id/appBar" //定位在AppBarLayout中的位置 app:layout_anchorGravity="bottom|end"

可折叠式标题栏

  • android:fitsSystemWindows属性指定成true,表示该控件会出现在系统状态栏里。(注意:xml中对应的所有父布局都应该设置这个属性。)

十、其它的高级技巧

Parcelable方式

Paracelable方式的实现原理是将一个完整的对象进行分解,而分解后的每一部分都是Intent所支持的数据类型。

public class Person implements Parcelable {
    
    private String name;
    
    private int age;
    
    ...
    
    @Override
    public int describeContents() {
        return 0;
    }
    
    @Override
    public void writeParcel(Parcel dest, int flags) {
        dest.writeString(name); 
        dest.writeInt(age);
    }
    
    public static final Parcelable.Creator<Person> CREATOR = new Parcelable.Creator<Person>() {
        
        @Override
        public Person createFromParcel(Parcel source) {
            Person person = new Person();
            person.name = source.readString();
            person.age = source.readInt();
            return person;
        }
        
        @Override
        public Person[] newArray(int size) {
            return new Person[size];
        }
    };
}

创建定时任务

Alarm机制

public class LongRunningService extends Service {
    
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
    
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                //在这里执行具体的逻辑操作
            }
        }).start();
        AlarmManager manager = (AlarmManager) getSystemService(ALARM_SERVICE);
        int anHour = 60 * 60 * 1000;
        long triggerAtTime = SystemClock.elapsedRealtime() + anHour;
        Intent i = new Intent(this, LongRunningService.class);
        PendingIntent pi = PendingIntent.getService(this, 0, i, 0);
        manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTime, pi);
        return super.onStartCommand(intent, flags, startId);
    }
}

对于manager.set()方法的参数的解释:

第一个参数为整形,指定AlarmManager的工作类型

  • ELAPSED_REALTIME表示让定时任务的触发时间从系统开机开始算起,但不会唤醒CPU。
  • ELAPSED_REALTIME_WAKEUP表示让定时任务的触发时间从系统开机开始算起,但会唤醒CPU。
  • RTC表示让定时任务的触发时间从1970年1月1日0点开始算起,但不会唤醒CPU。
  • RTC_WAKEUP表示让定时任务的触发时间从1970年1月1日0点开始算起,但会唤醒CPU。

此外:

  • SystemClock.elapsedRealtime()方法可以获取到系统开机至今所经历时间的毫秒数。
  • System.currentTimeMillis()方法可以获取到1970年1月1日0点至今所经历时间的毫秒数。

第二个参数,定时任务触发的时间,以毫秒为单位。

  • 如果第一个参数是ELAPSED_REALTIME或ELAPSED_REALTIME_WAKEUP,则这里传入开机至今的时间再加上延迟执行的时间。
  • 如果第一个参数使用的是RTC或RTC_WAKEUP,则这里传入1970年1月1日0点至今的时间再加上延迟执行的时间。

第三个参数是一个PendingIntent,一般调用getService()方法或者getBroadcast()方法来获取一个能够执行服务或广播的PendingIntent。这样当定时任务被触发的时候,服务的onStartCommand()方法或广播接收器的onReceive()方法就可以得到执行。

public class LongRunningService extends Service {
    
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
    
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                //在这里执行具体的逻辑操作
            }
        }).start();
        AlarmManager manager = (AlarmManager) getSystemService(ALARM_SERVICE);
        int anHour = 60 * 60 * 1000;
        long triggerAtTime = SystemClock.elapsedRealtime() + anHour;
        Intent i = new Intent(this, LongRunningService.class);
        PendingIntent pi = PendingIntent.getService(this, 0, i, 0);
        manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTiem, pi);
        return super.onStartCommand(intent, flags, startId);
    }
}

注意:从Android 4.4系统开始,Alarm任务的触发时间将会变得不准确,有可能会延迟一段时间后任务才能执行。这是系统在耗电方面的优化,它会自动检测目前有多少Alarm任务存在,然后将触发时间相近的几个任务放在一起执行,这就可以大幅度减少CPU被唤醒的次数,从而有效延长电池的使用时间。

使用setExact()方法替代set()方法,基本上可以保证任务准时执行。

Doze模式

当用户的设备是Android 6.0或以上系统时,如果该设备未插接电源,处于静止状态(Android 7.0删除了这一条件),且屏幕关闭了一段时间后,就会进入到Doze模式。此时系统会对CPU、网络、Alarm等活动进行限制,从而延长了电池的使用寿命。

系统不会一直处于Doze模式,会间歇性退出Doze模式一小段时间让应用去完成同步操作、Alarm任务等等。下图完整描述了Doze模式的工作过程。

image

注意:在Doze模式下,Alarm任务将会变得不准时。调用AlarmManager的setAndAllowWhileIdle()或setExactAndAllowWhileIdle()方法就能让定时任务即使在Doze模式下也能正常执行。

多窗口模式编程

多窗口模式并不会改变活动原有的生命周期,只是会将用户最近交互过的那个活动设置为运行状态,而将多窗口模式下另外一个课件的活动设置为暂停状态。

改变多窗口模式被重新创建这一默认行为,在对应的 标签中设置

android:configChanges="orientation|keyboardHidden|screenSize|screenLayout">

禁用多窗口模式

在AndroidManifest.xml的或标签中加入如下属性即可:

android:resizeableActivity=false//默认为true

注意:此属性仅在targetSdkVersion指定为24或者更高的时候才会有用。此时,设置活动不允许横竖屏切换即可禁用多窗口模式。

十一、开发库酷欧天气

比起使用MD,更简单实现背景图和状态栏融合的效果

if (Build.VERSION.SDK_INT >=21) {
    View decorView = getWindow().getDecorView();
    decorView.setSystemUiVisibility(
        View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
        | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
    getWindow().setStatusBarColor(Color.TRANSPARENT);
}

//单独为系统状态栏预留出空间
android:fitsSystemWindows="true"

十二、应用发布

使用Gradle生成签名apk

点击右侧工具栏的Gradle->项目名->:app->Tasks->build。

//生成正式版APK文件
点击clean Task,再点击assembleRelease Task。

生成多渠道APK文件

配置app下的build.gradle:

productFlavors {
    qihoo {
        applicationId "com.coolweather.android.qihoo"
    }
    baidu {
        applicationId "com.coolweather.android.baidu"
    }
}

在mian的平级目录下建立baidu目录和奇虎目录,实现各自的定制功能即可。

对未签名的加固APK文件进行手动签名

在命令行界面输入签名命令:

jarsigner -verbose -sigalg SHAlwithRSA -digestalg SHA1 -keystore [keystore文件路径] -storepass [keystore文件密码] [待签名APK路径] [keystore文件别名]

参考资料

  • 《第一行代码 第2版》