引言:2017年05月02日开始看郭霖的第一行代码第二版,在此过程中感觉有些地方需要代码实践一番。
时间:
作者:JustDo23
鼓励:Standing on Shoulders of Giants.
[TOC]
- 2003年10月,Andy Rubin 等人创办 Android 公司。
- 2005年08月,谷歌收购该公司,Andy Rubin 继续负责。
- 2008年09月,推出了 Android 系统的第一个版本。
- 2011年02月,谷歌发布了 Android 3.0 系统。
- 2011年10月,谷歌发布了 Android 4.0 系统。
- 2014年 Google I/O 大会发布了 Android 5.0 系统。
- 2015年 Google I/O 大会发布了 Android 6.0 系统。
- 2016年 Google I/O 大会发布了 Android 7.0 系统。
- Linux Kernel
- Linux 内核 提供底层驱动:显示驱动,音频驱动,照相机驱动,蓝牙驱动,Wi-Fi驱动,电源管理等。
- Libraries & Android Runtime
- 类库 通过 C/C++ 提供特性支持。SQLit 提供数据库,OpenGL|ES 提供 3D 绘图,Webkit 提供浏览器内核等。
- 运行时 提供一些核心库,允许开发使用 Java 编写 Android 应用;Dalvik 虚拟机 | ART 运行环境,使得每一个应用都能运行在独立的进程当中。
- Application Framework
- 应用程序框架层 提供 API 开发者调用开发程序。
- Applications
- 应用层 各种程序。
- 四大组件
- 活动 Activity
- 服务 Service
- 广播接收器 Broadcast Receiver
- 内容提供器 Content Provider
- 控件
- 系统控件
- 自定义控件
- SQLite 数据库
- 多媒体
- 音乐,视频,录音,拍照,闹铃
- 地理位置定位
- 其他
- gradle
- 目录下包含了 gradle wrapper 的配置文件,使用此方式不需要提前将 gradle 下载好,而是自动根据本地缓存情况决定是否需要联网下载 gradle。
- .gitignore
- 配置 Git 的忽略文件。https://github.com/GitHub/gitignore
- gradlew
- 在 Linux 系统使用,用于在命令行执行 gradle 命令。
- gradlew.bat
- 在 Windows 系统使用,用于在命令行执行 gradle 命令。
- *.iml
- iml 文件是所有 IntelliJ IDEA 项目自动生成,用于标识一个 IntelliJ IDEA 项目。不用修改。
- settings.gradle
- 指定项目中所有引入的模块。
- proguard-rules.pro
- 指定项目代码的混淆规则。
-
Gradle 是一个非常先进的项目构建工具,它使用了一种基于 Groovy 的领域特定语言(DSL)来声明项目设置,摒弃了传统基于 XML(如 Ant 和 Maven)的各种烦琐配置。
-
根目录下的 build.gradle 文件
// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript {// 构建脚本 repositories {// repositories 闭包 jcenter()// 代码托管仓库 jcenter maven } dependencies {// 闭包 dependencies 依赖 classpath 'com.android.tools.build:gradle:2.2.2'// 使用 classpath 声明了一个 Gradle 插件同时指定了插件版本号 // 因为 Gradle 并不是专门为构建 Android 项目而开发的 // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects {// 所有项目 repositories {// 指定引用的仓库 jcenter() } } task clean(type: Delete) { delete rootProject.buildDir }
-
app目录下的 build.gradle 文件
apply plugin: 'com.android.application'// 应用一个插件 // com.android.application 表示这是一个应用程序模块[可直接运行] // com.android.library 表示这是一个库模块[作为代码库] android { compileSdkVersion 25// 编译版本 buildToolsVersion "25.0.2"// 构建工具版本 defaultConfig {// 默认配置 applicationId "com.just.first"// 指定项目包名 minSdkVersion 14// 最低兼容的 Android 系统版本 targetSdkVersion 25// 目标版本 启用相应版本上的新功能新特性 versionCode 1// 项目版本号 versionName "1.0"// 项目版本名称 testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"// 测试需要 } buildTypes {// 构建类型 release {// 发布 minifyEnabled false// 是否对代码进行混淆 proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' // proguardFiles 指定混淆时使用的规则文件 // proguard-android.txt 在 Android SDK 目录下的通用混淆规则 // proguard-rules.pro 在当前项目的根目录下 自己编写特定的混淆规则 } } } dependencies {// 依赖库 [本地依赖][库依赖][远程依赖] compile fileTree(include: ['*.jar'], dir: 'libs')// 本地依赖 androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' })// Android 测试 compile 'com.android.support:appcompat-v7:25.3.1' // com.android.support 是域名部分 用于和其他公司的库做区分 // appcompat-v7 是组名称 用于和同一公司中不同的库做区分 // 25.3.1 是版本号 用户和同一库不同的版本做区分 testCompile 'junit:junit:4.12'// 测试依赖 compile 'com.android.support:recyclerview-v7:25.3.1'// 远程依赖 // Gradle 在构建项目时会首先检查一下本地是否已经有这个库的缓存,如果没有的话则会去自动联网下载,然后再添加到项目的构建路径当中。 }
- 关于Dalvik 虚拟机和ART 运行环境相关知识需要学习。
- AndroidManifest.xml 文件中指定的 package 和 build.gradle 文件中指定的 applicationId 区别。
- 创建 Activity 时候系统指定的模板及自定义模板的学习使用。
- 代码混淆相关的知识需要重新学习总结。
- 资源文件夹 mipmap 与 drawable 区别。
- 打印日志是否会影响性能和效率?
-
创建菜单文件
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/menu_add" android:title="Add" /> <item android:id="@+id/menu_remove" android:title="Remove" /> </menu>
-
在 Activity 中进行添加和处理
/** * 创建自定义菜单 * * @param menu 系统指定的菜单对象 * @return true, 表示允许创建的菜单显示出来 */ @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu, menu);// 从资源中加载菜单 return true;// 允许菜单显示 } /** * 菜单 Item 的点击事件 * * @param item 菜单 Item * @return */ @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_add: ToastUtil.showShortToast(this, "Add"); break; case R.id.menu_remove: ToastUtil.showShortToast(this, "Remove"); break; } return true; }
-
显式 Intent
-
隐式 Intent
- action 每个 Intent 中只能指定一个 action
- category 每个 Intent 中可以指定多个 category
- data
-
data 详解
- android:scheme 用于指定数据的协议部分。
- android:host 用于指定数据的主机名部分。
- android:port 用于指定数据的端口部分。
- android:path 用于指定主机名和端口之后部分。
- android:mimeType 用于指定可处理的数据类型,允许使用通配符的方式进行指定。
-
注意:
android.intent.categore.DEFAULT
是一种默认的 category 在调用startActivity()
方法的时候会自动将这个 category 添加到 Intent 当中。- 只有
<action>
和<category>
中的内容同时能够匹配上 Intent 中指定的 action 和 category 时,活动才能响应该 Intent。 - 只有
<data>
标签中指定的内容和 Intent 中携带的 Data 完全一致是,当前活动才能够响应该 Intent。
-
启动浏览器
Intent browserIntent = new Intent(Intent.ACTION_VIEW); browserIntent.setData(Uri.parse("https://www.baidu.com")); startActivity(browserIntent);
-
启动拨号
Intent dialIntent = new Intent(Intent.ACTION_DIAL); dialIntent.setData(Uri.parse("tel:10086")); startActivity(dialIntent);
-
返回栈
其实 Android 是使用任务(Task)来管理活动的,一个任务就是一组存放在栈里的活动的集合,这个栈也被称作返回栈(Back Stack)。栈是一种后进先出的数据接口。
-
活动状态
- 运行状态
- 栈顶,可见,可交互
- 暂停状态
- 栈中,可见,不可交互
- 停止状态
- 栈中,不可见,不可交互
- 销毁状态
- 出栈
- 运行状态
-
生命周期函数
- onCreat()
- 初始化操作,加载布局,绑定事件等。
- onStart()
- 活动由不可见变为了可见。
- onResume()
- 活动准备好和用户进行交互。
- onPause()
- 这个方法在系统准备去启动或者恢复另一个活动的时候调用。我们通常会在这个方法中将一些消耗 CPU 的资源释放掉,以及保存一些关键数据,但这个方法的执行速度一定要快,不然会影响到新的栈顶活动的使用。
- onStop()
- 活动完全不可见。如果启动的新活动是一个对话框式的活动,那么
onPause()
执行onStop()
不执行。
- 活动完全不可见。如果启动的新活动是一个对话框式的活动,那么
- onDestroy()
- 活动被销毁之前调用。
- onRestart()
- 活动由停止状态变为运行状态之前调用。
- onCreat()
-
活动的生存期
- 完整生存期
- 可见生存期
- 前台生存期
-
场景:应用中有一个活动 A,用户在活动 A 的基础上启动了活动 B,活动 A 就进入了停止状态,这个时候由于系统内存不足,将活动 A 回收掉了,然后用户按下 Back 键返回活动 A,会出现什么情况?会正常显示活动 A 但不会执行活动 A 的 onRestart() 方法,而是会执行 onCreate() 方法将活动 A 重新创建一次。
问题:打个比方,活动 A 中有一个文本输入框,用户输入一段文字,然后启动了活动 B,这时候活动 A 被回收了,按下 Back 键返回活动 A,却发现输入的�文字全部都没了。数据怎么存储?
-
生命周期回调:
// 启动活动 A JustDo23: RecoveryActivity ---> onCreate() JustDo23: RecoveryActivity ---> onStart() JustDo23: RecoveryActivity ---> onResume() // 在活动 A 的基础上启动活动 B JustDo23: RecoveryActivity ---> onPause() JustDo23: RecoveryActivity ---> onSaveInstanceState() JustDo23: RecoveryActivity ---> onStop() JustDo23: RecoveryActivity ---> onDestroy() // 按下 Back 键返回活动 A JustDo23: RecoveryActivity ---> onCreate() JustDo23: RecoveryActivity ---> tempData = Something you just typed JustDo23: RecoveryActivity ---> onStart() JustDo23: RecoveryActivity ---> onResume()
-
解决方法:Activity 中提供了一个 onSaveInstanceState() 方法,可以保证活动被回收之前一定会被调用。在活动的 onCreate() 方法中有一个对应的 Bundle 类型参数 saveInstanceState 从中获取保存的数据。
-
实现代码:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_recovery); if (savedInstanceState != null) {// 取出保存的数据 String tempData = savedInstanceState.getString("data_key"); LogUtils.e(simpleName + " ---> " + "tempData = " + tempData); } } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState);// 在销毁前进行的数据保存 String tempData = "Something you just typed"; outState.putString("data_key", tempData); }
-
测试方法
第一步,打开开发者选项;第二步,勾选不保留活动用户离开后即销毁每个活动。
-
测试发现
在测试中使用 EditText 测试 Activity 被系统回收,但发现返回之后 Activity 和 EditText 的确都是被重新创建了,但是 EditText 中输入的内容却仍然存在。其实是 View 有类似的保存数据的效果。
-
其他
- 四种启动模式
- standard
- singleTop
- singleTask
- singleInstance
- 指定为 singleInstance 模式的活动会启用一个新的返回栈来管理这个活动(其实如果 singTask 模式指定了不同的 taskAffinity,也会启动一个新的返回栈)。程序中有一个活动是允许其他程序调用的,则使用此模式。其他三种不能实现是因为每个应用都会有自己的返回栈,同一个活动在不同的返回栈中入栈时必须是创建了新的实例。
-
获取 Activity 的 Task id
this.getTaskId();
-
获取 Activity 的名字
this.getClass().getSimpleName();
-
杀死当前进程
android.os.Process.killProcess(android.os.Process.myPid());// 删掉当前进程
-
活动管理集合
public class ActivityCollector { public static List<Activity> activityList = new ArrayList<>(); public static void addActivity(Activity activity) { activityList.add(activity); } public static void removeActivity(Activity activity) { activityList.remove(activity); } public static void finishAll() { for (Activity activity : activityList) { if (!activity.isFinishing()) { activity.finish(); } } activityList.clear(); android.os.Process.killProcess(android.os.Process.myPid());// 删掉当前进程 } }
-
启动活动
/** * 其他活动启动当前获取 * * @param context 上下文 * @param data1 传递数据 * @param data2 传递数据 */ public static void actionStart(Context context, String data1, String data2) { Intent intent = new Intent(context, StartActivity.class); intent.putExtra("param1", data1); intent.putExtra("param2", data2); context.startActivity(intent); }
- 关于向下兼容的 AppCompatActivity 需要学习。
- 关于栈和堆的相关知识需要学习总结。
- 弹出 Dialog 或者 PopupWindow 并不影响 Activity 的生命周期。
- 在开发者选项中的各个功能的使用方式。
- 和启动模式相关的有一个 onNewIntent(Intent intent) 方法需要注意。
- Activity 的 taskAffinity 属性。
- 随时随地退出程序。
- 启动活动的最佳写法。
- 退出程序的相关问题,如何真正退出程序,杀掉进程,最优做法是什么?
这里主要记录一下,在布局文件里面给 Button 设置的文字是**"Button",但是最终的显示结果却是"BUTTON"**,小写变大写。这是由于系统会对 Button 中的所有英文字母自动进行大写转换,使用以下代码禁止这一默认特性:
android:textAllCaps="false"
其次,给按钮设置点击事件应该有四种方式。
通过 android:visibility 进行指定
- visible 可见。
- invisible 不可见,但仍然占据着原来的位置和大小,变成透明状态了。
- gone 不可见,而且不再占用任何屏幕空间。
-
LinearLayout 线性布局
- LinearLayout 的默认方向是
horizontal
android:layout_weight
属性的计算方法,对剩余空间按比例分配android:layout_gravity="center"
与android:gravity="center"
两者之间的区别
- LinearLayout 的默认方向是
-
RelativeLayout 相对布局
android:layout_centerInParent="true"
android:layout_alignParentLeft="true"
android:layout_above="@id/bt_center"
android:layout_below="@id/bt_center"
android:layout_toLeftOf="@id/bt_center"
android:layout_alignLeft="@id/bt_center"
android:layout_alignBottom="@id/bt_center"
android:layout_alignBaseline="@id/bt_center"
-
FrameLayout 帧布局
- 一层一层的覆盖
-
PercentRelativeLayout 和 PercentFrameLayout 百分比布局
-
由于
LinearLayout
本身已经支持按比例指定控件的大小,因此百分比布局只为RelativeLayout
和FrameLayout
进行了功能扩展。 -
不再使用
wrap_content
和match_parent
而是直接指定百分比。<android.support.percent.PercentRelativeLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"> <Button android:id="@+id/bt_center" android:layout_centerInParent="true" android:text="Center" android:textAllCaps="false" app:layout_heightPercent="20%" app:layout_marginPercent="5%" app:layout_widthPercent="20%" /> </android.support.percent.PercentRelativeLayout>
-
- 所有控件都是直接或者间接继承自 View 的。
- 所有布局都是直接或者间接继承自 ViewGroup 的。
- View 是 Android 中最基本的一种 UI 组件,它可以在屏幕上绘制一块矩形区域,并能响应这块区域的各种事件。
- ViewGroup 是一种特殊的 View,它可以包含很多子 View 和子 ViewGroup,是一个用于放置控件和布局的容器。
- 引入布局,使用
<include>
标签引入一个已经写好的布局。 - 注意: 获取上下文使用
getContext()
方法。 - 注意: 加载布局使用
LayoutInflater.from(context).inflate(R.layout.inclue_title, this);
注意 第二个参数 - 最简单的自定义控件,查看代码即可。
- 适配器有多种,简单的使用
ArrayAdapter
。 - 掌握使用
ViewHolder
进行复用来提升运行效率。 - 点击事件的基本使用。
-
注意在使用的时候需要先设置
布局管理器
。 -
为什么 ListView 很难或者根本无法实现的效果在 RecyclerView 上这么轻松就能实现?这主要得益于 RecyclerView 出色的设计。ListView 的布局排列是由自身去管理的,而 RecyclerView 则将这个工作交给了 LayoutManager,LayoutManager 中制定了一套可扩展的布局排列接口,子类只要按照接口的规范来实现,就能定制出各种不同排列方式的布局了。
-
RecyclerView 并没有提供像样的点击事件,其实,ListView 的在点击事件上的处理并不人性化,
setOnItemCLickListener()
方法注册的是子项的点击事件,但如果想点击的是子项里具体的某一个按钮呢?虽然 ListView 也能做到,但是实现起来就相对比较麻烦了。为此,RecyclerView 干脆直接摒弃了子项点击事件的监听,所有的点击事件都由具体的 View 去注册,就再没有这个困扰了。int position = viewHolder.getAdapterPosition();
- 点九图片
Nine-Patch
的相关知识和使用。- 在上边框和左边框绘制的部分表示当图片需要拉伸时就拉伸黑点标记的区域。
- 在下边框和右边框绘制的部分表示内容会被放置的区域。
- 使用鼠标在图片的边缘拖动就可以进行绘制。
- 按住
shift
键拖动可以进行擦除。
RecycleView
数据更新。
-
屏幕适配相关知识。慕课网 Android-屏幕适配全攻略。
-
Android Studio 中的 drawable-xhdpi 和 mipmap-xhdpi 文件夹的区别及使用。
-
EditText 使用时候软键盘的弹起和收起监听。
-
ImageView 使用时候的有三个属性需要注意
android:src="@mipmap/ic_launcher" android:background="@mipmap/ic_launcher" android:scaleType="centerCrop"
-
ProgressBar 如何修改颜色?
-
百分比布局需要更多的学习和使用。
-
关于
RecycleView
的源码分析可以简单的学习一下。 -
在 Android sdk 目录下有一个 tools 文件夹,其中的工具可以学习使用一下。
-
关于
LayoutInflater
布局填充的一些方法需要留意。
- 碎片是可以嵌入在活动当中的 UI 片段,轻量迷你的活动。
- 使用 support-v4 库中的
Fragment
作为基类。在 Android 4.2 系统中才开始支持在Fragment
中嵌套使用Fragment
。 - 在
build.gradle
文件中添加了 appcompat-v7 库的依赖,这个库会将 support-v4 库也一起引入进来。
-
继承并添加布局
/** * 继承 Fragment 并填充布局 * * @author JustDo23 */ public class LeftFragment extends Fragment { @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {// 简单加载布局 View rootView = inflater.inflate(R.layout.fragment_simple_left, container, false); return rootView; } }
-
在布局文件中使用
Fragmet
<fragment android:id="@+id/frag_left" android:name="com.just.first.chapter04.LeftFragment" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" />
代码关键点
- 标签使用
<fragment />
- 属性使用
android:name="all_name"
并制定全部路径名称 - 属性
android:id="frag_id"
是必须要有的否则会崩溃
- 标签使用
-
布局文件
<FrameLayout android:id="@+id/fl_bottom" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:background="@android:color/holo_orange_dark" />
-
碎步切换代码
/** * 切换 Fragment 操作 * * @param fragment 新的碎片 */ private void replaceFragment(Fragment fragment) { FragmentManager supportFragmentManager = this.getSupportFragmentManager();// 获取碎步管理类 FragmentTransaction fragmentTransaction = supportFragmentManager.beginTransaction();// 获取碎片管理事务 fragmentTransaction.replace(R.id.fl_bottom, fragment);// 进行替换 fragmentTransaction.commit();// 事务提交 }
-
动态加载碎片5个步骤
- 创建待添加的碎步实例
- 通过
this.getSupportFragmentManager()
方法获取FragmentManager
实例 - 通过
fragmentManager.beginTransaction()
方法开启一个事务
- 使用
fragmentTransaction.replace()
指定位置及碎片进行动态添加 - 最后通过
fragmentTransaction.commit()
方法提交事务以完成动态添加
-
碎片模拟返回栈
添加完碎片后按下返回键就直接退出了。在
commit()
方法之前执行以下代码来模拟返回栈。fragmentTransaction.addToBackStack(null);// 字符串参数用于描述返回栈状态
-
在 Activity 中通过
FragmentManager
获取LeftFragment leftFragment = (LeftFragment) getSupportFragmentManager().findFragmentById(R.id.frag_left);
-
在 Fragment 中直接调用
getActivity();
方法FragmentActivity activity = this.getActivity();
- 生命周期函数
- onAttach()
- 当 Fragment 与 Activity 建立关联时候调用
- onCreate()
- onCreateView()
- 为 Fragment 加载布局
- onActivityCreated()
- 确保与 Fragment 相关联的 Activity 一定已经创建完毕的时候调用
- onStart()
- onResume()
- onPause()
- onStop()
- onDestroyView()
- 当与 Fragment 相关联的视图被移除的时候调用
- onDestroy()
- onDetach()
- 当 Fragment 与 Activity 解除关联的时候调用
- onAttach()
- 静态加载时 Activity 及 Fragment 生命周期
- 动态加载时 Activity 及 Fragment 生命周期
- 调用
addToBackStack
方法对 Fragment 生命周期的影响
-
使用限定符
- 在
res
目录下创建与layout
目录平级的文件夹layout-large
- 在
layout
目录和
layout-large
目录下创建同名的布局文件 - 两个布局文件虽然同名但是布局内容不同
- 分别在手机和平板上运行才能看到实现的效果
其中
large
就是一个**限定符
**,程序运行在类似平板这种large
屏幕的设备上会自动加载layout-large
目录下的布局。 - 在
-
使用最小宽度限定符
- 在
res
目录下创建layout-sw600dp
目录
最小宽度限定符 Smallest-width Qualifier 允许我们对屏幕指定一个最小值,以 dp 为单位,然后以这个最小值为临界点,屏幕宽度大于这个值的设备就加载一个布局,屏幕宽度小于这个值的设备就加载另一个布局。
- 在
- 关于
Activity
与Fragment
之间交互的总结。 - 熟练
Fragment
的各个生命状态及生命周期。 - 注意
Activity
+Fragment
详细的生命周期。 - 经常开发手机版应用可以接触一下
平板
开发。
- 在一个 IP 网络范围中,最大的 IP 地址是被保留作为广播地址来使用的。比如某个网络的 IP 范围是
192.168.0.xxxx
子网掩码是255.255.255.0
那么这个网络的广播地址就是192.168.0.255
了。广播数据包会被发送到同一网络上的所有端口,这样在该网络中的每台主机都将会收到这条广播。 - 在 Android 中每个应用可以对自己感兴趣的广播进行注册,这样该程序就只会接收到自己关心的广播。应用可以自由的发送和接收广播。
- 标准广播
- 一种完全异步执行的广播。广播发出后,所有接收器几乎同时接收到广播消息,因此没有先后顺序,效率高,无法被截断。
- 有序广播
- 一种同步执行的广播。广播发出后,同一时刻只有一个接收器接收到广播消息,这个接收器执行完毕广播才会继续传递,因此有先后顺序,优先级高的接收器先收到,并且前面的可以截断正在传递的广播,这样后面的就无法收到广播消息了。
- 动态注册
- 需要使用
IntentFilter
类指定相应的action
注册
与解注册
成对出现,在onCreate
中进行注册在onDestroy
中进行解注册
- 需要使用
- 静态注册
- 需要在功能清单中使用
intent-filter
及action
标签指定 - 注册之后为系统
全局
的广播接收器 - 程序进程
运行中
则可以接收
,程序进程完成退出
则无法接收
- 需要在功能清单中使用
-
动态注册-监听网络变化
-
继承 BroadcastReceiver
/** * 动态广播监听网络变化 * * @author JustDo23 */ public class NetworkChangeReceive extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { ToastUtil.showShortToast(context, "Network changes."); ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); if (networkInfo != null && networkInfo.isAvailable()) { ToastUtil.showShortToast(context, "Network is available."); } else { ToastUtil.showShortToast(context, "Network is unavailable."); } } }
-
在 Activity 中注册与解注册
/** * 动态广播监听网络变化 * * @author JustDo23 */ public class NetworkChangeActivity extends BaseActivity { private IntentFilter intentFilter; private NetworkChangeReceive networkChangeReceive; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_network_change); register();// 注册广播 } @Override protected void onDestroy() { super.onDestroy(); unRegister();// 解注册广播 } /** * 注册广播 */ private void register() { intentFilter = new IntentFilter(); intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE"); networkChangeReceive = new NetworkChangeReceive(); this.registerReceiver(networkChangeReceive, intentFilter); } /** * 解注册广播 */ private void unRegister() { unregisterReceiver(networkChangeReceive); } }
-
添加权限
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
-
-
静态注册-监听手机开机
-
使用 Android Studio 创建广播接收器
右击
选择New
选择Other
选择Broadcast Receiver
进行命名- 此操作与手动创建一致
-
功能清单新增
<receiver android:name=".chapter05.CustomReceiver" android:enabled="true" android:exported="true"> </receiver>
enabled
属性表示是否启用这个广播接收器exported
属性表示是否允许这个广播接收器接收本程序以外的广播
-
添加
<intent-filter>
标签并指定<action>
标签信息<receiver android:name=".chapter05.CustomReceiver" android:enabled="true" android:exported="true"> <intent-filter android:priority="100"> <action android:name="com.just.first.CUSTOM" /> </intent-filter> </receiver>
priority
指定了广播的优先级
-
添加权限
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
-
-
发送标准广播
sendBroadcast(new Intent("com.just.first.CUSTOM"));// Intent 指定 action
-
发送有序广播
sendOrderedBroadcast(new Intent("com.just.first.CUSTOM"), null);// 第二参数是一个与权限相关的字符串
- 发送有序广播需要指定
优先级
,可以在功能清单中使用priority
指定优先级 - 前面的广播可以截断广播的传递,在
onReceive()
方法中调用abortBroadcast()
方法截断广播
- 发送有序广播需要指定
前面的广播都属于系统全局广播,即发出的广播可以被其他任何程序接收到,并且程序也可以接收来自其他任何应用的广播。这样存在安全问题。Android 引入了一套本地广播机制,使用本地广播机制广播只能在本应用内部进行发送和接收。主要是使用了一个 LocalBroadcastManager
来对广播进行管理。
-
本地广播注册
private LocalBroadcastReceiver localBroadcastReceiver; private LocalBroadcastManager localBroadcastManager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_local_broadcast); localBroadcastManager = LocalBroadcastManager.getInstance(this);// 获取实例 IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction("com.just.first.LOCAL"); localBroadcastReceiver = new LocalBroadcastReceiver();// 实例化接收器 localBroadcastManager.registerReceiver(localBroadcastReceiver, intentFilter);// 注册本地广播 }
-
本地广播解注册
@Override protected void onDestroy() { super.onDestroy(); localBroadcastManager.unregisterReceiver(localBroadcastReceiver); }
-
本地广播发送
public void sendLocalBroadcast(View view) { Intent intent = new Intent("com.just.first.LOCAL"); localBroadcastManager.sendBroadcast(intent);// 发送本地广播 }
-
重要提示
- 本地广播是无法通过静态注册的方式来接收的。
- 静态注册主要就是为了让程序在未启动的情况下也能接收到广播。
-
本地广播优势
- 可以明确知道正在发送的广播不会离开我们的程序,不必担心机密数据泄露。
- 其他程序无法将广播发送到我们程序的内部,不必担心有安全漏洞隐患。
- 发送本地广播比发送系统全局广播更加高效。
-
注意:开发时不能在
onReceive()
方法中添加过多逻辑或者进行任何的耗时操作,因为在广播接收器中不允许开启线程的,当onReceive()
方法运行较长时间而没有结束,程序就会出现 ANR 错误。 -
广播是一种可以跨进程的
通信
方式,例如我们可以接收系统广播。 -
在静态广播接收器及本地广播接收器中是没有办法弹出对话框的。
android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
-
在动态广播接收器中是可以弹出对话框的。
-
当在
BaseActivity
中注册广播从而使得所有子类都动态注册该广播的操作中,需要将广播的注册
与解注册
分别放在生命周期的onResume()
与onPause()
中。这样可以保障只有处于栈顶的活动才能接收到广播信息。
-
Git 是一个开源的分布式
版本控制
工具。 -
配置身份
$ git config --global user.name "JustDo23" $ git config --global user.email "JustDo_23@163.com"
-
创建代码仓库
$ git init
-
添加与提交
$ git add . $ git commit -m "describe for commit"
- 广播的生命周期简单了解。
- 进一步了解广播的工作原理。
- 瞬时数据就是指那些存储在内存当中,有可能会因为程序关闭或其他原因导致内存被回收而丢失的数据。
- 持久化数据就是指将那些内存中的瞬时数据保存到存储设备中,保证即使在手机或电脑关机的情况下,这些数据仍然不会丢失。
-
文件存储不对存储的内容进行任何的格式化处理,所有数据都是原封不动地保存到文件当中。因而比较适合用于存储一些简单的文本数据或二进制数据。
-
通过
Context
提供的openFileOutput()
方法将文件存入内部存储路径中。操作模式Context.MODE_PRIVATE
默认模式私有且覆盖Context.MODE_APPEND
模式每次写入数据进行追加
-
通过
Context
提供的openFileInput()
方法用于从文件中进行数据读取。 -
存数据
/** * 保存数据到内部存储文件 * * @param fileName 文件名称 * @param saveData 写入的数据 */ private void saveToFile(String fileName, String saveData) { FileOutputStream fileOutputStream = null; BufferedWriter bufferedWriter = null; try { fileOutputStream = this.openFileOutput(fileName, Context.MODE_APPEND);// [/data/data/com.just.first/files] bufferedWriter = new BufferedWriter(new OutputStreamWriter(fileOutputStream)); bufferedWriter.write(saveData); } catch (IOException e) { e.printStackTrace(); } finally { try { if (bufferedWriter != null) { bufferedWriter.close(); } if (fileOutputStream != null) { fileOutputStream.close(); } } catch (IOException e) { e.printStackTrace(); } } }
-
取数据
/** * 从内部存储文件中读取数据 * * @param fileName 文件名称 * @return 文件内容 */ private String loadFromFile(String fileName) { FileInputStream fileInputStream = null; BufferedReader bufferedReader = null; StringBuilder dataContent = new StringBuilder(); try { fileInputStream = this.openFileInput(fileName); bufferedReader = new BufferedReader(new InputStreamReader(fileInputStream)); String line = ""; while ((line = bufferedReader.readLine()) != null) { dataContent.append(line); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (bufferedReader != null) { bufferedReader.close(); } if (fileInputStream != null) { fileInputStream.close(); } } catch (IOException e) { e.printStackTrace(); } } return dataContent.toString(); }
-
SharedPreferences
使用键值对的方式存储数据。支持**多种
不同的数据类型
**存储。 -
文件存储路径
/data/data/主包名/shared_prefs
-
文件存储的是
xml
文件。 -
获取
sharedPreferences
实例的三种方法- 通过
Context
类的getSharedPreferences(String name, int mode)
方法获取。- 第一个参数文件名称
- 第二个参数模式
- 通过
Activity
类的getPreferences(int mode)
方法获取。- 一个参数模式
- 文件名称会自动获取当前活动类名
getLocalClassName()
- 通过
PreferenceManager
类的getDefaultSharedPreferences(Context context)
方法获取。- 一个参数上下文
- 文件名称会自动获取当前程序主包名
context.getPackageName() + "_preferences"
- 通过
-
存储数据需要三个步骤
- 获取
SharedPreferences.Editor
对象 - 通过
Editor
对象进行数据的添加 - 调用
Editor
的apply()
方法进行数据的提交
- 获取
-
存数据
/** * 将数据保存到 SharedPreferences * * @param fileName 文件名 * @param keyWord 键 * @param saveData 值 */ private void saveToSharedPreferences(String fileName, String keyWord, String saveData) { SharedPreferences sharedPreferences = this.getSharedPreferences(fileName, MODE_APPEND); SharedPreferences.Editor edit = sharedPreferences.edit(); edit.putString(keyWord, saveData); edit.apply(); }
-
取数据
/** * 从 SharedPreferences 加载数据 * * @param fileName 文件名 * @param keyWord 键 * @return 值 */ private String loadFromSharedPreferences(String fileName, String keyWord) { SharedPreferences sharedPreferences = this.getSharedPreferences(fileName, MODE_APPEND); return sharedPreferences.getString(keyWord, null);// 默认值 }
-
数据的写入和读取均可以根据类型进行操作。
-
SQLite
数据库是一款轻量级的关系型数据库,运算速度快,占用资源少。支持标准 SQL 语法,遵循数据库的 ACID 事务。 -
自定义
SQLiteOpenHelper
继承系统SQLiteOpenHelper
/** * SQLiteOpenHelper 实现数据库创建与升级 * * @author JustDo23 */ public class BookOpenHelper extends SQLiteOpenHelper { /** * 构造方法[必须实现] * * @param context 上下文 * @param name 数据库名称[带上后缀 .db] * @param factory 工厂[允许数据查询使用自定义 Cursor][一般传 null] * @param version 版本[整型] */ public BookOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) { super(context, "BookStore" + ".db", null, 1); } /** * 创建数据表方法[必须实现] * * @param db 数据库操作对象 */ @Override public void onCreate(SQLiteDatabase db) { String sql = "create table Book ( " + "id integer primary key autoincrement" + ", " + "author text" + ", " + "price real" + ", " + "pages integer" + ", " + "name text" + ")"; db.execSQL(sql);// 执行 SQL 语句 } /** * 数据库升级方法[必须实现] * * @param db 数据库操作对象 * @param oldVersion 旧的版本号 * @param newVersion 新的版本号 */ @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { } }
-
数据库文件存储路径
/data/data/主包名/databases
-
注意:
SQL 语句中相应位置的空格及分割符很重要 -
数据类型
integer
表示整型real
表示浮点型text
表示文本类型blob
表示二进制类型primary key
表示主键autoincrement
表示自增长
-
数据库的创建
- 继承系统
SQLiteOpenHelper
并实现相应方法后并没有实现数据库的创建 - 在
SQLiteOpenHelper
中有两个重要的方法getReadableDatabase()
获取读数据操作的对象getWritableDatabase()
获取写数据操作的对象- 这两个方法可以创建或打开一个数据库。数据库存在则直接打开,数据库不存在则创建并打开。
- 这两个方法返回的对象都可以对数据库进行读写操作。
- 当数据不可写入时候如磁盘空间已满,
getReadableDatabase()
方法将以只读方式打开数据库,getWritableDatabase()
方法会抛出异常。
- 继承系统
-
ADB 调试工具
-
进入 Shell 内核
$ adb shell
-
打开数据库
$ sqlite3 BookStore.db
-
查看数据库中的数据表
$ .table
- 数据表
android_metadata
是每个数据库自动生成的
- 数据表
-
查看建表语句
$ .schema
-
退出
$ .exit $ .quit
-
-
升级数据库
/** * 数据库升级方法 */ @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL("drop table if exists Book");// 删除原来的表 onCreate(db);// 重新进行创建 }
- 先将已经存在的数据表进行删除,然后创建新的表。数据表不能重复创建,否则会崩溃。
- 修改构造方法中的数据库版本号。
-
概述
数据库操作有 4 种简称
CRUD
C
代表Create
添加insert
R
代表Retrieve
查询select
U
代表Update
更新update
D
代表Delete
删除delete
-
添加数据
-
利用
SQLiteDatabase
对象的insert(String table, String nullColumnHack, ContentValues values)
方法进行数据数据添加 -
第一个参数表名
-
第二个参数用于在未指定添加数据的情况下给某些可为空的列自动赋值 NULL 一般传入 null 即可
-
第三个参数数据集合键值对关系键为列名因此值为数据
public void insert() { SQLiteDatabase writableDatabase = bookOpenHelper.getWritableDatabase();// 获取数据操作对象 ContentValues contentValues = new ContentValues();// 键值对集合 contentValues.put("name", "FirstLine");// 列名-数据 contentValues.put("author", "Guo"); contentValues.put("pages", "570"); contentValues.put("price", 79.0); writableDatabase.insert("Book", null, contentValues);// 指定表名添加 }
-
数据库的查询语句
$ select * from Book;
-
-
更新数据
-
利用
SQLiteDatabase
对象的update(String table, ContentValues values, String whereClause, String[] whereArgs)
方法进行数据数据更新 -
后两个参数用于约束更新某一行或者某几行的数据,不指定默认更新所有行。
-
第三个参数对应 SQL 语句的 where 部分其中
?
代表占位符 -
第四个参数按照先后顺序对应为占位符进行赋值
public void update() { SQLiteDatabase writableDatabase = bookOpenHelper.getWritableDatabase();// 获取数据操作对象 ContentValues contentValues = new ContentValues();// 键值对集合 contentValues.put("price", 99.9); writableDatabase.update("Book", contentValues, "name = ?", new String[]{"FirstLine"}); }
-
-
删除数据
-
利用
SQLiteDatabase
对象的delete(String table, String whereClause, String[] whereArgs)
方法进行数据数据删除public void delete() { SQLiteDatabase writableDatabase = bookOpenHelper.getWritableDatabase();// 获取数据操作对象 writableDatabase.delete("Book", "name = ?", new String[]{"FirstLine"}); }
-
-
查询数据
-
SQL
的全称是Structured Query Language
即结构化查询语言
-
利用
SQLiteDatabase
对象的query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy)
方法进行数据数据查询 -
query()
重载函数比较多功能与 SQL 语句中的查询类似query方法参数 对应 SQL 部分 描述 table from table_name 指定查询的表名 columns select column1, column2 指定查询的列名 selection where column = value 指定 where 的约束条件 selectionArgs - 为 where 中的占位符提供具体的值 groupBy group by column 指定需要 group by 的列 having having column = value 对 group by 后的结果进一步约束 orderBy order by column1, column2 指定查询结果的排序方式 -
不需要的参数可以指定为 null
public void query() { SQLiteDatabase readableDatabase = bookOpenHelper.getReadableDatabase();// 获取数据操作对象 Cursor cursor = readableDatabase.query("Book", null, null, null, null, null, null);// 查询获得游标 if (cursor.moveToFirst()) {// 是否可以移动位置 do { String author = cursor.getString(cursor.getColumnIndex("author")); String name = cursor.getString(cursor.getColumnIndex("name")); String pages = cursor.getString(cursor.getColumnIndex("pages")); String price = cursor.getString(cursor.getColumnIndex("price")); LogUtils.e("Book: " + author + " -- " + name + " -- " + pages + " -- " + price); } while (cursor.moveToNext());// 是否可以继续往下移动 } }
-
-
使用 SQL 语句
-
添加数据
writableDatabase.execSQL("insert into Book (name, author, pages, price) values (?, ?, ?, ?)", new String[]{"SecondLine", "Lin", "123", "66.6"});writableDatabase.execSQL("insert into Book (name, author, pages, price) values (?, ?, ?, ?)", new String[]{"SecondLine", "Lin", "123", "66.6"});
-
更新数据
writableDatabase.execSQL("update Book set pages = ? where author = ?", new String[]{"333", "Lin"});writableDatabase.execSQL("update Book set pages = ? where author = ?", new String[]{"333", "Lin"});
-
删除数据
writableDatabase.execSQL("delete from Book where pages > ?", new String[]{"10"});
-
查询数据
readableDatabase.rawQuery("select * from Book", null);// 查询获得游标
-
-
LitePal 采用了对象关系映射 ORM 的模式。简单说,我们使用的编程语言是面向对象语言,而使用的数据库则是关系型数据库,那么将面向对象的语言和面向关系的数据库之间建立一种映射关系,这就是对象关系映射了。因此,可以用面向对象的思维来操作数据库,而不用再和 SQL 语句打交道。
-
使用步骤
- 添加依赖
- 创建
assets
文件夹 - 创建
litepal.xml
配置文件 - 在
Application
中进行初始化 - 创建实体类也就是表结构
- 配置
litepal.xml
文件
-
创建数据库
LitePal.getDatabase();// 使用 LitePal 创建数据库
-
添加数据
-
实体类需要要继承
DataSupport
类 -
直接调用实体类的
save()
方法Book book = new Book();// 实例化实体类 book.save();// 使用 LitePal 插入数据
-
-
更新数据
-
通过对已存储的对象重新设值后重新调用
save()
方法来更新。 -
调用
model.isSaved()
方法返回true
则表示已存储的对象。一种是调用过save()
方法的对象,一种是通过LitePal
的查询 API
得到的对象。Book book = new Book();// 实例化实体类 book.save();// 使用 LitePal 插入数据 book.setPages("324");// 更新数据 book.save();// 对插入的数据进行更新
-
通过任意对象设置需要更新的值后调用
updateAll(String... conditions)
方法来更新 -
第一个参数可以指定条件约束,不指定代表更新所有
-
**
注意:
**将某个字段设置为默认值需要调用setToDefault(String fieldName)
参数字段名Book book = new Book();// 实例化实体类 book.setPages("776");// 更新数据 book.setToDefault("price");// 设置默认值 book.updateAll("name = ? and pages = ?", "老人与海", "76");
-
-
删除数据
-
通过调用已存储的对象的
delete()
方法来删除 -
直接使用
DataSupport.deleteAll()
传递参数进行删除,传递表名及约束,不传则删除所有DataSupport.deleteAll(Book.class, "pages < ?", "400");// 指定表名及约束进行删除
-
-
查询数据
-
直接使用
DataSupport
类中的相关方法进行查询 -
查询所有
DataSupport.findAll(Book.class);// 查询所有 DataSupport.findFirst(Book.class);// 查询第一条 DataSupport.findLast(Book.class);// 查询最后一条
-
更多查询功能
DataSupport.select("name", "author", "pages")// 指定查询的列 .where("pages > ?", "400")// 指定查询的约束条件 .order("pages desc")// 指定查询结果排序 .limit(10)// 指定查询结果数量 .offset(2)// 指定查询结果偏移-抛弃前2条 .find(Book.class);// 指定查询的表名
select()
方法用于指定查询哪几列的数据where()
方法用于指定查询的约束条件order()
方法用于指定查询结果的排序方式 另desc
表示降序asc
表示升序limit()
方法用于指定查询结果的数量offset()
方法用于指定查询结果偏移量
-
- 文件存储核心是
Java
中的I/O 流操作
因此需要进行复习练习。 - 注意
SharedPreferences
提交apply()
方法与commit()
方法。 - 数据库的原生 API 使用及一些第三方库的使用。
- 数据库操作对象及游标对象等在使用结束后一定要进行关闭。
- 跨程序共享数据,内容提供器 Content Provider 主要用于在不同的应用程序之间是实现数据共享功能。它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访问数据的安全性。
- 内容提供器可以选择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄漏的风险。
-
Android 6.0 以下
版本安装时授权,不授权不安装。可在应用管理界面查看权限申请情况。 -
Android 6.0 及以上
版本运行时授权,不授权部分功能不能用。可在应用管理界面管理权限授权或不授权。 -
权限分类
- 普通权限
- 危险权限
- 特殊权限
-
每个危险权限都属于一个权限组,申请的某个权限被授权时,该组所有权限也会同时被授权。
-
请求权限核心方法
-
ContextCompat.checkSelfPermission(@NonNull Context context, @NonNull String permission)
检查是否有权限 -
ActivityCompat.requestPermissions(Activity activity, String[] permissions, int requestCode)
请求权限 -
onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)
权限请求结果的回调/** * 点击按钮执行操作 */ public void request(View view) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {// 判断没有权限 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CALL_PHONE}, 1);// 请求权限[上下文][权限数组集合][请求码] return; } else {// 判断有权限 callPhone(); } } /** * 请求权限用户操作后回调函数 * * @param requestCode 请求码 * @param permissions 权限数组集合 * @param grantResults 授权情况数组集合 */ @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode) { case 1: if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { callPhone(); } else { ToastUtil.showShortToast(this, "You denied the permission"); } break; } }
-
-
读取系统联系人
private void readContacts() { Cursor cursor = null;// 游标对象 try { cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null); if (cursor != null) { while (cursor.moveToNext()) {// 循环读取数据 String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));// 姓名 String number = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));// 手机号 } } } catch (Exception e) { e.printStackTrace(); } finally { if (cursor != null) { cursor.close();// 关闭游标 } } }
-
ContentResolver 的基本用法
-
通过
Context
中的getContentResolver()
方法获取到ContentResolver
的实例。 -
利用
ContentResolver
实例进行数据的CRUD
操作insert()
方法进行添加
数据update()
方法进行更新
数据delete()
方法进行删除
数据query()
方法进行查询
数据
-
不同于
SQLite 的是方法都不接收表名参数,而是使用一个Uri
参数代替。 -
内容 URI 给内容提供器中的数据建立了唯一标识符,主要由两部分组成:
authority
和path
-
authority
用于对不同的应用程序做区分,采用包名进行命名 -
path
则是用于对同一应用不同表名进行区分,添加在authority
之后 -
schema
协议添加于头部String uriString = "content://com.just.first/table"; Uri uri = Uri.parse(uriString);
-
-
查询数据
Cursor cursor = getContentResolver().query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
- 查询返回
Cursor
对象
query() 方法参数 对应 SQL 部分 描述 uri from table_name 指定查询某应用程序的某张表 projection select column1, column2 指定查询的列名 selection where column = value 指定 where 的约束条件 selectionArgs - 为 where 中的占位符提供具体的值 sortOrder order by column1, column2 指定查询结果的排序方式 - 查询返回
-
添加数据
getContentResolver().insert(Uri url, ContentValues values);
- 同样使用
ContentValues
键值对进行数据的封装
- 同样使用
-
修改数据
getContentResolver().update(Uri uri, ContentValues values, String where, String[] selectionArgs)
-
删除数据
getContentResolver().delete(Uri uri, ContentValues values, String where, String[] selectionArgs)
-
-
自定义内容提供器继承
ContentProvider
-
实现 6 个抽象方法
/** * 7.4.1 自定义内容提供器 * * @author JustDo23 */ public class FirstContentProvider extends ContentProvider { /** * 初始化内容提供器。完成数据库的创建和升级操作。[只有当存在 ContentResolver 尝试访问时才会初始化] * * @return [true, 初始化成功][false,初始化失败] */ @Override public boolean onCreate() { return false; } /** * 从内容提供器查询数据。 * * @param uri 指定查询哪张表 * @param projection 确定查询哪些列 * @param selection 约束查询哪些行 * @param selectionArgs 为约束赋值 * @param sortOrder 查询结果排序 * @return 游标对象 */ @Nullable @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { return null; } /** * 向内容提供器中添加数据。 * * @param uri 指定哪张表 * @param values 待添加数据键值对 * @return 返回一个用户表示这条新纪录的 URI */ @Nullable @Override public Uri insert(Uri uri, ContentValues values) { return null; } /** * 更新内容提供器中已有数据。 * * @param uri 指定哪张表 * @param values 待更新数据键值对 * @param selection 约束更新哪些行 * @param selectionArgs 为约束赋值 * @return 返回受影响的行数 */ @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { return 0; } /** * 从内容提供器中删除数据。 * * @param uri 指定哪张表 * @param selection 约束删除哪些行 * @param selectionArgs 为约束赋值 * @return 返回被删除的行数 */ @Override public int delete(Uri uri, String selection, String[] selectionArgs) { return 0; } /** * 返回 MIME 类型 * * @param uri 指定哪张表 * @return 返回 MIME 类型 */ @Nullable @Override public String getType(Uri uri) { return null; } }
-
通配符
一个标准的内容 URI 写法
content://com.just.first/table
表示访问应用
com.just.first
中的table
数据表。还可以在其后添加一个id
content://com.just.first/table/23
表示访问表中
id
为23
的数据。内容 URI
的格式主要有以上两种,以路径结尾就表示期望访问该表中的所有数据,以 id 结尾就表示期望访问该表中拥有相应 id 的数据。可以使用通配符来分别匹配这两种格式的内容 URI。- 星号表示匹配任意长度的任意字符
- 井号表示匹配任意长度的数字
一个能够匹配任意表的内容 URI 格式可以写成
content://com.just.first/*
一个能够匹配表中任意一行数据的内容 URI 格式可以写成
content://com.just.first/table/#
-
通配符使用
public class FirstContentProvider extends ContentProvider { public static final int TABLE_1_DIR = 0;// 自定义码 public static final int TABLE_1_ITEM = 1; public static final int TABLE_2_DIR = 2; public static final int TABLE_2_ITEM = 3; public static UriMatcher uriMatcher;// 用于匹配的对象 public static final String PACKAGE_NAME = "com.just.first";// 主包名 static {// 静态代码块 uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);// 用于匹配的对象 uriMatcher.addURI(PACKAGE_NAME, "table1", TABLE_1_DIR);// 添加路径 uriMatcher.addURI(PACKAGE_NAME, "table1/#", TABLE_1_ITEM);// 可以使用通配符 uriMatcher.addURI(PACKAGE_NAME, "table2", TABLE_2_DIR); uriMatcher.addURI(PACKAGE_NAME, "table2/#", TABLE_2_ITEM); } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { switch (uriMatcher.match(uri)) {// 进行匹配并返回相应的自定义码 case TABLE_1_DIR: LogUtils.e("查询 table1 表中的所有数据"); break; case TABLE_1_ITEM: LogUtils.e("查询 table1 表中的单条数据"); break; case TABLE_2_DIR: LogUtils.e("查询 table2 表中的所有数据"); break; case TABLE_2_ITEM: LogUtils.e("查询 table2 表中的单条数据"); break; } return null; } }
-
关于类型
-
getType()
方法是所有内容提供器必须提供的一个方法,用于获取相应的 MIME 类型。 -
一个内容 URI 所对应的
MIME
字符串主要由 3 部分组成必须
以 vnd 开头- 如果 URI 以
路径
结尾则后接android.cursor.dir/
- 如果 URI 以
id
结尾则后接android.cursor.item/
- 最后接上
vnd.<authority>.<path>
内容 URI
content://com.just.first/table
返回 MIME 类型
vnd.android.cursor.dir/vnd.com.just.first.table
内容 URI
content://com.just.first/table/23
返回 MIME 类型
vnd.android.cursor.item/vnd.com.just.first.table
-
根据以上内容重写
getType()
方法
-
-
数据安全问题
因为所有的 CRUD 操作都一定要匹配到相应的内容 URI 格式才能进行,而我们当然不可能向 UriMatcher 中添加隐私数据的 URI,所以这部分数据根本无法被外部程序访问到,安全问题也就不存在了。
-
跨进程访问时不能直接使用 Toast
-
使用内容提供器需要进行注册
<provider android:name=".chapter07.DataBaseProvider" android:authorities="com.just.first.provider" android:enabled="true" android:exported="true" />
-
忽略文件
项目目录下
.gitignore
是忽略文件,允许用户将指定的文件排除在版本控制之外。 -
查看状态
$ git status
-
查看修改内容
$ git diff
其后可以指定文件来查看该文件的更改记录
- 加号代表新增
- 减号代表删除
-
撤销未添加的修改
$ git checkout fileName
- 前提是还没有执行
add
命令
- 前提是还没有执行
-
撤销未提交的修改
$ git reset HEAD
- 前提是执行了
add
命令但还没有执行commit
命令
- 前提是执行了
-
查看提交记录
$ git log
- 提交记录包含
提交 id
及提交人
及提交日期
及提交描述
这4个信息
$ git log id -l
- 命令后为
提交 id
及小写-L
查看该 ID 的记录
$ git log id -l -p
- 查看该 ID 的修改内容
- 提交记录包含
- 运行时权限
- 内容提供者
-
通知 Notification 在手机最上方的状态栏中会显示一个通知的图标,下拉状态栏后可以看到通知的详细内容。
-
通知可以在活动里创建,可以在广播接收器中创建,可以在服务里创建。
-
创建并显示通知
private void showNotification() { NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);// 获取通知管理对象 Notification notification = new NotificationCompat.Builder(this)// Builder 设计模式 .setContentTitle("This is title")// 设置标题 .setContentText("This is content")// 设置文本 .setWhen(System.currentTimeMillis())// 指定通知被创建的时间以毫秒为单位 .setSmallIcon(R.mipmap.ic_launcher)// 设置小图标只能使用纯 alpha 图层的图片 .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_mario))// 设置大图标 .build();// 创建 notificationManager.notify(23, notification);// 通知管理器去显示该条通知[通知的ID][通知对象]要保证 ID 的不同 }
- 通过 Context 获取通知管理对象
- 为了保证各个版本兼容使用
support-v4
包中的NotificationCompat
来创建NotificationManager
对象 - 注意设计模式之
Builder
设计模式 - 设置小图标只能使用纯 alpha 图层的图片
- 通知显示时需要 ID 同时要保证 ID 的不同
-
PendingIntent
- 类似 Intent 指明意图,可用于
启动活动
,启动服务
,发送广播
等。 - 不同点 Intent 更加倾向于立即执行某个动作,而 PendingIntent 更加倾向于在某个合适的时机去执行某个动作。
- 可以把 PendingIntent 简单理解为延迟执行的 Intent
Intent intent = new Intent(this, MainActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
- 第二个参数请求码
- 第四个参数确定行为的标识,取值有四种
FLAG_ONE_SHOT
,FLAG_NO_CREATE
,FLAG_CANCEL_CURRENT
,FLAG_UPDATE_CURRENT
- 类似 Intent 指明意图,可用于
-
通知取消
-
在
build()
方法之前进行调用.setAutoCancel(true)// 设置自动取消
-
通过通知管理对象取消指定 ID 的通知
notificationManager.cancel(23);// 通知管理对象取消指定 ID 的通知
-
-
设置声音
.setSound(Uri.fromFile(new File("/system/media/audio/ringtones/23_Game.ogg")))// 设置声音
- 通过 Uri 传递一个音频文件的地址
-
设置振动
.setVibrate(new long[]{0, 1000, 1000, 1000, 1000, 1000})// 设置振动[静止时长][振动时长]单位毫秒
- 参数是长整型的数组,用于设置手机静止和振动的时长,以毫秒为单位
- 参数顺序
[静止时长][振动时长][静止时长][振动时长]
如此循环 - 振动需要手机权限
<uses-permission android:name="android.permission.VIBRATE" />
-
设置 LED 灯
.setLights(Color.RED, 1000, 1000)// 设置 LED 灯 [颜色][亮灯时长][灭灯时长]单位毫秒
- 发送通知后息屏过一会儿观察 LED 灯
- 程序被死之后 LED 也会停止闪烁
-
设置通知为默认配置
.setDefaults(NotificationCompat.DEFAULT_ALL)// 设置默认配置
- 它会根据当前手机的环境来决定播放什么铃声,以及如何振动等
-
构建富文本通知
-
显示超长的文本
.setStyle(new NotificationCompat.BigTextStyle().bigText("If we can only encounter each other rather than stay with each other,then I wish we had never encountered."))// 显示特别长的文本
-
显示大图片
.setStyle(new NotificationCompat.BigPictureStyle().bigPicture(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_mountain)))// 显示大图片
-
-
设置通知重要程度
.setPriority(NotificationCompat.PRIORITY_MAX)// 设置通知的重要程度
PRIORITY_DEFAULT
默认程度,和不设置一样PRIORITY_MIN
最低重要程度,系统会在特定情况显示比如下拉状态栏的时候PRIORITY_LOW
较低重要程度,系统会将通知缩小或改变其显示的顺序将其靠后PRIORITY_HIGH
较高重要程度,系统会将通知放大或改变其显示的顺序将其靠前PRIORITY_MAX
最高重要程度,必须让用户立刻看到甚至需要用户做出响应操作
-
调用摄像头拍照
private Uri imageUri;// 获取一个 URI 对象 public static final int TAKE_PHOTO = 23; /** * 调用摄像头拍照 */ public void takePhoto(View view) { File imageFile = new File(getExternalCacheDir(), "image.jpg");// 指定文件的路径及名称 if (imageFile.exists()) { imageFile.delete();// 文件存在就删除 } try { imageFile.createNewFile();// 创建新的文件 } catch (IOException e) { e.printStackTrace(); } // 内容提供者 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {// Android 7.0 进行适配 imageUri = FileProvider.getUriForFile(this, "com.just.first.fileprovider", imageFile);// [上下文][任意一个唯一字符串][File对象] } else {// 这个 URI 标识者图片的本地真是路径 imageUri = Uri.fromFile(imageFile); } Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");// 创建意图并指定 Action intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);// 携带参数 startActivityForResult(intent, TAKE_PHOTO);// 启动意图 } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) {// 此方式中指定了拍照图片位置因此 data 为 null case TAKE_PHOTO: try {// 手机拍照图片一般3M左右因此处理图片内存溢出需要注意 Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(imageUri));// 从内容提供者中获取数据 iv_photo.setImageBitmap(bitmap);// 进行图片的显示 } catch (FileNotFoundException e) { e.printStackTrace(); } break; } }
-
兼容适配
调用 FileProvider 的 getUriForFile() 方法将 File 对象转换成一个封装过的 Uri 对象。该 getUriForFile() 方法接收3个参数,第一个 Context 对象,第二个可以是任意唯一的字符串,第三个是 File 对象。从 Android 7.0 开始直接使用本地真实路径的 Uri 被认为是不安全的,会抛出一个 FileUriExposedException 异常。而 FileProvider 则是一种特殊的内容提供器,它使用了和内容提供器类似的机制来对数据进行保护,可以选择性地将封装过的 Uri 共享给外部,从而提高了应用的安全性。
**注意:**并没有结束,还需要在功能清单中注册内容提供器
<!-- Android 7.0 --> <provider android:name="android.support.v4.content.FileProvider" android:authorities="com.just.first.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider>
-
android:name
属性的值是固定的 -
android:authorities
属性的值必须和getUriForFile()
方法第二个参数一致 -
<meta-data/>
标签指定 Uri 的共享路径,引用了一个资源文件 -
在项目的
res
路径下创建xml
文件夹并创建file_paths.xml
文件<?xml version="1.0" encoding="utf-8"?> <resources> <paths> <external-path name="camera_photos" path="Android/data/com.just.first/" /> <external-path name="external_storage_root" path="." /> </paths> </resources>
<external-path/>
标签指定 Uri 共享name
属性可以随便填写path
属性值表示共享的具体路径path
属性不填写表示将整个 SD 卡进行共享
-
-
权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
在 Android 4.4 系统之后访问 SD 卡的应用关联目录不用声明权限。
-
从相册中选择照片
public static final int CHOOSE_ALBUM = 24; /** * 点击按钮从手机相册中选取 */ public void chooseAlbum(View view) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {// 检查是否有权限 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 25);// 没有权限进行申请权限 } else { openAlbum();// 有权限则打开相册 } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode) { case 25: if (grantResults != null && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { openAlbum();// 有权限则打开相册 } else { ToastUtil.showShortToast(this, "You denied the permission."); } break; } } private void openAlbum() { Intent intent = new Intent("android.intent.action.GET_CONTENT");// 指定 action intent.setType("image/*");// 指定类型 startActivityForResult(intent, CHOOSE_ALBUM);// 通过 Intent 打开相册 } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case CHOOSE_ALBUM: if (RESULT_OK == resultCode) {// 正常的返回码 if (Build.VERSION.SDK_INT >= 19) {// Android 4.4 及以上版本 HandleImageOnKitKat(data); } else {// Android 4.4 以下版本 HandleImageBeforeKitKat(data); } } break; } } @RequiresApi(api = Build.VERSION_CODES.KITKAT) private void HandleImageOnKitKat(Intent data) { String imagePath = null;// 图片路径 Uri uri = data.getData();// 获取 Uri 对象 if (DocumentsContract.isDocumentUri(this, uri)) {// 如果是 Document 类型的 Uri String documentId = DocumentsContract.getDocumentId(uri); if ("com.android.providers.media.documents".equals(uri.getAuthority())) { String id = documentId.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 = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(documentId)); 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;// 通过内容提供者获取图片路径 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 (!TextUtils.isEmpty(imagePath)) {// 注意内存溢出 Bitmap bitmap = BitmapFactory.decodeFile(imagePath);// 利用工厂类从路径加载出图片 iv_photo.setImageBitmap(bitmap);// 显示图片 } else { ToastUtil.showShortToast(this, "Failed to get image."); } }
-
动态权限申请
相册中的图片都是存储在 SD 卡上的,我们要从 SD卡中读取照片就需要申请这个权限。
-
意图启动
指定意图 action 为
android.intent.action.GET_CONTENT
以及意图类型为image/*
启动相册 -
兼容适配
在 Android 4.4 及以上的版本中,选取相册中的图片不再返回图片真实的 Uri 了,而是一个封装过的 Uri 对象,因此需要对这个封装过的对象进行解析。
-
使用 MediaPlayer 播放本地音频
private MediaPlayer mediaPlayer; /** * 初始化播放器 */ private void initMediaPlay() { release();// 资源释放 mediaPlayer = new MediaPlayer();// 初始化 File audioFile = new File(Environment.getExternalStorageDirectory() + "/JustDo23/audio/", "Sugar.mp3");// 指定文件路径 try { mediaPlayer.setDataSource(audioFile.getPath()); mediaPlayer.prepare(); } catch (IOException e) { e.printStackTrace(); } } /** * 释放资源 */ private void release() { if (mediaPlayer != null) { if (mediaPlayer.isPlaying()) { mediaPlayer.stop(); mediaPlayer.reset(); } mediaPlayer.release(); mediaPlayer = null; } } @Override public void onClick(View v) { switch (v.getId()) { case R.id.bt_play:// 播放 if (mediaPlayer != null) { mediaPlayer.start(); } break; case R.id.bt_pause:// 暂停 if (mediaPlayer != null && mediaPlayer.isPlaying()) { mediaPlayer.pause(); } break; case R.id.bt_stop:// 停止 initMediaPlay(); break; } }
-
使用 MediaPlayer 常用方法
方法名 功能描述 setDataSource() 设置要播放的音频文件的位置 prepare() 进行准备操作 start() 开始或继续播放 pause() 暂停或者继续播放 reset() 重置到刚刚创建的状态 seekTo() 从指定位置开始播放 stop() 停止播放调用之后需要重新初始才能继续播放 release() 释放相关的资源 isPlaying() 是否正在播放 getDuration() 获取载入的音频文件的总时长 -
注意方法的调用顺序
-
注意配合 Activity 的生命周期进行控制
@Override protected void onDestroy() { super.onDestroy(); release(); }
-
使用 VideoView 播放本地视频
private VideoView videoView; /** * 初始视频路径 */ private void initVideoPath() { File videoFile = new File(Environment.getExternalStorageDirectory() + "/JustDo23/video/", "luck.mp4");// 指定文件路径 videoView.setVideoPath(videoFile.getPath()); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.bt_play:// 播放 if (!videoView.isPlaying()) { videoView.start(); } break; case R.id.bt_pause:// 暂停 if (videoView.isPlaying()) { videoView.pause(); } break; case R.id.bt_restart:// 停止 if (videoView.isPlaying()) { videoView.resume(); } break; } } @Override protected void onDestroy() { super.onDestroy(); if (videoView != null) { videoView.suspend();// 资源释放 } }
-
使用 VideoView 常用方法
方法名 功能描述 setVideoPath() 设置要播放的视频文件的位置 start() 开始或继续播放 pause() 暂停播放 resume() 视频从头开始播放 seekTo() 从指定位置开始播放 isPlaying() 是否正在播放 getDuration() 获取载入的视频文件的总时长
- 类似网易云音乐这种可以控制音乐播放的通知的实现。自定义通知布局。
- 图片的相关处理,避免内存溢出等。
- 对 MediaPlayer 进行详细的总结。
- 对 VideoView 进行详细的学习总结。
- 查阅 VideoView 源码。
-
使用 WebView 加载网页
private void initWebView() { wb_net.getSettings().setJavaScriptEnabled(true);// 支持 JavaScript 脚本 wb_net.setWebViewClient(new WebViewClient());// 网页跳转仍在当前浏览器 wb_net.loadUrl("http://www.baidu.com");// 加载网页 }
-
方法
setWebViewClient()
方法作用,当需要从一个网页跳转另一个网页时,目标网页仍然在当前 WebView 中显示,而不是打开系统浏览器。
-
网络权限
<uses-permission android:name="android.permission.INTERNET" />
-
工作原理
客户端向服务器发出一条 HTTP 请求,服务器收到请求之后返回一些数据给客户端,然后客户端再对这些数据进行解析和处理就可以了。
-
使用 HttpURLConnection
private void sendRequestWithHttpURLConnection() { new Thread(new Runnable() {// 网络请求耗时操作放在子线程中 @Override public void run() { HttpURLConnection httpURLConnection = null;// 连接对象 BufferedReader bufferedReader = null;// 数据读取流 try { URL url = new URL("http://www.baidu.com");// URL 对象 httpURLConnection = (HttpURLConnection) url.openConnection();// 打开连接 httpURLConnection.setRequestMethod("GET");// 设置网络请求模式 httpURLConnection.setConnectTimeout(8000);// 设置连接超时时间 httpURLConnection.setReadTimeout(8000);// 设置数据读取超时时间 InputStream inputStream = httpURLConnection.getInputStream();// 获取数据读取流 bufferedReader = new BufferedReader(new InputStreamReader(inputStream));// 对流封装提供效率 StringBuilder response = new StringBuilder();// 请求结果 String line; while ((line = bufferedReader.readLine()) != null) { response.append(line);// 从数据读取流中读取数据 } showResponse(response.toString());// 进行界面展示 } catch (IOException e) { e.printStackTrace(); } finally { if (bufferedReader != null) { try { bufferedReader.close();// 关闭数据流 } catch (IOException e) { e.printStackTrace(); } } if (httpURLConnection != null) { httpURLConnection.disconnect();// 关闭网络连接 } } } }).start(); } private void showResponse(final String response) { runOnUiThread(new Runnable() {// 界面刷新的工作必须放在主线程中 @Override public void run() { tv_result.setText(response); } }); }
-
小细节
- 网络请求模式有
GET
和POST
等,其中GET
表示希望从服务器那里获取数据,而POST
表示希望提交数据给服务器。 - 设置网络连接超时时间
- 设置数据读取超时时间
- 设置请求头数据
- 网络请求等耗时操作需要放在子线程
- 界面控件刷新需要放在主线程
- 网络请求模式有
-
添加依赖
compile 'com.squareup.okhttp3:okhttp:3.4.1'// OKHttp
-
使用 OKHttp
private void sendRequestWithOkHttp() { new Thread(new Runnable() {// 网络请求耗时操作放在子线程中 @Override public void run() { try { OkHttpClient okHttpClient = new OkHttpClient();// OK 客户端 RequestBody requestBody = new FormBody.Builder() .add("userName", "admin") .add("passWord", "232323") .build();// 参数封装 Request request = new Request.Builder() .url("https://www.baidu.com") .post(requestBody)// 用 POST 请求携带参数 .build(); Response response = okHttpClient.newCall(request).execute();// 执行请求返回响应对象 String responseContent = response.body().string();// 从响应对象中获取字符串 showResponse(responseContent);// 进行界面展示 } catch (IOException e) { e.printStackTrace(); } } }).start(); }
-
XML 格式数据内容
<apps> <app> <id>1</id> <name>Google</name> <version>1.1</version> </app> <app> <id>2</id> <name>FaceBook</name> <version>1.2</version> </app> <app> <id>3</id> <name>Twitter</name> <version>1.3</version> </app> </apps>
-
使用 Pull 解析
private void parseXMLWithPull(String responseContent) { try { XmlPullParserFactory xmlPullParserFactory = XmlPullParserFactory.newInstance();// 获取工厂实例 XmlPullParser xmlPullParser = xmlPullParserFactory.newPullParser();// 工厂实例获取一个解析器 xmlPullParser.setInput(new StringReader(responseContent));// 以流的方式给解析器设置数据源 int eventType = xmlPullParser.getEventType();// 获取事件类型 String id = ""; String name = ""; String version = ""; while (eventType != XmlPullParser.END_DOCUMENT) {// 不是文档结尾 String nodeName = xmlPullParser.getName();// 获取节点名称 switch (eventType) {// 事件类型 case XmlPullParser.START_TAG:// 标签开始 if ("id".equals(nodeName)) {// 判断标签名称 id = xmlPullParser.nextText();// 获取标签中的内容 } else if ("name".equals(nodeName)) { name = xmlPullParser.nextText(); } else if ("version".equals(nodeName)) { version = xmlPullParser.nextText(); } break; case XmlPullParser.END_TAG:// 标签结束 if ("app".equals(nodeName)) { LogUtils.e("id = " + id + " name = " + name + " version = " + version + "\n"); } break; default: break; } eventType = xmlPullParser.next();// 获取下一个事件 } } catch (Exception e) { e.printStackTrace(); } }
-
重要方法
getName()
方法获取当前节点名称nextText()
方法获取当前节点内的具体内容
-
自定义 SaxHandler 继承自 DefaultHandler
/** * 9.3.2 SAX 解析方式 * * @author JustDo23 * @since 2017年08月01日 */ public class SaxHandler extends DefaultHandler { private String nodeName; private StringBuilder id; private StringBuilder name; private StringBuilder version; /** * 开始解析文档 * * @throws SAXException 异常 */ @Override public void startDocument() throws SAXException { super.startDocument(); id = new StringBuilder(); name = new StringBuilder(); version = new StringBuilder(); } /** * 开始解析节点 * * @param uri 命名空间字符串[可能为空] * @param localName 节点名称[可能为空] * @param qName 限定名[可能为空] * @param attributes 属性 * @throws SAXException 异常 */ @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { super.startElement(uri, localName, qName, attributes); nodeName = localName;// 当前节点名称 } /** * 获取节点内容[可能会调用多次,一些换行符也被当作内容解析出来] * * @param ch 字节数组 * @param start 起始位置 * @param length 有效长度 * @throws SAXException */ @Override public void characters(char[] ch, int start, int length) throws SAXException { super.characters(ch, start, length); if ("id".equals(nodeName)) { id.append(ch, start, length); } else if ("name".equals(nodeName)) { name.append(ch, start, length); } else if ("version".equals(nodeName)) { version.append(ch, start, length); } } /** * 完成节点解析 * * @param uri 命名空间字符串[可能为空] * @param localName 节点名称[可能为空] * @param qName 限定名[可能为空] * @throws SAXException 异常 */ @Override public void endElement(String uri, String localName, String qName) throws SAXException { super.endElement(uri, localName, qName); if ("app".equals(localName)) { LogUtils.e("id = " + id.toString() + " name = " + name.toString() + " version = " + version.toString() + "\n"); id.setLength(0); name.setLength(0); version.setLength(0); } } /** * 完成文档解析 * * @throws SAXException 异常 */ @Override public void endDocument() throws SAXException { super.endDocument(); LogUtils.e("SAX 解析结束"); } }
-
使用 SAX 解析
private void parseXMLWithSAX(String responseContent) { try { SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();// 获取工厂实例 XMLReader xmlReader = saxParserFactory.newSAXParser().getXMLReader();// 利用工厂获取解析器后获取XML读取器 SaxHandler saxHandler = new SaxHandler();// 实例化自定义的 Handler xmlReader.setContentHandler(saxHandler);// 读取器设置 Handler xmlReader.parse(new InputSource(new StringReader(responseContent)));// 开始解析 } catch (Exception e) { e.printStackTrace(); } }
-
json 格式数据内容
[ { "id": "4", "name": "Chrome", "version": "1.4" }, { "id": "5", "name": "Safari", "version": "1.5" }, { "id": "6", "name": "Firefox", "version": "1.6" } ]
-
使用 JSONObject
private void parseJson(String responseContent) { try { JSONArray jsonArray = new JSONArray(responseContent);// 获取数组对象 for (int i = 0; i < jsonArray.length(); i++) {// 对数组进行循环 JSONObject jsonObject = jsonArray.getJSONObject(i);// 挨个获取JSONObject String id = jsonObject.getString("id"); String name = jsonObject.getString("name"); String version = jsonObject.getString("version"); LogUtils.e("id = " + id + " name = " + name + " version = " + version + "\n"); } } catch (Exception e) { e.printStackTrace(); } }
-
使用 Gson
-
添加依赖
compile 'com.google.code.gson:gson:2.8.1'// Gson
-
定义实体类
-
使用 Gson
private void parseJsonWithGson(String responseContent) { Gson gson = new Gson();// 实例化对象 List<Product> productList = gson.fromJson(responseContent, new TypeToken<List<Product>>() {}.getType());// 解析数组方法 for (Product product : productList) { LogUtils.e("id = " + product.getId() + " name = " + product.getName() + " version = " + product.getVersion() + "\n"); } }
-
-
回调接口
public interface HttpCallBackListener { /** * 网络请求完成时回调 * * @param response 返回数据 */ void onFinish(String response); /** * 网络请求出现错误 * * @param e 异常 */ void onError(Exception e); }
-
工具类封装
public class HttpUtil { public static void sendHttpRequest(final String address, final HttpCallBackListener httpCallBackListener) { new Thread(new Runnable() { @Override public void run() { HttpURLConnection httpURLConnection = null; try { URL url = new URL(address); httpURLConnection = (HttpURLConnection) url.openConnection(); httpURLConnection.setRequestMethod("GET"); httpURLConnection.setConnectTimeout(8000); httpURLConnection.setReadTimeout(8000); httpURLConnection.setDoInput(true); httpURLConnection.setDoOutput(true); InputStream inputStream = httpURLConnection.getInputStream(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); StringBuilder response = new StringBuilder(); String line; while ((line = bufferedReader.readLine()) != null) { response.append(line); } if (httpCallBackListener != null) { httpCallBackListener.onFinish(response.toString()); } } catch (Exception e) { if (httpCallBackListener != null) { httpCallBackListener.onError(e); } } finally { if (httpURLConnection != null) { httpURLConnection.disconnect(); } } } }).start(); } public static void sendOkHttpRequest(String address, okhttp3.Callback callback) { OkHttpClient okHttpClient = new OkHttpClient(); Request request = new Request.Builder() .url(address) .build(); okHttpClient.newCall(request).enqueue(callback);// 开启子线程 } }
-
注意线程问题
- 对 WebView 进行更详细的学习使用。
- 对 HTTP 协议进行更详细的学习使用。
- 如果可以那就看看 OKHttp 等的源码解析。
- 其他第三方网络请求框架简单了解。
-
服务 Service 是 Android 中实现程序后台运行的解决方案,它非常适合去执行那些不需要和用户交互而且还要求长期运行的任务。
-
需要注意的是服务并不是运行在一个独立的进程当中,而是依赖于创建服务时所在的应用程序进程。当某个应用程序进程被杀掉时,所有依赖于该进程的服务也会停止运行。
-
实际上服务并不会自动开启线程,所有的代码都是默认运行在主线程当中的。服务中的耗时操作仍然需要我们为其创建子线程,否则会出现主线程被阻塞的情况。
-
继承 Thread
class FirstThread extends Thread { @Override public void run() { // 耗时操作 } }
启动线程
new FirstThread().start();
-
实现 Runnable
class FirstRunnable implements Runnable { @Override public void run() { // 耗时操作 } }
启动线程
new Thread(new FirstRunnable()).start();
-
综合匿名内部类
new Thread(new Runnable() { @Override public void run() { // 耗时操作 } }).start();
-
Android 中的 UI 是线程不安全的,也就是说必须在主线程中进行更新,否则会异常。
-
在子线程中需要更新 UI 是可以使用 Handler 机制进行。
private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what) { case 32: // 更新 UI 操作 tv_result.setText("Nice to meet you"); break; } } }; public void uiReferenceHandler(View view) { new Thread(new Runnable() { @Override public void run() { Message message = handler.obtainMessage();// 获取消息对象 message.what = 32;// 设置标志码 handler.sendMessage(message);// 发送消息 } }).start(); }
-
异步消息处理主要由 4 个部分组成
Message
是在线程之间传递的消息,它可以在内部携带少量的信息,用于在不同线程之间交换数据。Handler
是消息处理者,主要用于发送和处理消息。发送消息一般是使用 Handler 的sendMessage()
方法,而发出的消息经过一系列地辗转处理后,最终会传递到 Handler 的handleMessage()
方法中。MessageQueue
是消息队列,它主要用于存放所有通过 Handler 发送的消息。这部分消息会一直存在于消息队列中,等待被处理。每个线程中只会有一个消息队列MessageQueue
对象。Looper
是每个线程中的 MessageQueue 的管家,调用 Looper 的loop()
方法后,就会进入到一个无限循环当中,然后每当发现MessageQueue
中存在一条消息,就会将它取出,并传递到 Handler 的handleMessage()
方法中。每个线程中也只会有一个 Looper 对象。
-
上小节中由于 Handler 是在主线程中创建的,所以此时
handlerMessage()
方法中代码也会在主线程中运行,于是就可以安心进行 UI 操作了。 -
上小节中使用的
runOnUiThread()
方法其实就是一个异步消息处理机制的接口封装。
-
使用 AsyncTask 可以十分简单地从子线程切换到主线程。当然,AsyncTask 背后的实现原理也是基于异步消息处理机制。
-
自定义类继承抽象类
AsyncTask<Params, Progress, Result>
同时指定 3 个泛型参数Params
在执行 AsyncTask 时需要传入的参数,可用于在后台任务中使用。Progress
后台任务执行时,如果需要在界面上显示当前的进度,则使用这里指定的泛型作为进度单位。Result
当任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。
-
实现抽象类中的抽象方法
onPreExecute()
- 后台任务开始之前进行回调
- 用于进行一些界面上的初始化操作
doInBackground(Params... params)
- 在子线程中执行耗时操作
- 耗时操作执行结束之后将结果返回
- 此方法中不能进行 UI 操作
onProgressUpdate(Progress... values)
- 后台耗时操作执行过程中的进度回调
- 此方法中可以进行 UI 操作
onPostExecute(Result result)
- 后台耗时操作执行结束并通过 return 语句进行返回时被调用
- 可以利用返回的数据进行 UI 的刷新
-
使用 AsyncTask
/** * 自定义异步任务 * * @since 2017年08月03日 */ class NetAsyncTask extends AsyncTask<String, Integer, String> { /** * 任务启动之前 */ @Override protected void onPreExecute() { super.onPreExecute(); } /** * 任务启动并后台运行 */ @Override protected String doInBackground(String... params) { publishProgress(20);// 进行进度刷新 return null; } /** * 任务运行进度 */ @Override protected void onProgressUpdate(Integer... values) { super.onProgressUpdate(values); } /** * 任务执行完毕 */ @Override protected void onPostExecute(String s) { super.onPostExecute(s); } }
publishProgress(Progress... values)
- 进行进度刷新调用之后会回调
onProgressUpdate(Progress... values)
方法
- 进行进度刷新调用之后会回调
-
启动 AsyncTask
new NetAsyncTask().execute("https://www.baidu.com");
-
自定义服务继承 Service
/** * 10.3.1 服务入门 * * @since 2017年08月03日 */ public class FirstService extends Service { /** * 在服务被创建时调用 */ @Override public void onCreate() { super.onCreate(); } /** * @param intent 意图 * @return 绑定对象 */ @Override public IBinder onBind(Intent intent) { // TODO: Return the communication channel to the service. throw new UnsupportedOperationException("Not yet implemented"); } /** * 在每次服务启动的时候调用 * * @param intent 意图 * @param flags 标识 * @param startId 启动码 * @return 整型 */ @Override public int onStartCommand(Intent intent, int flags, int startId) { return super.onStartCommand(intent, flags, startId); } /** * 在服务销毁时调用 */ @Override public void onDestroy() { super.onDestroy(); } }
-
功能清单中注册
<service android:name=".chapter10.FirstService" android:enabled="true" android:exported="true" />
-
启动与停止
-
启动
Intent startIntent = new Intent(this, FirstService.class);// 意图指定服务 startService(startIntent);// 启动服务
-
停止
Intent stopIntent = new Intent(this, FirstService.class);// 意图指定服务 stopService(stopIntent);// 停止服务
-
注意
- 调用
Context
类中的startService()
方法和stopService()
方法进行启动和停止 - 完全由 Activity 进行控制,服务本身有一个
stopSelf()
方法可以停止服务
- 调用
-
-
打印日志
// 点击启动 E/JustDo23: FirstService --> onCreate() E/JustDo23: FirstService --> onStartCommand() // 再次点击启动 E/JustDo23: FirstService --> onStartCommand() // 再次点击启动 E/JustDo23: FirstService --> onStartCommand() // 点击停止 E/JustDo23: FirstService --> onDestroy()
onCreate()
方法是在服务第一次创建的时候调用onStartCommande()
方法则在每次启动服务的时候都会调用
-
使用 Binder
public class FirstService extends Service { private DownloadBinder downloadBinder = new DownloadBinder(); @Override public IBinder onBind(Intent intent) { LogUtils.e("FirstService --> onBind()"); return downloadBinder; } /** * 使用 Binder 机制 * * @since 2017年08月04日 */ class DownloadBinder extends Binder { public void startDownload() { LogUtils.e("DownloadBinder --> startDownload()"); } public int getProgress() { LogUtils.e("DownloadBinder --> getProgress()"); return 0; } } }
-
获取 Binder
public class FirstServiceActivity extends BaseActivity { private FirstService.DownloadBinder downloadBinder; /** * 服务连接对象 */ private ServiceConnection serviceConnection = new ServiceConnection() { /** * 服务连接回调 */ @Override public void onServiceConnected(ComponentName name, IBinder service) { downloadBinder = (FirstService.DownloadBinder) service; downloadBinder.startDownload(); downloadBinder.getProgress(); } /** * 服务断开连接回调 */ @Override public void onServiceDisconnected(ComponentName name) { } }; }
-
绑定与解绑
-
绑定
Intent bindIntent = new Intent(this, FirstService.class);// 意图指定服务 bindService(bindIntent, serviceConnection, BIND_AUTO_CREATE);// 绑定服务[标志位表示活动和服务进行绑定之后自动创建服务]
-
解绑
unbindService(serviceConnection);// 解绑服务[停止服务]
-
-
打印日志
// 点击绑定 E/JustDo23: FirstService --> onCreate() E/JustDo23: FirstService --> onBind() E/JustDo23: DownloadBinder --> startDownload() E/JustDo23: DownloadBinder --> getProgress() // 点击解绑绑 E/JustDo23: FirstService --> onDestroy()
- 进行绑定服务时传递的标志位需要注意会影响生命周期函数
- 这里
BIND_AUTO_CREATE
标志位在绑定时执行了onCreate()
方法而不执行上边onStartCommande()
方法 - 一旦绑定成功,再次进行绑定就不会执行任何方法
- 解绑之后活动随即会被销毁
- 任何一个服务在整个应用程序范围内都是通用的,可以和任何一个活动进行绑定,获得相同的 Binder 对象。
-
生命周期图
-
生命周期整理
- 在任何位置调用
Context
的startService()
方法,服务启动并回调onStartCommand()
方法。如果服务之前没有创建则onCreate()
方法先于onStartCommand()
方法。服务启动后便会一直保持运行状态,虽然每次调用startService()
方法后都会回调onStartCommand()
方法,但实际上服务只会存在一个实例。因此,只需调用一次stopService()
方法或stopSelf()
方法,服务就会停止。 - 调用
Context
的bindService()
方法获取一个服务的持久连接,并回调onBind()
方法。类似地,如果服务之前没有创建则onCreate()
方法先于onbind()
方法。之后调用方获取 IBinder 对象可以用于通信。如果再次绑定不会回调onCreate()
和onbind()
方法。只要调用方和服务之间的连接没有断开,服务就一直保持运行状态。 - 当调用
startService()
方法启动服务时,则调用stopService()
方法来停止并销毁服务。当调用bindService()
方法绑定并启动服务时,则调用unbindService()
方法来解绑并停止并销毁服务。如果对一个服务既调用了startService()
方法又调用了bindService()
方法,那如何销毁服务?根据 Android 系统的机制,一个服务只要被启动或者被绑定之后,就会一直处于运行状态,必须要让以上两种条件同时满足才能销毁服务。因此这种情况下需要同时调用stopService()
方法和unbindService()
方法,onDestroy
方法才会执行。
- 在任何位置调用
-
前台服务
- 后台服务的系统优先级比较低,当系统内存不足的情况下,有可能会回收掉正在后台运行的服务。
- 前台服务和普通服务最大的区别在于,它会一直有一个正在运行的图标在系统的状态栏显示,下拉可以看到更加详细的信息。
public class ForegroundService extends Service { @Override public void onCreate() { super.onCreate();// 构建一个通知 Intent intent = new Intent(this, MainActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(this, 23, intent, 0); Notification notification = new NotificationCompat.Builder(this) .setContentTitle("ForegroundService") .setContentText("This the foreground service") .setWhen(System.currentTimeMillis()) .setSmallIcon(R.mipmap.ic_launcher) .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_mountain)) .setContentIntent(pendingIntent) .build(); startForeground(22, notification);// 前台运行服务 } }
- 前台服务需要创建一个通知
- 调用
startForeground()
方法设置前台
-
IntentService
- 服务中的代码默认运行在主线程中,因此不能直接在服务中执行耗时操作而需要多线程技术,线程停止需要杀死服务。为了简单地创建一个异步的且会自动停止的服务,Android 中提供了 IntentService 类。
public class FirstIntentService extends IntentService { public FirstIntentService() { super("FirstIntentService");// 父类含参构造。参数用来命名工作线程。 } /** * 运行在子线程中,运行结束后销毁服务。每次只处理一个 Intent。 * * @param intent 意图 */ @Override protected void onHandleIntent(Intent intent) { LogUtils.e("FirstIntentService --> onHandleIntent()"); LogUtils.e("FirstIntentService --> Thread id is " + Thread.currentThread().getId()); LogUtils.e("FirstIntentService --> Thread name is " + Thread.currentThread().getName()); } @Override public void onDestroy() { super.onDestroy(); LogUtils.e("FirstIntentService --> onDestroy()"); } }
- 需要注意构造方法必须要调用父类构造传递当前线程的名称
- 在
onHandleIntent()
方法中执行具体的逻辑,运行于子线程。 - IntentService 集开启线程和自动停止服务于一身。
- 完整的下载案例
- 接口回调
- 状态封装,每次写入数据是判断状态。
- 通过一次网络请求获取文件的大小。
- 通过设置网络请求头实现断点续传,写入文件时先跳过已下载的字节。
- 注意文件路径的使用,取消则删除文件。
- 同时调用
startService()
和bindService()
方法来启动和绑定服务。这一点至关重要,因为启动服务可以保证服务一直在后台运行,绑定服务则可以让活动与服务进行通信。 - 活动销毁同时需要进行服务的解绑,不然可能会造成内存泄露。
- 定时关机与开机的功能如何实现。
- Android 中的线程需要更加深入了解。
- 主线程的一些运行机制及原理需要了解。
- 编程**。
- 更多实践。
- 基于位置的服务 Location Based Service 简称 LBS 主要的工作原理就是利用无线电通讯网络和 GPS 等定位方式来确定出移动设备所在的位置。
- 核心是确定位置,通常有两种方式:一种是通过 GPS 定位,一种是通过网络定位。
- GPS 定位的工作原理是基于手机内置的 GPS 硬件直接和卫星交互来获取当前的经纬度信息,这种方式精确度非常高。缺点是只能在室外使用,室内基本无法接收到卫星的信号。
- 网络定位的工作原理是根据手机当前网络附近的三个基站进行测速,以此计算出手机和每个基站之间的距离,再通过三角定位确定出一个大概的位置,这种方式精确度一般,优点是室内外均可使用。
- GPS 定位不需要网络。
-
获取签名文件的 SHA1 指纹
-
在
Android Studio
右侧工具栏Gradle
选择项目名
选择:app
选择Tasks
选择android
双击signingReport
在控制台输出 SHA1 指纹。 -
使用命令
$ keytool -list -v -keystore <签名文件>
-
-
要使用 GPS 定位必须要用户在设置中自主选择打开后才可以。
-
并不需要担心一旦启用 GPS 定位功能后,手机的电量就会直线下滑,这只是表明你已经同意让应用程序来对你的手机进行 GPS 定位了,但只有当定位操作真正开始的时候,才会影响到手机的电量。
-
分支
-
分支的主要作用就是在现有代码的基础上开辟一个分叉口,使得代码可以在主干线和分支线上同时进行开发,且相互之间不会影响。
-
查看分支
$ git branch
-
创建分支
$ git branch name
指定新分支名字
-
切换分支
$ git checkout name
指定名字进行切换
-
合并分支
$ git checkout master $ git merge dev
首先切换到 master 分支然后将 dev 分支内容合并到 master 分支
-
删除分支
$ git branch -D name
指定名字进行删除
-
-
与远程版本库协作
-
提交代码
$ git push origin master
其中
origin
指定的是远程仓库的 Git 地址而master
指定的是同步到哪一个分支
-
-
从远程仓库拉去
方式一:
-
拉去远程仓库
$ git fetch origin master
将远程仓库代码同步到本地,但是同步下来的代码并不会合并到任何分支,而是会存放到一个
orgin/master
分支上,这时可以通过diff
命令来查看修改的地方 -
查看修改的地方
$ git diff origin/master
之后使用
merge
命令将orgin/master
分支合并到主分支 -
合并
$ git merge origin/master
方式二:
-
pull 命令
$ git pull origin master
相当于
fetch
和merge
两个命令放在一起执行了
-
- 百度定位更多方法查看官方文档。
- Git 分支使用很重要。
Material Design 是由谷歌的设计工程师基于传统的优秀的设计原则,结合丰富的创意和科学技术所发明的一套全新的界面设计语言。
-
主题
<!-- Base application theme. --> <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <!-- Customize your theme here. --> <item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorAccent">@color/colorAccent</item> </style>
- 主题
Theme.AppCompat.Light.DarkActionBar
是一个带有ActionBar
的深色主题。 - 主题
Theme.AppCompat.NoActionBar
是一个不带有ActionBar
的深色主题,它会将界面的主体颜色设成深色,陪衬颜色设成浅色。 - 主题
Theme.AppCompat.Light.NoActionBar
是一个不带有ActionBar
的浅色主题,它会将界面的主体颜色设成浅色,陪衬颜色设成深色。
- 主题
-
颜色
-
属性
- 属性
colorAccent
不只是用来指定一个按钮的颜色,而是更多的表达了一个强调的意思,比如一个控件的选中状态也会使用colorAccent
的颜色。
- 属性
-
布局
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="@color/colorPrimary" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> </FrameLayout>
- 使用
xmlns:app
指定一个命名空间之后可以使用app:attribute
之指定相关的属性。 - 全局主题是淡色主题,因此 Toolbar 是淡色但其上面的各种元素会自动使用深色,这是为了和主体颜色区别开。
- 局部使用
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
指定 Toolbar 单独使用深色主题。 - 使用
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
指定弹出菜单为淡色主题。
- 使用
-
使用
public class ToolbarActivity extends BaseActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_toolbar); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);// 找控件 toolbar.setTitle("JJFly");// 修改标题 setSupportActionBar(toolbar);// 设置 } }
-
修改标题
-
方式一
功能清单中添加标签属性
android:label="Just"
-
方式二
toolbar.setTitle("JJFly");// 修改标题
-
-
添加菜单
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <item android:id="@+id/menu_backup" android:icon="@mipmap/ic_backup" android:title="Backup" app:showAsAction="always" /> <item android:id="@+id/menu_delete" android:icon="@mipmap/ic_delete" android:title="Delete" app:showAsAction="ifRoom" /> <item android:id="@+id/menu_settings" android:icon="@mipmap/ic_settings" android:title="Settings" app:showAsAction="never" /> </menu>
- 使用属性
app:showAsAction
来指定按钮的显示位置- 值
always
表示永远显示在 Toolbar 中,如果空间不够则不显示 - 值
ifRoom
表示空间足够则显示在 Toolbar 中,空间不够则不显示 - 值
never
表示永远显示在菜单中,不在 Toolbar 显示
- 值
- 使用属性
-
显示及事件
@Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_toolbar, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_backup: ToastUtil.showShortToast(this, "Click Backup"); break; case R.id.menu_delete: ToastUtil.showShortToast(this, "Click Delete"); break; case R.id.menu_settings: ToastUtil.showShortToast(this, "Click Settings"); break; } return true; }
-
DrawerLayout
- 首先它是一个布局,在布局中允许放入两个直接子控件
- 第一个子控件是主屏幕中显示的内容
- 第二个子控件是滑动菜单中显示的内容
<?xml version="1.0" encoding="utf-8"?> <android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/drawerLayout" android:layout_width="match_parent" android:layout_height="match_parent"> </android.support.v4.widget.DrawerLayout>
- 第二个子控件中必须手动指定
android:layout_gravity="start"
属性来指定告诉DrawerLayout
滑动菜单是在屏幕的左边还是右边。
-
代码控制菜单
public class DrawerLayoutActivity extends BaseActivity { private DrawerLayout drawerLayout; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_drawer_layout); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);// 找控件 toolbar.setTitle("DrawerLayout");// 修改标题 setSupportActionBar(toolbar);// 设置 drawerLayout = (DrawerLayout) findViewById(R.id.drawerLayout); ActionBar actionBar = getSupportActionBar();// 获取相应的 toolbar if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true);// 允许导航按钮显示 actionBar.setHomeAsUpIndicator(R.mipmap.ic_menu);// 设置导航按钮的图标 } } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home:// 处理点击事件 drawerLayout.openDrawer(GravityCompat.START);// 打开菜单 break; } return true; } }
-
NavigationView
-
导入依赖库
compile 'com.android.support:design:25.3.1'// Material Design compile 'de.hdodenhof:circleimageview:2.1.0'// 圆形 ImageView
-
-
菜单项
-
菜单分组
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <group android:checkableBehavior="single"> <item android:id="@+id/menu_nav_call" android:icon="@mipmap/nav_call" android:title="Call" /> </group> </menu>
- 使用
<group>
标签进行分组 - 使用
android:checkableBehavior="single"
属性指定行为single
表示组中所有菜单项只能单选all
表示组中所有菜单项都能选中none
表示组中所有菜单项都不能选中
- 使用
-
-
头部布局
-
头部布局可以随意定制
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="180dp" android:background="?attr/colorPrimary" android:padding="10dp"> <de.hdodenhof.circleimageview.CircleImageView android:id="@+id/civ_head" android:layout_width="70dp" android:layout_height="70dp" android:layout_centerInParent="true" android:src="@mipmap/nav_icon" /> </RelativeLayout>
- 固定高度
180dp
是一个比较适合的高度
- 固定高度
-
-
使用 NavigationView
-
将
DrawerLayout
第二个子控件指定为NavigationView
<android.support.design.widget.NavigationView android:id="@+id/navigationView" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="start" app:headerLayout="@layout/navigation_header" app:menu="@menu/menu_navigation" />
-
使用
app:headerLayout
属性指定想要的头部布局 -
使用
app:menu
属性指定显示的菜单
-
-
代码控制
navigationView = (NavigationView) findViewById(R.id.navigationView);// 找控件 navigationView.setCheckedItem(R.id.menu_nav_call);// 设置选中 navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {// 设置菜单选中监听 @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { drawerLayout.closeDrawers(); return true; } });
-
悬浮按钮 FloatingActionButton
<android.support.design.widget.FloatingActionButton android:id="@+id/floatingActionButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_margin="16dp" android:src="@mipmap/ic_done" app:elevation="8dp" />
-
属性
- 控件
FloatingActionButton
默认使用colorAccent
来作为按钮的颜色 - 属性
app:elevation
指定按钮的高度,高度值越大投影范围越大投影效果越淡,高度值越小投影范围越小投影效果越浓。
- 控件
-
简介
Snackbar
并不是Toast
的替代品Toast
只能告诉用户现在发生了什么事情,无法进行交互。Snackbar
告诉用户的同时可以在提示中添加一个可交互的按钮
-
Snackbar
floatingActionButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Snackbar.make(view, "Data deleted", Snackbar.LENGTH_LONG) .setAction("Undo", new View.OnClickListener() { @Override public void onClick(View v) { ToastUtil.showShortToast(v.getContext(), "Data restored"); } }) .show(); } });
-
方法
make(View view, CharSequence text, int duration)
方法创建控件- 第一个参数是当前界面布局中任意一个 View 都可以,利用它自动查找最外层布局
- 第二个参数为提示信息
- 第三个参数指定显示时长
setAction(CharSequence text, final View.OnClickListener listener)
方法添加交互按钮- 第一个参数为交互按钮文本
- 第二个参数为交互的点击事件
-
简介
CoordinatorLayout
是一个加强版的FrameLayout
CoordinatorLayout
可以监听其所有子控件的各种事件,然后自动做出最为合理的响应。
-
替换
FrameLayout
<android.support.design.widget.CoordinatorLayout android:layout_width="match_parent" android:layout_height="match_parent"> </android.support.design.widget.CoordinatorLayout>
-
解决遮挡问题
首先
CoordinatorLayout
可以监听其所有子控件的各种事件,但是Snackbar
好像并不是CoordinatorLayout
的子控件,为什么它却可以被监听?其实道理很简单,在
Snackbar
的make()
方法中传入的第一个参数,就指定了Snackbar
是基于哪个View
来触发的,传递的FloatingActionButton
是CoordinatorLayout
的子控件,因此这个事件就理所应当能被监听到了。
-
添加依赖库
compile 'com.android.support:cardview-v7:25.3.1'// 卡片式布局
-
卡片式布局 CardView
<android.support.v7.widget.CardView xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/cardView" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="10dp" app:cardCornerRadius="8dp" app:cardElevation="8dp"> </android.support.v7.widget.CardView>
- 属性
app:cardCornerRadius
指定卡片布局的圆角度数 - 属性
app:cardElevation
指定卡片布局的高度
- 属性
-
问题产生
- 出现问题是
RecycleView
将Toolbar
遮挡。之前提到CoordinatorLayout
是一个加强版的FrameLayout
但其并非直接继承自FrameLayout
而是直接继承ViewGroup
的,因而所有子控件在不进行明确定位的情况下,默认都摆放在布局的左上角,从而产生遮挡问题。 - 布局
AppBarLayout
实际上是一个垂直方向的LinearLayout
它在内部做了很多滚动事件的封装。
- 出现问题是
-
问题解决
- 将
Toolbar
嵌套到AppBarLayout
中 - 给
RecycleView
指定一个布局行为
- 将
-
AppBarLayout
<?xml version="1.0" encoding="utf-8"?> <android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/drawerLayout" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.design.widget.CoordinatorLayout android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.design.widget.AppBarLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="@color/colorPrimary" app:layout_scrollFlags="scroll|enterAlways|snap" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> </android.support.design.widget.AppBarLayout> <android.support.v7.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" /> </android.support.design.widget.CoordinatorLayout> </android.support.v4.widget.DrawerLayout>
-
属性
- 属性
app:layout_behavior
指定一个布局行为 - 属性
app:layout_scrollFlags
指定当AppBarLayout
接收到滚动事件时候,它内部的子控件行为。- 值
scroll
表示RecyclerView
向上滚动时Toolbar
会跟着一起向上滚动并实现隐藏。 - 值
enterAlways
表示RecyclerView
向下滚动时Toolbar
会跟着一起向下滚动并重新显示。 - 值
snap
表示当Toolbar
还没有完全隐藏或显示的时候会根据当前滚动的距离自动选择是隐藏还是显示。
- 值
- 属性
-
包裹需要刷新控件
<android.support.v4.widget.SwipeRefreshLayout android:id="@+id/srl_car" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v7.widget.RecyclerView android:id="@+id/rv_car" android:layout_width="match_parent" android:layout_height="match_parent" /> </android.support.v4.widget.SwipeRefreshLayout>
-
设置刷新监听等
@Override protected void onCreate(Bundle savedInstanceState) { srl_car = (SwipeRefreshLayout) findViewById(R.id.srl_car);// 找控件 srl_car.setColorSchemeResources(R.color.colorPrimary, R.color.colorAccent);// 设置颜色 srl_car.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {// 刷新监听 @Override public void onRefresh() {// 主线程 refreshCars(); } }); } private void refreshCars() { new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(6000); } catch (InterruptedException e) { e.printStackTrace(); } runOnUiThread(new Runnable() { @Override public void run() { ToastUtil.showShortToast(SwipeRefreshLayoutActivity.this, "Refresh Success"); srl_car.setRefreshing(false);// 停止刷新 } }); } }).start(); }
-
可折叠式标题栏
- 注意
CollapsingToolbarLayout
是不能独立存在的,只能作为AppBarLayout
的直接子布局使用。 - 同时
AppBarLayout
又必须是CoordinatorLayout
的子布局。 - 其实
CollapsingToolbarLayout
在折叠之后就是一个普通的Toolbar
。
- 注意
-
使用方法
<?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.design.widget.AppBarLayout android:id="@+id/appBarLayout" android:layout_width="match_parent" android:layout_height="230dp"> <android.support.design.widget.CollapsingToolbarLayout android:id="@+id/collapsingToolbarLayout" android:layout_width="match_parent" android:layout_height="match_parent" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" app:contentScrim="@color/colorPrimary" app:layout_scrollFlags="scroll|exitUntilCollapsed"> <ImageView android:id="@+id/iv_hide" android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="centerCrop" app:layout_collapseMode="parallax" /> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:layout_collapseMode="pin" /> </android.support.design.widget.CollapsingToolbarLayout> </android.support.design.widget.AppBarLayout> <android.support.v4.widget.NestedScrollView android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <android.support.v7.widget.CardView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="15dp" android:layout_marginLeft="15dp" android:layout_marginRight="15dp" android:layout_marginTop="35dp" app:cardCornerRadius="8dp"> <TextView android:id="@+id/tv_content" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="10dp" /> </android.support.v7.widget.CardView> </LinearLayout> </android.support.v4.widget.NestedScrollView> <android.support.design.widget.FloatingActionButton android:id="@+id/fab_content" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="16dp" android:src="@mipmap/ic_comment" app:layout_anchor="@id/appBarLayout" app:layout_anchorGravity="bottom|end" /> </android.support.design.widget.CoordinatorLayout>
-
属性
- 属性
app:contentScrim
用于指定CollapsingToolbarLayout
在趋于折叠状态以及折叠之后的背景颜色。 - 属性
app:layout_scrollFlags
用于指定AppBarLayout
接收到滚动事件时候,它内部的子控件行为。- 值
scroll
表示CollapsingToolbarLayout
随着滚动一起滚动。 - 值
exitUntilCollapsed
表示当CollapsingToolbarLayout
随着滚动完成折叠之后就保留在界面上,不再移出屏幕。
- 值
- 属性
app:layout_collapseMode
用于指定当前控件在CollapsingToolbarLayout
折叠过程中的折叠模式- 值
pin
表示在折叠的过程中位置始终保持不变 - 值
parallax
表示在折叠的过程中产生一定的错位偏移
- 值
- 属性
app:layout_anchor
指定了一个锚点 - 属性
app:layout_anchorGravity
指定了悬浮按钮的位置
- 属性
-
NestedScrollView
- 可以认为
NestedScrollView
是一个加强版的ScrollView
- 内部增加了嵌套响应滚动事件的功能
- 类似
ScrollView
他们的内部都只允许存在一个直接子布局
- 可以认为
-
界面
/** * 12.7.1 可折叠式标题栏 * * @since 2017年08月20日 */ public class CollapsingToolbarLayoutActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_collapsing_toolbar_layout); // 可折叠标题栏 CollapsingToolbarLayout collapsingToolbarLayout = (CollapsingToolbarLayout) findViewById(R.id.collapsingToolbarLayout); collapsingToolbarLayout.setTitle("Fruit Detail");// 设置标题 // 图片 ImageView iv_hide = (ImageView) findViewById(R.id.iv_hide); Glide.with(this).load(R.mipmap.ic_orange).into(iv_hide); // Toolbar Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); } // 内容详情 TextView tv_content = (TextView) findViewById(R.id.tv_content); tv_content.setText("orange"); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: finish(); break; } return true; } }
-
只能在 Android 5.0 系统及之后的系统进行设置。
-
借助属性
android:fitsSystemWindows="true"
指定控件会出现在系统状态里。- 之前示例需要将 ImageView 及其所有父布局都设置上这个属性。
-
在主题中将状态栏颜色指定成透明颜色
-
创建
values-v21
文件夹及styles.xml
文件 -
指定属性
android:statusBarColor
为透明颜色<!-- 状态栏 --> <style name="StatusBarTheme" parent="FullScreen"> <item name="android:statusBarColor">@android:color/transparent</item> </style>
-
文件
res/values/styles.xml
中不需要指定属性
-
- 对 Material Design 中的新控件多尝试多练习。
- 了解 Material Design 的设计思维和设计理念。
public class FirstLineApplication extends Application {
private static Context context;
@Override
public void onCreate() {
super.onCreate();
context = getApplicationContext();
}
public static Context getContext() {
return context;
}
}
-
简介
Serializable
是序列化的一种,表示将一个对象转换成可存储或可传输的状态。序列化后的对象可以在网络上进行传输,也可以存储到本地。 -
使用
实现
Serializable
接口就可以了。 -
获取
Value value = (Value) getIntent().getSerializableExtra("key");// 根据键获取值需要强制转换
-
简介
Parcelable
是将一个完整的对象进行分解,而分解后的每一部分都是 Intent 所支持的数据类型,这样也就实现对象传递的功能了。 -
使用
import android.os.Parcel; import android.os.Parcelable; public class Car implements Parcelable { private String name; private int imageId; @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(this.name);// 写出 name dest.writeInt(this.imageId);// 写出 imageId } protected Car(Parcel in) { this.name = in.readString();// 读 name this.imageId = in.readInt();// 读 imageId } public static final Parcelable.Creator<Car> CREATOR = new Parcelable.Creator<Car>() { @Override public Car createFromParcel(Parcel source) { return new Car(source); } @Override public Car[] newArray(int size) { return new Car[size]; } }; }
- 必须重写
describeContents()
方法和writeToParcel()
方法describeContents()
方法返回 0writeToParcel()
方法将实体类中的字段逐个写出
- 提供一个名为
CREATOR
的常量,创建Parcelable.Creator
接口的一个实现,重写了createFromParcel()
方法和newArray()
方法createFromParcel()
方法中读取所写入的字段,同时读取顺序一定要和写入的顺序相同newArray()
方法根据参数中size
返回数组
- 必须重写
-
获取
Value value = (Value) getIntent().getParcelableExtra("key");// 根据键获取值需要强制转换
-
区别
Serializable
方式较为简单,但由于会把整个对象进行序列化,因此效率会比Parcelable
方式低一些,所以通常情况下推荐使用Parcelable
方式。
public class LogUtils {
public static final int VERBOSE = 1;
public static final int DEBUG = 2;
public static final int INFO = 3;
public static final int WARN = 4;
public static final int ERROR = 5;
public static final int NOTHING = 6;
public static int level = VERBOSE;
public static void v(String tag, String msg) {
if (level <= VERBOSE) {
Log.v(tag, msg);
}
}
public static void d(String tag, String msg) {
if (level <= DEBUG) {
Log.d(tag, msg);
}
}
public static void i(String tag, String msg) {
if (level <= INFO) {
Log.i(tag, msg);
}
}
public static void w(String tag, String msg) {
if (level <= WARN) {
Log.w(tag, msg);
}
}
public static void e(String tag, String msg) {
if (level <= ERROR) {
Log.e(tag, msg);
}
}
}
- 方式一:
- 打断点
- 调试运行
- 逐行运行
- 方式二:
- 随时进入调试
- 点击工具栏
Attach debugger to Android process
Android 中的定时任务一般有两种实现方式,一种是使用 Java API 里提供的 Timer 类,一种是使用 Android 的 Alarm 机制。这两种方式在多数情况下都能实现类似的效果,但 Timer 有一个明显的短板,它并不太适用于那些需要长期在后台运行的定时任务。我们都知道,为了能让电池更加耐用,每种手机都会有自己的休眠策略,Android 手机就会在长时间不操作的情况下自动让 CPU 进入到睡眠状态,这就有可能导致 Timer 中的定时任务无法正常运行。而 Alarm 则具有唤醒 CPU 的功能,它可以保证在大多数情况下需要执行定时任务的时候 CPU 都能正常工作。需要注意,这里唤醒 CPU 和唤醒屏幕完全不是一个概念,千万不要产生混淆。
-
入门代码
AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);// 利用上下文获取管理对象 long triggerAtTime = SystemClock.elapsedRealtime() + 10 * 1000;// 10秒之后执行 Intent intent = new Intent(this, MainActivity.class);// 意图 PendingIntent pendingIntent = PendingIntent.getActivity(this, 23, intent, 0);// 延迟意图 alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTime, pendingIntent);// 启动计时
-
代码解释
- 上下文获取
AlarmManager
实例 - 调用
AlarmManager
的set()
方法设置一个定时任务- 第一个参数是一个整型参数,用于指定
AlarmManager
的工作类型。ELAPSED_REALTIME
表示让定时任务的触发时间从系统开机开始算起,但不会唤醒 CPU。ELAPSED_REALTIME_WAKEUP
表示让定时任务的触发时间从系统开机开始算起,但会唤醒 CPU。RTC
表示让定时任务的触发时间从 1970年01月01日 00:00:00 开始算起,但不会唤醒 CPU。RTC_WAKEUP
表示让定时任务的触发时间从 1970年01月01日 00:00:00 开始算起,但会唤醒 CPU。
- 第二个参数是一个长整型参数,用于指定定时任务触发的时间,以毫秒为单位。
- 第三个参数是一个 PendingIntent 一般定时启动广播或服务。
- 第一个参数是一个整型参数,用于指定
SystemClock.elapsedRealtime()
方法获取系统开机至今所经历的时间毫秒数SystemClock.currentThreadTimeMillis()
方法获取从 1970年01月01日 00:00:00 开始至今所经历的时间毫秒数
- 上下文获取
-
实现长时间后台定时运行的服务
- 服务
public class LongRunningService extends Service { @Nullable @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 alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE); int anHour = 1 * 1000;// [一小时]一分钟的毫秒数 long triggerAtTime = SystemClock.elapsedRealtime() + anHour;// 开机时间 + 指定时间 Intent intents = new Intent(this, LongRunningService.class);// 指定自己 PendingIntent pendingIntent = PendingIntent.getService(this, 0, intents, 0); alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTime, pendingIntent);// 定时启动服务自己 return super.onStartCommand(intent, flags, startId); } }
- 启动
Intent intent = new Intent(this, LongRunningService.class); startService(intent);
- 子线程
在
onStartCommand()
方法中开启了一个子线程,这个是子线程是有必要的,因为逻辑操作也是需要耗时的,如果放在主线程中执行可能会对定时任务的准确性造成轻微的影响。 -
注意
从 Android 4.4 系统开始,Alarm 任务的触发时间将会变得不准确,有可能会延迟一段时间后任务才能得到执行。这并不是个 Bug ,而是系统在耗电性能方面进行的优化。系统会自动检测目前有多少 Alarm 任务存在,然后将触发时间相近的几个任务放在一起执行,这就可以大幅度减少 CPU 被唤醒的次数,从而有效延长电池的使用时间。
当然,如果你要求 Alarm 任务的执行时间必须准确无误,Android 仍然提供了解决方案。使用 AlarmManager 的
setExact()
方法来替代set()
方法,就基本上可以保证任务能够准时执行了。
-
起因
虽然 Android 每个系统版本都在手机电量方面努力优化,但一直没能解决后台服务泛滥,手机电量消耗过快的问题。于是在 Android 6.0 系统中,加入了一个全新的 Doze 模式,从而可以大幅度地延长电池的使用寿命。
-
概况
当用户的设备是 Android 6.0 或以上时,如果设备未插接电源,处于静止状态( Android 7.0 删除这一条件 ),且屏幕关闭了一段时间之后,就会进入到 Doze 模式。在 Doze 模式下,系统会对 CPU 、网络、Alarm 等活动进行限制,从而延长了电池的使用寿命。
当然系统并不会一直处于 Doze 模式,而是会间歇性地退出 Doze 模式一小段时间,在这段时间中,应用就可以去完成它们的同步操作、Alarm 任务等等。
-
工作过程图
- 可以看到,随着设备进入 Doze 模式的时间越长,间歇性地退出 Doze 模式的时间间隔也会越长。因为如果设备长时间不使用的话,是没必要频繁退出 Doze 模式来执行同步等操作的,Android 在这些细节上的把控使得电池寿命进一步得到了延长。
-
在 Doze 模式下受限功能
- 网络访问被禁止
- 系统忽略唤醒 CPU 或者屏幕操作
- 系统不再执行 WiFi 扫描
- 系统不再执行同步服务
- Alarm 任务将会在下次退出 Doze 模式的时候执行
-
注意
- 在 Doze 模式下 Alarm 任务将会变得不准时
- 特殊需求使 Alarm 任务在 Doze 模式下必须正常执行
alarmManager.setAndAllowWhileIdle();
alarmManager.setExactAndAllowWhileIdle();
-
官方介绍
-
生命周期
- 在多窗口模式并不会改变原有的生命周期,将最近与用户交互的活动设置为运行状态,另一个活动设置为暂停状态。
// 启动界面 JustDo23: --> onCreate() JustDo23: --> onStart() JustDo23: --> onResume() // 点击 OverView 按钮 JustDo23: --> onPause() JustDo23: --> onStop() // 拖动至多窗口 JustDo23: --> onDestroy() JustDo23: --> onCreate() JustDo23: --> onStart() JustDo23: --> onResume() JustDo23: --> onPause() JustDo23: --> onResume() // 选择另一多窗口 JustDo23: --> onPause() // 选择当前窗口 JustDo23: --> onResume()
- 类似横竖屏界面被销毁并重新创建
- 界面在运行状态和暂停状态之间切换
- 逻辑:最好不要在活动
onPause()
方法中去处理视频播放器的暂停逻辑,而是应该在onStop()
方法中去处理暂停,并且在onStart()
方法中恢复视频的播放。
-
避免重新创建
<activity android:name=".chapter13.MultiWindowActivity" android:configChanges="orientation|keyboardHidden|screenSize|screenLayout" />
- 不管是横竖屏还是多窗口都不会销毁重新创建,而是会将屏幕发生变化的事件通知到 Activity 的
onConfigurationChanged()
方法中。
- 不管是横竖屏还是多窗口都不会销毁重新创建,而是会将屏幕发生变化的事件通知到 Activity 的
-
禁用
-
在
AndroidManifest.xml
中的<application />
标签中<application android:resizeableActivity="false"/>
-
除此之外必须设置
targetSdkVersion 24
及以上,如果是 24 以下的版本仍需在 Activity 添加配置<activity android:screenOrientation="portrait" />
-
-
简介
Java 8 中非常有特色的功能。Lambda 表达式本质上是一种匿名方法,它没有方法名,没有访问修饰符和返回值类型,使用他来编写代码将会更加简洁。
-
配置
-
app/build.gradle
文件android { jackOptions.enabled = true;// 支持 Lambda 表达式 } compileOptions {// 编译选项 sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } }
-
-
使用
-
启动线程
-
Java 7
new Thread(new Runnable() { @Override public void run() { // 处理具体逻辑 } }).start();
-
Java 8
new Thread(() -> { // 处理具体逻辑 }).start();
-
-
实例化 Runnable
-
Java 7
private Runnable runnable = new Runnable() { @Override public void run() { // 处理具体逻辑 } };
-
Java 8
private Runnable runnableScroll = () -> { // 处理具体逻辑 };
-
-
设置点击事件
-
Java 7
findViewById(R.id.bt_click).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 处理具体逻辑 } });
-
Java 8
findViewById(R.id.bt_click).setOnClickListener(v -> { // 处理具体逻辑 });
-
-
-
规律
其实只要是符合接口中只有一个待实现方法这个规则的功能,都是可以使用 Lamabda 表达式来编写的。
- 序列化两种方式
Serializable
和Parcelable
原理及异同。 - 定时任务更多知识需要学习使用。
- 针对 Doze 模式查阅官方文档。
-
功能需求
- 罗列全国所有省市县
- 查看全部任意城市天气
- 自由切换城市
- 提供手动更新及后台自动更新
-
可行性
- 省市县接口
- 彩云天气接口
- 和风天气接口
- 注册 GitHub 账号
- 创建仓库
- 本地项目与远程仓库连接
- 数据库使用 LitePal 方便快捷
-
定义 Gson 实体类
public class Basic { @SerializedName("city")// 字段名 public String cityName;// 新名字 @SerializedName("id") public String weatherId; }
-
动态加载布局
-
状态栏全屏
private void initStatusBar() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {// Android 5.0 View decorView = getWindow().getDecorView(); decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);// 设置标记全屏 getWindow().setStatusBarColor(Color.TRANSPARENT);// 设置透明 } }
-
获取必应每日一图
- 接口 http://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1
- 接口
http://www.bing.com/az/hprichbg/rb/BasongcuoNP_ZH-CN9819436811_1920x1080.jpg
- 启动服务
- 定时任务
- 添加设置功能
- 优化软件界面
- 允许选择多个城市同时观察
- 提供更加完成的天气信息
- 主题夜间模式等
- 添加有趣的自定义控件
- 添加资讯功能
- 在使用
Material Design
时总是嵌套很多层,如何优化。 - 学习知识更重要的是灵活运用,理解掌握实践开发。
-
简介
-
生成签名
- 导航栏
Build
点击Generate Signed APK
- 下一步可以选择
Create new…
创建新的签名文件 - 选择
choose existing…
指定签名文件 - 输入密码指定位置
Finish
生成签名 APK
- 导航栏
-
使用 Gradle 生成
-
配置
app/build.gradle
文件android { defaultConfig {// 默认配置 } signingConfigs {// 签名配置 config { storeFile file('xxx.jks')// 签名文件路径 storePassword 'Password'// 签名密码 keyAlias 'Alias'// 签名别名 keyPassword 'Password'// 别名密码 } } buildTypes {// 构建类型 release {// 发布 signingConfig signingConfigs.config// 指定之前的签名配置 } } }
-
点击右侧工具栏的
Gradle
点击项目名
点击:app
点击Tasks
点击build
clean
清理当前项目assembleDebug
生成测试版 APKassembleRelease
生成正式版 APKassemble
同时生成正式版和测试版
-
生成文件在
app/build/outputs/apk
目录下app-release.apk
正式签名的 APK
-
-
提高安全性
-
将密码配置在根目录下
gradle.properties
文件中# 签名信息 KEY_PATH=xxx.jks KEY_PASS=Password ALIAS_NAME=Alias ALIAS_PASS=Password
-
修改
app/build.gradle
文件signingConfigs {// 默认配置 config { storeFile file(KEY_PATH)// 签名文件路径 storePassword KEY_PASS// 签名密码 keyAlias ALIAS_NAME// 签名别名 keyPassword ALIAS_PASS// 别名密码 } }
-
-
配置多个渠道
android { defaultConfig {// 默认配置 } productFlavors {// 多渠道配置 qihoo {// 奇虎360 applicationId "com.just.first.qihoo" } baidu {// 百度 applicationId "com.just.first.baidu" } } }
- 在
productFlavors
闭包中添加渠道配置。 - 渠道名的闭包中可以复写
defaultConfig
中的任何一个属性。
- 在
-
差异文件
-
修改应用名
-
在
app/src/baidu/res/values
目录下新建strings.xml
文件 -
指定该版本项目名称
<resources> <string name="app_name">BaiDuCode</string> </resources>
-
-
打包
- 右侧
Gradle Tasks
列表中多出几个新的Task
assembleBaidu
只生成百度渠道assembleQihoo
只生成奇虎渠道
- 导航栏
Build
点击Generate Signed APK
- 提示选择渠道单选或者多选
- 右侧
-
安装
$ adb install xxx.apk
- 个人开发者
- 填写个人资料信息
- 发布应用
- 谷歌收购了 AdMob 公司,是全球最早致力于移动设备上提供广告服务的公司之一。
- 可惜 AdMob 不适合国内开发者。
- 国内腾讯广告联盟(原广点通)特别专业。
- 注册需要身份证及银行卡照片等进行审核。
- 审核通过后在后台新建媒体。
- 接下来需要下载 SDK 完成新建媒体并进入等待审核状态。
- 审核通过后新建广告位。
- 进行 SDK 的接入。
- 升级版本进行重新发布。
- 签名流程
- 签名对齐
- 在
app/build.gradle
文件中的applicationId
与AndroidManifest.xml
文件中的package