Android 学习
第一行代码——Android(第2版)读书笔记
项目文件结构
上级目录 | 名称 | 说明 | 类型 |
---|---|---|---|
.gitignore | 版本控制文件 | 文件 | |
local.properties | 配置本机 android sdk 目录 | 文件 | |
build.gradle | 项目全局的构建脚本 | 文件 | |
settings.gradle | 项目引入文件 | 文件 | |
Source.iml | IDE 项目文件 | 文件 | |
gradlew | 执行 gradle linux 下使用 | 文件 | |
gradlew.bat | 执行 gradle windows 下使用 | 文件 | |
.gradle | 程序自动管理目录 | 目录 | |
.idea | 程序自动管理目录 | 目录 | |
gradle | gradle wrapper 配置文件 | 目录 | |
app | 代码、资源目录 | 目录 | |
app | app.iml | IDE 项目文件 | 目录 |
app | build.gradle | 项目构建脚本 | 文件 |
app | proguard-rules.pro | 项目混淆规则 | 文件 |
app | build | 编译时自动创建文件 | 目录 |
app | libs | 需要引用的第三方 jar 包 | 目录 |
app | src | 项目代码 | 目录 |
app\src | androidTest | android 测试用例 | 目录 |
app\src | test | 自动化测试代码 | 目录 |
app\src | main | 主代码目录 | 目录 |
app\src\main | AndroidManifest.xml | android 项目配置文件 | 文件 |
app\src\main | java | 代码目录 | 目录 |
app\src\main | res | 代码引用的资源 | 目录 |
AndroidManifest.xml
所有活动都要添加到 AndroidManifest.xml 中
<!-- .HelloWorl 表示相应的 java 类,package 引用包名,这里可以简写用.开头, MAIN 表示是主活动,LAUNCHER 表示启动项 -->
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.x.demo.xapplication">
<activity android:name=".HelloWorld">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</manifest>
视图类
public class HelloWorld extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 加载布局资源文件 Res\layout\activity_hello_world.xml
setContentView(R.layout.activity_hello_world);
}
}
资源文件夹
文件夹名格式 类型_分辨率, 类型有如下几种 drawable: 图片 , mipmap: 图标, values:字符串、样式、颜色 等配置
资源使用
res/values/strings.xml 文件中定义
<resources>
<string name="app_name">HelloWorld</string>
</resources>
// 代码中引用 R.string.app_name
// xml中引用 @string/app_name
// xml 增加一个 id
android:id="@+id/button_1" # 表示添加一个类型为 id 名称为 button_1 的资源
代码中使用 findViewById(R.id.button_1)
// 再比如 AndroidManifest.xml 文件
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme" />
build.gradle
编译配置文件
全局 build.gradle
buildscript {
repositories { ## 指定仓库
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1' ## 指定编译器,这是给 android 用的
}
}
allprojects {
repositories { ## 指定仓库
google()
jcenter()
}
}
app build.gradle
apply plugin: 'com.android.application' # 表明这是一个 app
# apply plugin: 'com.android.library' # 表明这是一个为模块
android {
compileSdkVersion 28 # 编译版本
defaultConfig {
applicationId "com.x.demo.xapplication" # 项目的包名
minSdkVersion 15 # 最低支持的系统版本
targetSdkVersion 28 # 充分测试的系统版本。系统根据这个版本决定运行时支持什么特性
versionCode 1 # 项目版本号
versionName "1.0" # 项目版本名
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false # 要不要对项目进行混淆
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' # 混淆规则,proguard-android.txt:系统提供的规则 proguard-rules.pro:当前项目中可自定义的规则
}
}
}
dependencies { # 定义当前项目的所有依赖包, 本地依赖,库依赖,远程依赖
implementation fileTree(dir: 'libs', include: ['*.jar']) # 本地依赖,将 libs 下所有 *.jar 的文件都编译进去
implementation 'com.android.support:appcompat-v7:28.0.0' # 远程依赖 com.android.support:公司名, appcompat-v7:组件名, 28.0.0: 版本号, 如果本地没有缓存,将从远程下载(应该就是全局 build.gradle 中定义的远程库了)
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
testImplementation 'junit:junit:4.12' # 测试使用
androidTestImplementation 'com.android.support.test:runner:1.0.2' # android 测试使用
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
# 还有一个库依赖,这里没有例子
}
日志
Log.v() # verbose
Log.d() # debug
Log.i() # info
Log.w() # warn
Log.e() # errro
Log.v(this.getLocalClassName(), "启动");
活动视图
添加按钮
// 如果多次绑定单击监听事件,则最后一次生效
@Override
protected void onCreate(Bundle savedInstanceState) {
// 1. 获得按钮
View button = this.findViewById(R.id.button_1);
// 2. 1 将 this 绑定到 click 事件上
button.setOnClickListener(this);
// 2. 2 new 一个 OnClickListener 对象绑定到 click 事件上
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 3.1 处理单击事件
HelloWorld.this.showToast(String.format("you clocked button 1:%s", this.getClass().getSimpleName()));
}
});
}
@Override
public void onClick(View v) {
// 3.2 处理单击事件
this.showToast(String.format("you clocked button 1:%s", this.getLocalClassName()));
}
添加菜单
// 初化菜单
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
// 处理菜单消息
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()){
case R.id.add_item: break;
case R.id.remove_item: break;
}
return super.onOptionsItemSelected(item);
}
运行一个 Intent
显式运行
Intent intent = new Intent(this, XmessageBox.class);
this.startActivity(intent);
隐式运行
定义一个活动
<activity android:name=".XmessageBox">
<intent-filter>
<action android:name="com.x.demo.xapplication.START"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="com.x.demo.xapplication.XSEARCH" />
</intent-filter>
</activity>
调用
Intent intent = new Intent("com.x.demo.xapplication.START"); # 定义 Intent 的 action 及 category , DEFAUTL 可以不用写
intent.addCategory("com.x.demo.xapplication.XSEARCH"); # 如果有多个 category 可以继续添加
this.startActivity(intent); # 启动
显示网页
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://www.baidu.com"));
this.startActivity(intent);
// 默认为浏览器响应,如果AndroidManifest加入如下配置, 则系统会提示使用者选择浏览器还是内置 activity 响应
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http" />
// 打电话
Intent intent = new Intent(Intent.ACTION_DIAL);
intent.setData(Uri.parse("tel:10086"));
传输数据
// 发送方
Intent intent = new Intent(this, XmessageBox.class);
intent.putExtra("xData", "hallowed.");
this.startActivity(intent);
// 接收方
Intent intent = this.getIntent();
String xdata = intent.getStringExtra("xData"); // 或 intent.getExtra("xData");
Log.v(this.getLocalClassName(), String.format("找到数据:%s", xdata ));
// 接收方可以实现, 创建一个启动辅助函数
public static Intent actionStart(Activity activity, String title, String message, int requestCode){
Intent intent = new Intent(activity, XmessageBox.class);
intent.putExtra("xTitle", title);
intent.putExtra("xMessage", message);
activity.startActivityForResult(intent, requestCode);
return intent;
}
返回数据
// 第一页
Intent intent = new Intent(this, XmessageBox.class); // 创建需要显示的活动
intent.putExtra("xData", "hallowed."); // 添加传输值
this.startActivityForResult(intent, 10012); // 要求完成后回调
/*** 活动关闭回调函数*/
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
switch (requestCode){
case 10012:
int xReturn = data.getIntExtra("xReturn", -1);
Log.d(this.getLocalClassName(), String.format("获取状态:%s 返回值:%s: ", resultCode, xReturn));
break;
}
super.onActivityResult(requestCode, resultCode, data);
}
// 第二页
Intent intent = this.getIntent();
intent.putExtra("xReturn", 12345); // 设置一个返回的内容
this.setResult(RESULT_OK, intent); // 设置返回值,只能是 int, 相当于 showdialog 的返回值
this.finish(); // 关闭窗口
活动周期
/** 创建时, 进行一些初始化 */
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); }
/** 不可见至可见的时候调用 */
@Override
protected void onStart() {
super.onStart(); }
/** 活动位于 task 栈顶,准备与用户交互时 */
@Override
protected void onResume() {
super.onResume(); }
/** 准备启动另外一个活动时,可以释放一些资源或保存一些状态 */
@Override
protected void onPause() {
super.onPause(); }
/** 活动不可见时调用。比如准备启动另外一个活动时。如果启动对话框,则不会被调用 */
@Override
protected void onStop() {
super.onStop(); }
/** 活动被销毁时调用 */
@Override
protected void onDestroy() {
super.onDestroy(); }
/** 活动被重新启用时 */
@Override
protected void onRestart() {
super.onRestart(); }
活动被回收
系统资源不足时,活动会被回收。活动在回收前会调用 onSaveInstanceState, 将相关值写入到参数中 outState.putXXX 之类的。然后在活动 onCreate 中读回相关值
启动模式
<!-- AndroidManifest.xml 中设置 -->
<activity android:name=".HelloWorld" android:launchMode="singleInstance">
getTaskId() 返回当前栈 id
standard: 默认模式。使用时创建,不活动时销毁
singleTop: 栈项的活动任务,再启动时使用自身。不活动时销毁
singleTask: 活动时如果栈中已经存在,则使用该实例,并把这个活动之上的活动全部出栈。
singleInstance: 会保存在单独的一个栈中。比如 A栈 中 活动1 调用, 在 B栈中创建 singleInstance 的活动2 ,再调用 A栈中的 活动3. 活动3 back 时是会抬到 活动1 的,再 back A栈中无活动,于是就到 B 栈。 再 back B 栈无活动,程序退出。
关闭程序
- 实现一个基类,维护一个列表,在 onCreate 时将自动加入到列表,onDestory 时将自身移出列表。在关闭时,调用列表中所有元素的 finish()
- 关闭自身进程 android.os.Process.killProcess(android.os.Process.myPid());
界面
常用属性
layout_width\layout_height: 定义宽高,
match_parent(fill_parent):占满整个父窗口
wrap_content: 刚好包住内容
android:layout_weight='1':定义控件在水平方向上的权重,定义后 layout_width 失效
android:hint="这是提示内容"
android:gravity: 文字排列方式
android:visibility="invisible":控制可见性 gone:不可见,不占空间; invisible:不可见,占空间; visible:可见
显示一个对话框
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
dialog.setTitle("Title");
dialog.setMessage("message");
dialog.setCancelable(false);
dialog.setPositiveButton("ok", listener);
dialog.setNegativeButton("cancel", listener);
dialog.show();
布局
-
可以使用 LinearLayout 、FrameLayout 等布局控件
-
可以自定义控件,然后使用
<include layout="@layout/title">
引入自定义控件 -
继承 LinearLayout(或别的 layout), 初始化时调用 LayoutInflater.form(context).inflate(R.layout.title, this) 将 this 与 布局进行捆绑
将自定义布局加入到活动页面中 , 然后可以在自定义类中实现一个函数,作为通用函数
listview
布局
```java
// 为 listview 准备数据,及数据对应的项的视图, android.R.layout.simple_list_item_1 为系统内置
ArrayAdapter
ListView listView = this.findViewById(R.id.xListView); listView.setAdapter(arrayAdapter); // 将数据绑定视图
// 如果要自定义 listview 中的项,则自定义一个 ArrayAdapter, 并重载 getView, 在函数中生成相应的 view (内部调用 LayoutInflater.form(context).inflate) ,填充数据并返回
```
单击
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
var item = arrayAdapter.getItem(position); // 返回指定位置对应的项目, 并使用
}
});
RecyclerView
布局
- 界面中加入 RecyclerView,
- 在 onCreate 中将 RecyclerView 与 数据源进行绑定
- 重载数据源处理的相关函数,返回每一项的视图及监听相应的事件
var recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
// 设置布局, 可以使用不同的布局,这就是比 listview 方便的地方
var layoutManager = new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL);
// var layoutManager = new LinearLayoutManager(this); // 比如换个布局管理器
recyclerView.setLayoutManager(layoutManager);
FruitAdapter adapter = new FruitAdapter(fruitList);
recyclerView.setAdapter(adapter); // 设置数据源
数据源管理
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder>{
private List<Fruit> mFruitList;
/** 为每一项设置一个对象,管理对应项的视图内容 */
static class ViewHolder extends RecyclerView.ViewHolder {
View fruitView; ImageView fruitImage; TextView fruitName;
public ViewHolder(View view) {
super(view);
fruitView = view;
fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
fruitName = (TextView) view.findViewById(R.id.fruit_name);
}
}
/** 为每一项创建一个相应的视图 */
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// 通过资源布局创建一个视图对象
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item, parent, false);
final ViewHolder holder = new ViewHolder(view);
// 创建单击事件, 可以直接绑定至子项中的某一个元素
holder.fruitView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = holder.getAdapterPosition();
Fruit fruit = mFruitList.get(position);
Toast.makeText(v.getContext(), "you clicked view " + fruit.getName(), Toast.LENGTH_SHORT).show();
}
});
return holder;
}
/** 将视图与某个位置的内容进行绑定 */
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
Fruit fruit = mFruitList.get(position);
holder.fruitImage.setImageResource(fruit.getImageId());
holder.fruitName.setText(fruit.getName());
}
}
一个聊天消息窗口
设置一个聊天内容布局,里面可以有接收消息与发送消息的样式。根据消息的类型自动设置
初始化
var msgRecyclerView = (RecyclerView) findViewById(R.id.msg_recycler_view);
LinearLayoutManager layoutManager = new LinearLayoutManager(this); // 设置对话窗口的布局
msgRecyclerView.setLayoutManager(layoutManager);
var adapter = new MsgAdapter(msgList); // 初始化并设置数据源
msgRecyclerView.setAdapter(adapter);
添加消息内容
// 在按钮事件中
Msg msg = new Msg(content, Msg.TYPE_SENT); // 创建一个聊天消息项
msgList.add(msg); // 将消息添加到列表中
adapter.notifyItemInserted(msgList.size() - 1); // 当有新消息时,刷新ListView中的显示, 其它还有更新、删除函数
msgRecyclerView.scrollToPosition(msgList.size() - 1); // 将ListView定位到最后一行
inputText.setText(""); // 清空输入框中的内容
数据源管理器
// MsgAdapter
/** 初始化聊天中某一项的视图对象 */
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.msg_item, parent, false);
return new ViewHolder(view);
}
/** 设置聊天中某一项的视图对象 */
public void onBindViewHolder(ViewHolder holder, int position) {
Msg msg = mMsgList.get(position);
if (msg.getType() == Msg.TYPE_RECEIVED) {
// 如果是收到的消息,则显示左边的消息布局,将右边的消息布局隐藏
holder.leftLayout.setVisibility(View.VISIBLE);
holder.rightLayout.setVisibility(View.GONE);
holder.leftMsg.setText(msg.getContent());
} else if(msg.getType() == Msg.TYPE_SENT) {
// 如果是发出的消息,则显示右边的消息布局,将左边的消息布局隐藏
holder.rightLayout.setVisibility(View.VISIBLE);
holder.leftLayout.setVisibility(View.GONE);
holder.rightMsg.setText(msg.getContent());
}
}
碎片
碎片是可以嵌入到活动中的 UI 片段,可以充分利用屏幕的空间
创建
-
与普通活动一样创建布局
-
为布局创建对应的从 Fragment 继承的类, 并重载 onCreateView ,将 类与布局进行绑定
java
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.left_fragment, container, false); // 将布局与碎片进行绑定
return view;
}
-
添加碎片
-
在活动视图中加入碎片
```java <!-- 将 name 设置为类名, 其它与普通活动布局一致 --> <fragment android:id="@+id/left_fragment" android:name="com.example.fragmenttest.LeftFragment" android:layout_width="match_parent" android:layout_height="match_parent"/> ```
-
动态添加
1. 在活动视图中添加一个 layout作为碎片的容器,如下 ```xml <FrameLayout android:id="@+id/right_layout" android:layout_height="match_parent" android:layout_weight="3" /> ``` 2. 在活动类中加入如下代码 ```java FragmentManager fragmentManager = getSupportFragmentManager(); FragmentTransaction transaction = fragmentManager.beginTransaction(); // 注意是替换掉了。需要的话使用 fragmentManager.findFragmentById(R.id.right_layout) 找回 transaction.replace(R.id.right_layout, fragment); transaction.addToBackStack(null); // 将操作入栈,这样按 back 时可以返回未替换前的状态 transaction.commit(); ```
-
碎片相互通信
// 活动中找碎片
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.findFragmentById(R.id.right_layout)
// 碎片中找活动
FragmentActivity activity = this.getActivity(); // 返回当前碎片所在的活动
// 碎片找碎片, 1. 找到所在活动 2. 调用活动方法找到碎片
FragmentActivity activity = this.getActivity();
FragmentManager manager = activity.getSupportFragmentManager();
fragmentManager.findFragmentById(R.id.right_layout)
碎片生命周期
根据设备限定符加载不同布局
指定不同屏幕大小 small\normal\large\xlarge 指定一个范围 ldpi/mdpi/hdpi/xhdpi/xxhdpi 0-120/160/240/320/480dpi 指定不同方向 land/port 横屏/竖屏 指定最小值 swxxxxdp sw600dp
自适应程序
-
为不同设备设计主活动,根据不同的屏幕大小加载不同的碎片
-
在碎片的 onActivityCreated 中判断一当前活动中其它碎片的情况
java
// 在碎片中获取当前所在的活动,然后查找其它碎片是否存在
if (getActivity().findViewById(R.id.news_content_layout) != null) {
isTwoPane = true; // 可以找到news_content_layout布局时,为双页模式
} else { isTwoPane = false; // 找不到news_content_layout布局时,为单页模式
}
3. 在处理程序时,根据没同的情况进行不同的处理
java
News news = mNewsList.get(holder.getAdapterPosition());
if (isTwoPane) {
// 如果当前在其它碎片的存在,则直接将内容加载到此碎片中
NewsContentFragment newsContentFragment = (NewsContentFragment)
getFragmentManager().findFragmentById(R.id.news_content_fragment);
newsContentFragment.refresh(news.getTitle(), news.getContent());
} else {
// 如果没有其它碎片的存在,直接加载一个活动
NewsContentActivity.actionStart(getActivity(), news.getTitle(), news.getContent());
}
广播机制
标准广播: 异步执行广播,所有客户端几乎同一时间接收。 有序广播: 同一时刻只有一个接收器接收。优先级高的接收器先接收,接收完成后可以向后传播或停止传播。
接收系统消息
动态注册
需要程序运行,在页面中调用,传入页面对象
-
继承 BroadcastReceiver, 重写 onReceive
-
new IntentFilter , 添加需要接收的广播类型
-
调用 registerReceiver 进行监听
-
调用 unregisterReceiver 释放资源
-
例子, 在活动中调用
```java // 接收器 class NetworkChangeReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) {} }
// 监听 intentFilter = new IntentFilter(); intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE"); networkChangeReceiver = new NetworkChangeReceiver(); this.registerReceiver(networkChangeReceiver, intentFilter);
// 释放 this.unregisterReceiver(networkChangeReceiver); ```
静态注册
可以使用向导创建,右键,新建,广播接收器, 然后它会创建接收器类及在 AndroidManifest.xml 中添加相关的节点。 在节点中添加需要注册消息类型(intent-filter)即可, 注意有可能还有请求相应的权限。
<receiver
android:name=".MyBroadcastReceiver" <!-- 自动生成对应的类 -->
android:enabled="true" <!-- 是否启用 -->
android:exported="true"> <!-- 是否接受全局消息 -->
<intent-filter android:priority="100">
<action android:name="com.example.broadcasttest.MY_BROADCAST"/> <!-- 需要接收的消息的类型 -->
</intent-filter>
</receiver>
发送自定义广播
// 1. 静态或是动态注册接收器
// 2. 发送消息
Intent intent = new Intent("com.example.broadcasttest.LOCAL_BROADCAST");
this.sendBroadcast(intent);
发送有序广播
// 1. 与发送标准广播类型, 调用函数不一样
Intent intent = new Intent("com.example.broadcasttest.LOCAL_BROADCAST");
this.sendOrderedBroadcast(intent, null); // 第二个为权限值,先传入 null
// 2. AndroidManifest.xml 中定义一下接收优先级
<intent-filter android:priority="100" />
// 3. 在 onReceive 中可以调用 abortBroadcast(); //中断消息传播
本地广播
// 与系统广播一致, 使用 LocalBroadcastManager
// 1. 发送广播
localBroadcastManager = LocalBroadcastManager.getInstance(this); // 获取实例
Intent intent = new Intent("com.example.broadcasttest.LOCAL_BROADCAST");
localBroadcastManager.sendBroadcast(intent); // 发送本地广播
// 2. 接收广播
intentFilter = new IntentFilter();
intentFilter.addAction("com.example.broadcasttest.LOCAL_BROADCAST");
localReceiver = new LocalReceiver();
localBroadcastManager.registerReceiver(localReceiver, intentFilter); // 注册本地广播监听器
// 3. 注销广播接收器
localBroadcastManager.unregisterReceiver(localReceiver);
接收下线消息
- 订制一个活动基类
- 基类在 onCreate 时将自身加入到一个静态列表中,在 onDestroy 中将自身从静态列表中移除
-
基类在 onResume 中监听广播,onPause 中停止监听广播。 onResume / onPause 是活动在栈顶与非栈顶时的消息 。所以广播接收者必定会在顶层时才能接收到消息
-
接收到消息时,调用静态列表中所有活动的 finish 函数,然后在最后调用登录活动
java
ActivityCollector.finishAll(); // 销毁所有活动
Intent intent = new Intent(context, LoginActivity.class);
context.startActivity(intent); // 重新启动 LoginActivity
请求权限
接收事件时可能需要进行一些系统调用,需要在 AndroidManifest.xml 中添加权限要求,比如下面要求查询网络的权限
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
持久化数据
文件形式存储
标准文件流
/data/data/
/files
保存
public void save(String inputText) {
FileOutputStream out = null;
BufferedWriter writer = null;
try {
// Context.MODE_APPEND: 追加 Context.MODE_PRIVATE: 覆盖
out = openFileOutput("data", Context.MODE_PRIVATE);
writer = new BufferedWriter(new OutputStreamWriter(out));
writer.write(inputText);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (writer != null) {
writer.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
读取
public String load() {
FileInputStream in = null;
BufferedReader reader = null;
StringBuilder content = new StringBuilder();
try {
in = openFileInput("data");
reader = new BufferedReader(new InputStreamReader(in));
String line = "";
while ((line = reader.readLine()) != null) {
content.append(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return content.toString();
}
SharedPreference 存储
相当于字典
// context 中实现
SharedPreferences preferences = this.getSharedPreferences("fileName", this.MODE_PRIVATE);
// activity 中实现, 以当前活动名作为文件名
SharedPreferences preferences = this.getPreferences(this.MODE_PRIVATE);
// 静态函数,传入 context , 以当前包名作为文件名
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
// 设置值
SharedPreferences.Editor edit = preferences.edit();
edit.putInt("key", 123);
edit.putLong("key", 123);
edit.clear(); // 清空所有数据
editor.apply(); // 提交保存
// 返回值
preferences.getInt("key", -1);
preferences.getLong("key", -1);
SQLITE
- 继承 SQLiteOpenHelper
```java
// 重载创建数据表函数
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_BOOK);
db.execSQL(CREATE_CATEGORY);
}
// 重载更新数据表函数, 需要 new MyDatabaseHelper 时, 更新版本号才会被调用 @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL("drop table if exists Book"); db.execSQL("drop table if exists Category"); onCreate(db); } ```
- 打开数据库
java
// 最后一个是版本号, 版本号更新 onUpgrade 会被调用
dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 2);
// 打开只读数据库
dbHelper.getReadableDatabase();
// 打开读写数据库
dbHelper.getWritableDatabase();
- 插入数据
java
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("name", "The Da Vinci Code");
values.put("pages", 454);
values.put("price", 16.96);
db.insert("Book", null, values); // 插入第一条数据, 第二个参数先默认填入 null, 返回插入的数据的 rowid
values.clear();
- 更新数据
java
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("price", 10.99);
// 使用 where 找到数据进行更新
db.update("Book", values, "name = ?", new String[] { "The Da Vinci Code" });
- 删除数据
java
SQLiteDatabase db = dbHelper.getWritableDatabase();
db.delete("Book", "pages > ?", new String[] { "500" });
- 查询
```java
SQLiteDatabase db = dbHelper.getWritableDatabase();
// 查询Book表中所有的数据
// Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy)
// Cursor cursor = db.query("Book", new String[] {"rowid as rowid", "*"}, null, null, null, null, null);
Cursor cursor = db.query("Book", null, null, null, null, null, null);
if (cursor.moveToFirst()) {
do {
long rowid = cursor.getLong(cursor.getColumnIndex("rowid"));
String name = cursor.getString(cursor.getColumnIndex("name"));
String author = cursor.getString(cursor.getColumnIndex("author"));
int pages = cursor.getInt(cursor.getColumnIndex("pages"));
double price = cursor.getDouble(cursor.getColumnIndex("price"));
} while (cursor.moveToNext());
} cursor.close(); ```
- 直接操作
java
db.execSQL();
db.rawQuery();
LitePal
准备工作
- 修改 build.gradle
java
# 在 app 的 build.gradle 中添加引用
dependencies {
compile 'org.litepal.android:core:1.3.2'
}
```
- 创建 litepal.xml
xml
<!-- main/assets/litepal.xml -->
<?xml version="1.0" encoding="utf-8"?>
<litepal>
<!-- 数据库名 -->
<dbname value="BookStore" ></dbname>
<!-- 数据库版本, 只要版本增加,数据库将自动升级 -->
<version value="2" ></version>
<list>
<mapping class="com.example.litepaltest.Book"></mapping>
<mapping class="com.example.litepaltest.Category"></mapping>
</list>
</litepal>
- 修改 AndroidManifest.xml
```xml
- 新建 ORM 类
```java // 类需要继承自 DataSupport package com.example.litepaltest; import org.litepal.crud.DataSupport; public class Book extends DataSupport {
private int id;
private String author;
private double price;
public int getId() {
return id; }
public void setId(int id) {
this.id = id; }
public String getAuthor() {
return author; }
public void setAuthor(String author) {
this.author = author; }
public double getPrice() {
return price; }
public void setPrice(double price) {
this.price = price; }
} ```
使用
```java // 将会初始化数据库 LitePal.getDatabase(); Connector.getDatabase();
// 添加数据 Book book = new Book(); book.setAuthor("Dan Brown"); book.setPrice(16.96); book.save();
// 更新数据, 注意,因为 new Book 时,所有值为默认值,所以如果某个值被更新为默认值时,是不会写入到数据库中的 Book book = new Book(); book.setPrice(14.95); book.updateAll("name = ? and author = ?", "The Lost Symbol", "Dan Brown"); // 将某个值更新为默认值 Book book = new Book(); book.setToDefault("price"); book.updateAll();
// 删除1, 我未测试 Book book = new Book(); book.setId(1); book.delete();
// 删除2 DataSupport.deleteAll(Book.class, "price < ?", "15"); // 不指定条件,删除所有的
// 删除3 // 调用原生 sql
// 查询
List
// 原生 sql Cursor cursor = DataSupport.findBySQL("select * from Bok where price > ?; ", "300"); ```
序列化
android 中提供了序列化对象接口( Serializable),但是书中没有提到,等可能用到时再说
内容提供器
为了在不同应用之间共享数据,可以使用内容提供器进行数据共享,
运行时权限
<!-- AndroidManifest.xml 中指定需要的权限 -->
<uses-permission android:name="android.permission.CALL_PHONE" />
在低于 6.0 系统上会在安装时提示权限要求,6.0 或以上版本应用在使用到相关权限时再请求
andorid 中权限分为正常权限与危险权限,正常权限会自动授权,但是危险权限会提示用户授权。相关见 [权限说明]: https://developer.android.com/guide/topics/security/permissions?hl=zh-cn
请求权限流程
在按钮函数中判断是否已经存在权限了, 如果有权限直接外呼,如果没有权限,请求权限并返回 。用户给予权限后通过回调函数得到授权情况,并继续调用外呼函数。 例子如下:以后可以将整个权限处理流程都包到一个函数中,函数参数为调用函数及要求的权限。
/* 回调接口,如果没有权限,将函数包起来等待回调 */
interface IDoable {
void doIt();
}
/** 生成一个不重复序号, 供回调使用 */
static AtomicInteger atomicInteger = new AtomicInteger();
/** 缓存请求权限处理后的处理函数 */
Map<Integer, IDoable> doItMap = new HashMap<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button makeCall = (Button) findViewById(R.id.make_call);
makeCall.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 取得 activity 的引用
final MainActivity mainActivity = MainActivity.this;
// 需要的外呼权限
String callPhonePermission = Manifest.permission.CALL_PHONE;
// 判断权限,如果有,直接调用
int result = ContextCompat.checkSelfPermission(mainActivity, callPhonePermission);
if (result == PackageManager.PERMISSION_GRANTED) {
Log.d(mainActivity.getLocalClassName(), "有拨号权限");
mainActivity.call();
return;
}
// 如果没有权限,包装一个IDoable 对象,请求权限,并等待 onRequestPermissionsResult 的回调
Log.d(mainActivity.getLocalClassName(), "没有拨号权限");
int requestCode = atomicInteger.getAndIncrement();
Log.d(mainActivity.getLocalClassName(), String.format("请求拨号权限: %s", requestCode));
doItMap.put(requestCode, new IDoable() {
// 包装函数
@Override
public void doIt() { mainActivity.call(); }
});
// 请求权限
ActivityCompat.requestPermissions(mainActivity, new String[]{callPhonePermission}, requestCode);
}
});
}
/**
* 权限请求处理后的回调函数
* @param requestCode
* @param permissions 要求的请求
* @param grantResults 对应 permissions 的请求是否通过 (==PackageManager.PERMISSION_GRANTED)
*/
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
// 查找有没有对应的处理函数,如果就直接调用
IDoable iDoable = doItMap.get(requestCode);
if (iDoable == null) {
Log.d(this.getLocalClassName(), String.format("权限请求 %s 没有处理函数", requestCode));
return;
}
Log.d(this.getLocalClassName(), String.format("开始处理请求 %s", requestCode));
// 简单的认为第一项就是需要的权限, todo: doIt 调用
if (grantResults != null && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
iDoable.doIt();
}
}
/** 呼叫电话 */
private void call() {
try {
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
} catch (SecurityException e) {
// 如果没有权限就进行记录
e.printStackTrace();
Log.e(this.getLocalClassName(), "外呼电话发生错误", e);
Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show();
}
}
使用内容提供器
以读取电话号码 本为例子
// uri 为读取器提供的唯一定义符,一般使用包名来命名, 比如 Uri.parse("content://com.example.app.provider/table1") 之类的形式,前面定位应用,后面定位应用中的项目
Uri contentUri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
// 打开读取器
ContentResolver contentResolver = this.getContentResolver();
// 读取联系人, 记得关闭 cursor
// query(Uri 定位符, String[] 查询的列名, String where 限制, String[] where 参数, String 排序 参数)
cursor = contentResolver.query(contentUri, null, null, null, null);
while (cursor != null && cursor.moveToNext()) {
int nameIdx = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME);
int numberIdx = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
String displayName = cursor.getString(nameIdx);
String number = cursor.getString(numberIdx);
}
// 添加联系人
ContentValues values = new ContentValues();
values.put("column1", "x");
contentResolver.insert(contentUri, values);
// 更新联系人
ContentValues values = new ContentValues();
values.put("column1", "x");
contentResolver.update(contentUri, values, "column2 = ?", new String[]{"123"});
// 删除联系人
contentResolver.delete(contentUri, "column2 = ?", new String[]{"123"});
创建内容提供器
可以使用向导创建
- AndroidManifest.xml 中添加
<!-- name:类名; authorities:唯一定位符 -->
<provider
android:name=".DatabaseProvider"
android:authorities="com.example.databasetest.provider"
android:enabled="true" android:exported="true" />
- 继承一个 ContentProvider 类
可以使用 UriMatcher 辅助解析出 uri
// 继承一个 ContentProvider 类
public class XContentProvider extends ContentProvider {
public boolean onCreate() { /** 被查询时调用, 返回 true 表示成功 */ }
public Cursor query(Uri uri, String[] strings, String s, String[] strings1, String s1) { }
public Uri insert(Uri uri, ContentValues contentValues) {}
public int delete(Uri uri, String s, String[] strings) { }
public int update(Uri uri, ContentValues contentValues, String s, String[] strings) { }
public String getType(Uri uri) {
// 返回 uri 对应的 MIME 类型,
// vnd.android.cursor.{dir|item}/vnd.{AUTHORITY}.{PATH}
// 如果 uri 以 / 结尾,则为 dir, 不然为 item
}
}
多媒体
通知
// 生成通知发生的时间, 为了演示时间如何进行计算,这里将时间修改为一个小时间前
Date date = new Date(System.currentTimeMillis());
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.HOUR, -1);
// 构建一个点击通知后的执行 intent, requestCode 最好每次都不一样
int noteMsgId = 128; // 通知的编号,可以通过这个编号将通知取消
Intent intent = new Intent(this, NotificationActivity.class); // 设置回调使用的 intent
intent.putExtra("noteMsgId", noteMsgId);
intent.putExtra("messageInfoId", 32);
PendingIntent pi = PendingIntent.getActivity(this, 128, intent, 0);
// 1. 构建通知内容, Notification 在不同版本中有不同的区别,所以使用 NotificationCompat 统一构建通知
Notification notification = new NotificationCompat.Builder(this)
.setContentTitle("This is content title") // 通知的标题
.setContentText("This is content text") // 通知的内容
// 标记通知发生的时间, 简单一些可以直接使用 System.currentTimeMillis() ,表示当前时间
.setWhen(calendar.getTimeInMillis())
.setSmallIcon(R.mipmap.ic_launcher) // 小图标
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)) // 大图标, 需要转为 bitmap
.setContentIntent(pi) // 点击后执行的 intent
.setSound(Uri.fromFile(new File("/system/media/audio/ringtones/Luna.ogg"))) // 播放声音
.setVibrate(new long[]{0, 1000, 1000, 1000}) // 进行振动, 单位数为静止时长,双位数表示振动时长, 需要权限
.setLights(Color.GREEN, 1000, 1000) // 设置消息灯, 灯时长,灭时长
.setDefaults(NotificationCompat.DEFAULT_ALL) // 使用系统默认的声音、振动、灯光设置 (传入不同参数), 就不用自定义这些值了
// setContentText 只能设置短内容,这里可以设置长内容,但是长文件与大图片只能显示一样
.setStyle(new NotificationCompat.BigTextStyle().bigText("这里填写长文本"))
//这里可以设置大图片,但是长文件与大图片只能显示一样
.setStyle(new NotificationCompat.BigPictureStyle().bigPicture(BitmapFactory.decodeResource(getResources(), R.drawable.big_image)))
.setPriority(NotificationCompat.PRIORITY_MAX) // 通知的重要程序
.setAutoCancel(true) // 点击通知后自动消失
.build(); // 构建通知
// 2. 获取通知服务接口
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
// 3. 发送通知
manager.notify(noteMsgId, notification);
// 4. 主动取消消息
manager.cancel(noteMsgId);
// 显示通知时
Intent intent = this.getIntent();
int noteMsgId = intent.getIntExtra("noteMsgId", -1);
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
manager.cancel(noteMsgId); // 关闭消息栏上的通知
int messageId = intent.getIntExtra("messageInfoId", -1);
// 使用 messageId 从数据库中读出消息进行显示
Log.d(this.getLocalClassName(), String.format("返回消息编号 %s", messageId));
// 关闭自身
View button = this.findViewById(R.id.close_button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
NotificationActivity.this.finish();
}});
摄像头
权限声明
需要设置珍上内容提供器,供摄像机保存照片使用
<!-- AndroidManifest.xml, name 固定,authorities 为对外提供读取的标识符
因为应用会创建一个文件,让摄像头去保存文件,所以需要一个内容提供器, 这里使用内置类 android.support.v4.content.FileProvider
需要在 meta-data 中指定共享的路径, 下例在 xml 目录下的 file_paths.xml 中进行配置
-->
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.example.cameraalbumtest.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<!-- xml/file_paths.xml, external-path 表示共享配置, name 随意, path 为相应的路径, 空为全部 -->
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="" />
</paths>
<!-- 早期(android 4.4之前)共享文件访问权限的时候也要申请权限,为了兼容,这里直接请求权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
代码
// 1. 创建File对象,用于存储拍照后的图片
// getExternalCacheDir() 返回应用的缓存目录,其它目录可能要权限,就保存在这里好了,
// sdcard/android/packagename/cache
File outputImage = new File(getExternalCacheDir(), "output_image.jpg");
outputImage.createNewFile(); // 如果 exists() 先 delete()
// 2. 构建图像保存的 uri 供相机保存图像使用
if (Build.VERSION.SDK_INT < 24) {
// 低版本,直接获取图像保存路径
imageUri = Uri.fromFile(outputImage);
} else {
// 高版本创建一个内容提供器,供相机使用
imageUri = FileProvider.getUriForFile(MainActivity.this, "com.example.cameraalbumtest.fileprovider", outputImage);
}
// 3. 启动相机程序, 并传入回调编号
Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent, TAKE_PHOTO);
// 4. 回调函数
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if(requestCode == TAKE_PHOTO && resultCode == RESULT_OK ){
// 5. 输出连接,得到图像文件 uri ,并返回流, 转为图像对象
InputStream stream = getContentResolver().openInputStream(this.imageUri);
Bitmap bitmap = BitmapFactory.decodeStream(stream);
picture.setImageBitmap(bitmap);
}
}
判断有没有相机
/**
* 判断某个意图是否存在
*/
public static boolean isHaveCame(String intentName) {
PackageManager packageManager = App.context.getPackageManager();
Intent intent = new Intent(intentName);
List<ResolveInfo> list = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
return list.size() > 0;
}
相册选择照片
这里有一些说明 https://www.jianshu.com/p/7c6a53db8b12 https://link.jianshu.com/?t=https://github.com/YuanTiger/PhotoGet 里面有一段根据 兼容不同 uri 返回路径的代码
// 1. 判断并请求 Manifest.permission.WRITE_EXTERNAL_STORAGE 权限
// 有权限时直接打开,没有权限 onRequestPermissionsResult 中进行回调用
// 2. 一个打开相册
Intent intent = new Intent("android.intent.action.GET_CONTENT");
intent.setType("image/*");
startActivityForResult(intent, CHOOSE_PHOTO); // 打开相册
// 3. 回调函数
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if(requestCode == CHOOSE_PHOTO && resultCode == RESULT_OK ){
// 判断手机系统版本号
if (Build.VERSION.SDK_INT >= 19) {
// 4.4及以上系统使用这个方法处理图片
handleImageOnKitKat(data);
} else {
// 4.4以下系统使用这个方法处理图片
handleImageBeforeKitKat(data);
}
}
}
// 4. 显示图像, TargetApi 见 TargetApi 节的说明
// 这里很复杂。看起来是针对多种情况进行了兼容, 有需要再深入的进行分析
@TargetApi(19)
private void handleImageOnKitKat(Intent data) {
String imagePath = null;
Uri uri = data.getData();
Log.d("TAG", "handleImageOnKitKat: uri is " + uri);
if (DocumentsContract.isDocumentUri(this, uri)) {
// 如果是document类型的Uri,则通过document id处理
String docId = DocumentsContract.getDocumentId(uri);
if("com.android.providers.media.documents".equals(uri.getAuthority())) {
String id = docId.split(":")[1]; // 解析出数字格式的id
String selection = MediaStore.Images.Media._ID + "=" + id;
imagePath = getImagePath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection);
} else if ("com.android.providers.downloads.documents".equals(uri.getAuthority())) {
Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(docId));
imagePath = getImagePath(contentUri, null);
}
} else if ("content".equalsIgnoreCase(uri.getScheme())) {
// 如果是content类型的Uri,则使用普通方式处理
imagePath = getImagePath(uri, null);
} else if ("file".equalsIgnoreCase(uri.getScheme())) {
// 如果是file类型的Uri,直接获取图片路径即可
imagePath = uri.getPath();
}
displayImage(imagePath); // 根据图片路径显示图片
}
// 早期版本中根据所有的数据直接读出图像
private void handleImageBeforeKitKat(Intent data) {
Uri uri = data.getData();
String imagePath = getImagePath(uri, null);
displayImage(imagePath);
}
// 使用内容提供器读取文件
private String getImagePath(Uri uri, String selection) {
String path = null;
// 通过Uri和selection来获取真实的图片路径
Cursor cursor = getContentResolver().query(uri, null, selection, null, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
}
cursor.close();
}
return path;
}
// 显示图片文件
private void displayImage(String imagePath) {
Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
picture.setImageBitmap(bitmap);
}
TargetApi
TargetApi 是为了在低版本的 sdk 中能调用高版本的 sdk 而声明的。 要使用高级版本的 sdk 有 3种方式。
- 将目标平台设置为高版本,但是在低版本的机器上运行的时候会死得很难看
- 将目标平台设置为低版本,然后通过反射获得高版本的 sdk 函数,然后再进行调用 。但是开发难度会很高
-
将目标平台设置为低版本,但在调用高版本的函数上声明 @TargetApi(版本号)。这样就不会发生编译错误了。但是在代码中根据版本判断调用 不同的函数即可。
-
asf
-
af
-
asf
-
saf
-
asf
-
asf
-
sadf
-
afasf
-
asf