一. Handler 的使用方法
1. Handler 作用
在开发中,我们经常会需要做一些耗时的操作:比如下载图片、打开网页、下载视频等。如果将这些耗时的操作放在主线程(UI线程),长时间的阻塞导致应用ANR。必然应该将这些操作放在子线程中处理,这些操作处理过程中,我们需要更新UI界面以告知用户现在具体的进度、状态等信息。
所以:在多线程的应用场景中,将工作线程中需更新UI
的操作信息 传递到 UI
主线程,从而实现 工作线程对UI
的更新处理,最终实现异步消息的处理
但是,多个线程并发执行UI 主线程,会导致数据更新异常,使用 Handler 的作用时: 多个线程并发更新UI的同时 保证线程安全
2. Handler 相关的名词
Handler 作为主线程和 工作线程的一个媒介
Handler
、Message
、Message Queue
、Looper
3. 使用方法
在子线程中Handler将消息发送到MessageQueue中,然后Looper不断的从MessageQueue中读取消息,并调用Handler的dispatchMessage发送消息,最后再Handler来处理消息。为了更好的帮助大家一起理解,我画了一个Handler机制的原理图:
使用方式分为2 种:使用Handler.sendMessage()
、使用Handler.post()
3.1 Handler.sendMessage()
使用步骤:
1. 自定义Handler子类(继承Handler类) & 复写handleMessage()方法
@Override
public void handleMessage(Message msg) { ...
switch(msg.what){} // 根据不同工作线程,执行不同操作
// 需执行的UI操作 }
2. 在 UI 主线程中, 创建Handler实例
private Handler mhandler = new mHandler();
3. 在工作线程中 , 创建所需的消息对象
Message msg = Message.obtain(); // 实例化消息对象 ,推荐使用这种方法。而不是 Message msg = new Message();
msg.what = 1; //
消息标识 msg.obj = "AA"; // 消息内容存放
4. 在工作线程中, 通过Handler发送消息到消息队列中
mHandler.sendMessage(msg);
5. 在监听事件中,开启工作线程
例子:模拟点击按钮,进行下载的小栗子
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity implements Button.OnClickListener {
private TextView statusTextView = null;
private Handler uiHandler;
class Mhandler extends Handler {
//步骤1:(自定义)新创建Handler子类(继承Handler类) & 复写handleMessage()方法
@Override
public void handleMessage(Message msg) {
// 工作线程发过来的信息
switch (msg.what){
case 1:
System.out.println("handleMessage thread id " + Thread.currentThread().getId());
System.out.println("msg.arg1:" + msg.arg1);
System.out.println("msg.arg2:" + msg.arg2);
MainActivity.this.statusTextView.setText("文件下载完成");
// 获取传入的参数
Bundle bundle = msg.getData();
String value = bundle.getString("list");
textView.setText(value);
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 步骤2:在主线程中创建Handler实例
uiHandler = new Handler()
statusTextView = (TextView)findViewById(R.id.statusTextView);
Button btnDownload = (Button)findViewById(R.id.btnDownload);
btnDownload.setOnClickListener(this);
System.out.println("Main thread id " + Thread.currentThread().getId());
}
@Override
public void onClick(View v) {
DownloadThread downloadThread = new DownloadThread();
downloadThread.start();
}
// 工作线程
class DownloadThread extends Thread{
@Override
public void run() {
try{
System.out.println("DownloadThread id " + Thread.currentThread().getId());
System.out.println("开始下载文件");
//此处让线程DownloadThread休眠5秒中,模拟文件的耗时过程
Thread.sleep(5000);
System.out.println("文件下载完成");
//文件下载完成后更新UI
// 步骤3:在工作线程中 创建所需的消息对象
Message msg = new Message();
//msg = Message.obtain(); // 最好使用这种方法
//what是我们自定义的一个Message的识别码,以便于在Handler的handleMessage方法中根据what识别
//出不同的Message,以便我们做出不同的处理操作
msg.what = 1;
//我们可以通过arg1和arg2给Message传入简单的数据
msg.arg1 = 123;
msg.arg2 = 321;
//我们也可以通过给obj赋值Object类型传递向Message传入任意数据
//msg.obj = null;
//我们还可以通过setData方法和getData方法向Message中写入和读取Bundle类型的数据
//msg.setData(null);
//Bundle data = msg.getData();
// 传入更多的参数
Bundle bundle = new Bundle();
bundle.putString("list", "this is mesage");
bundle.putString("list2", "this is list2");
msg.setData(bundle);
//步骤4:在工作线程中 通过Handler发送消息到消息队列中
uiHandler.sendMessage(msg);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
3.2 Handler.post()使用方法:较为简单,但是底层还是 调用Handler.sendMessage()
package ispring.com.testhandler;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity implements Button.OnClickListener {
private TextView statusTextView = null;
//uiHandler在主线程中创建,所以自动绑定主线程
private Handler uiHandler = new Handler();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
statusTextView = (TextView)findViewById(R.id.statusTextView);
Button btnDownload = (Button)findViewById(R.id.btnDownload);
btnDownload.setOnClickListener(this);
System.out.println("Main thread id " + Thread.currentThread().getId());
}
@Override
public void onClick(View v) {
DownloadThread downloadThread = new DownloadThread();
downloadThread.start();
}
class DownloadThread extends Thread{
@Override
public void run() {
try{
System.out.println("DownloadThread id " + Thread.currentThread().getId());
System.out.println("开始下载文件");
//此处让线程DownloadThread休眠5秒中,模拟文件的耗时过程
Thread.sleep(5000);
System.out.println("文件下载完成");
//文件下载完成后更新UI
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Runnable thread id " + Thread.currentThread().getId());
MainActivity.this.statusTextView.setText("文件下载完成");
}
};
// 这里使用 post
uiHandler.post(runnable);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
二、AsyncTask 异步任务
Android 提供了一个轻量级的用于处理异步任务的类 AsyncTask,对Thread 和 Handler 进行了封装,用于多线程通信。
我们一般是继承 AsyncTask
,然后在类中实现异步操作,再将异步执行的进度,反馈给 UI 主线程
AsyncTask
是一个抽象类,一般我们都会定义一个类继承 AsyncTask 然后重写相关方法
AsyncTask<Params, Progress, Result>
三个参数说明:
参数 | 说明 |
---|---|
Params | 启动任务执行的是如参数,比如一个网络请求的 URL |
Progress | 后台任务执行的百分比 |
Result | 后台任务执行完毕后返回的结果 |
如果不需要一些参数,可以使用
void
代替
需要重写的方法,不能直接调用
方法 | 说明 |
---|---|
onPreExecute() | 在执行后台耗时操作前调用,通常用于一些初始化操作,比如显示进度条 |
doInBackground(params...) | 在 onPreExecute() 方法执行完毕后立即执行,该方法运行于后台,主要负责执行耗时的后台处理工作,可调用 publishProgress(progress) 来更新时时的任务进度 |
onPostExecute(Result) | 在 doInBackground(params...) 执行完毕后,该方法会被 UI 线程调用,后台任务的运行结果将通过该方法传递到 UI 线程,然后展示给用户 |
onProgressUpdate(progress) | 在 publishProgress(progress...) 被调用后,接收publishProgress传来的参数, UI 线程将调用该方法在界面上展示任务的进度,比如更新进度条 |
onCancelled() | 用户取消线程操作的时候调用,也就是在主线程调用 doInBackground方法中调用cancel时会触发该方法 |
可以直接调用的方法
方法 | 说明 |
---|---|
execute | 开始执行异步处理任务。params参数对应execute方法的输入参数 |
executeOnExecutor | 以指定线程池模式开始执行任务。THREAD_POOL_EXECUTOR表示异步线程池, SERIAL_EXECUTOR表示同步线程池。默认是SERIAL_EXECUTOR。 |
publishProgress | 更新进度。该方法只能在doInBackground方法中调用,调用后会触发onProgressUpdate方法。 |
cancel | 取消任务。该方法调用后,doInBackground的处理立即停止,并且接着调用onCancelled方法,而不会调用onPostExecute方法。 |
get | 获取处理结果。 |
getStatus | 获取任务状态。PENDING表示还未执行,RUNNING表示正在执行,FINISHED表示执行完毕 |
isCancelled | 判断该任务是否取消。true表示取消,false表示未取消 |
使用 AsyncTask 几点注意事项
- Task 的实例必须在 UI 线程中创建
execute()
方法必须在 UI 线程中调用- 不要手动调用
onPreExecute()
、doInBackground(params...)
、onPostExecute
、onCancelled()
这几个方法 - Task 只能执行一次,运行多次会出现异常
- execute() 开启任务入口,应该在UI 线程中运行
模拟下载的例子:
1. activity_main.xml
ui文件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/txttitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<!--设置一个进度条,并且设置为水平方向-->
<ProgressBar
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/pgbar"
style="?android:attr/progressBarStyleHorizontal"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/btn_update"
android:text="开始下载"/>
</LinearLayout>
2. 创建 Download.java
用于模拟耗时操作
package cn.twle.android.asynctask;
public class Download {
//延时操作,用来模拟下载
public void delay()
{
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();;
}
}
}
3. 创建 MsDownloadTask.java
import android.os.AsyncTask;
import android.widget.ProgressBar;
import android.widget.TextView;
public class MsDownloadTask extends AsyncTask<Integer,Integer,String>
{
private TextView txt;
private ProgressBar pgbar;
public MsDownloadTask(TextView txt,ProgressBar pgbar)
{
super();
this.txt = txt;
this.pgbar = pgbar;
}
//该方法运行在UI线程中,可对UI控件进行设置
@Override
protected void onPreExecute() {
txt.setText("开始下载");
}
//该方法不运行在UI线程中,主要用于异步操作,执行耗时程序,通过调用publishProgress()方法
//触发onProgressUpdate对UI进行操作
@Override
protected String doInBackground(Integer... params) {
Download dop = new Download();
int i = 0;
for (i = 10;i <= 100; i+=10)
{
dop.delay();
publishProgress(i);
}
return i + params[0].intValue() + "";
}
//在doBackground方法中,每次调用publishProgress方法都会触发该方法
//运行在UI线程中,可对UI控件进行操作
@Override
protected void onProgressUpdate(Integer... values) {
int value = values[0];
pgbar.setProgress(value);
}
}
4. MainActivity.java
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
private TextView txttitle;
private ProgressBar pgbar;
private Button btnupdate;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
txttitle = (TextView)findViewById(R.id.txttitle);
pgbar = (ProgressBar)findViewById(R.id.pgbar);
btnupdate = (Button)findViewById(R.id.btn_update);
btnupdate.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
MsDownloadTask myTask = new MsDownloadTask(txttitle,pgbar);
myTask.execute(1000);
}
});
}
}
5. 如何取消下载进程:
假设增加了一个 Button id 是 stop
stop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i(TAG, "onClick: 停止1");
myTask.cancel(true);
}
});
更改 方法:
@Override
protected String doInBackground(Integer... params) {
Download dop = new Download();
int i = 0;
for (i = 10;i <= 100; i+=10) {
try {
if (isCancelled()){
Log.i(TAG, "doInBackground: 被标记停止了");
break;
}
}catch (InterruptedException e) {
e.printStackTrace();
}
dop.delay();
publishProgress(i);
return i + params[0].intValue() + "";
}
重提:3个参数
AsyncTask<Integer, Integer, String>
// 1.Integer: AsyncTask.execute(100) 执行参数 100
// 2.Integer: publishProgress 的进度,onProgressUpdate会使用
// 3.String: doInBackground的返回值,onPostExecute会使用
- Params表示用于AsyncTask执行任务的参数的类型【】
- Progress表示在后台线程处理的过程中,可以阶段性地发布结果的数据类型
- Result表示任务全部完成后所返回的数据类型
download.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
DownloadTask dTask = new DownloadTask();
dTask.execute(100);
}
});
}
class DownloadTask extends AsyncTask<Integer, Integer, String>{
//后面尖括号内分别是参数(例子里是线程休息时间),进度(publishProgress用到),返回值 类型
@Override
protected void onPreExecute() {
//第一个执行方法
super.onPreExecute();
}
@Override
protected String doInBackground(Integer... params) {
//第二个执行方法,onPreExecute()执行完后执行
for(int i=0;i<=100;i++){
pb.setProgress(i);
publishProgress(i);
try {
Thread.sleep(params[0]);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return "执行完毕";
}
@Override
protected void onProgressUpdate(Integer... progress) {
//这个函数在doInBackground调用publishProgress时触发,虽然调用时只有一个参数
//但是这里取到的是一个数组,所以要用progesss[0]来取值
//第n个参数就用progress[n]来取值
tv.setText(progress[0]+"%");
super.onProgressUpdate(progress);
}
@Override
protected void onPostExecute(String result) {
//doInBackground返回时触发,换句话说,就是doInBackground执行完后触发
//这里的result就是上面doInBackground执行后的返回值,所以这里是"执行完毕"
setTitle(result);
super.onPostExecute(result);
}
}
}
6. 并行
安卓默认会串行执行任务,是因为内部默认的线程池中将任务进行了排队,保证他们一个一个来。只要我们换个满足要求的线程池来执行任务就行了。AstncTask内部就有一个线程池AsyncTask.THREAD_POOL_EXECUTOR
可以使用。当然,用Executors来创建也行。
然后将开始任务的execute(Params... params)
方法改为executeOnExecutor(Executor exec,Params... params)
.这里用AsyncTask.THREAD_POOL_EXECUTOR
.
task1.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
task2.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
三、Toast 吐司使用方法
(吐司 一般用于提示信息的,是一种很方便的消息提示框,会在屏幕中显示一个消息提示框,没任何按钮,也不会获得焦点一段时间过后自动消失
Toast
定义了两个常量,分别表示显示多长时间后消失
常量 | 说明 |
---|---|
LENGTH_LONG | 显示比较长时间 |
LENGTH_SHORT | 显示比较短时间 |
我们可以通过 Toast.setDuration(int duration)
自己定义时长
常用的一些方法:
方法 | 说明 |
---|---|
void cancel() | 如果 Toast 已经显示,则关闭 Toast, 如果 Toast 还未显示,则取消显示 |
void show() | 显示指定持续时间的视图 |
int getDuration() | 返回持续时间 |
int getGravity() | 获取 Toast 应显示在屏幕上的位置 |
float getHorizontalMargin() | 返回水平边距 |
float getVerticalMargin() | 返回垂直边距 |
View getView() | 返回 Toast 的 View |
int getXOffset() | 以 px 为单位返回 X 偏移量 |
int getYOffset() | 以 px 为单位返回 Y 偏移量 |
static Toast makeText(Context context, int resId, int duration) | 创建 Toast 用于显示给定的资源 |
static Toast makeText(Context context, CharSequence text, int duration) | 创建 Toast 用于显示给定的文本 |
void setDuration(int duration) | 设置 Toast 的显示时间 |
void setGravity(int gravity, int xOffset, int yOffset) | 设置 Toast 在屏幕上显示的位置 |
void setMargin(float horizontalMargin, float verticalMargin) | 设置视图的边距 |
void setText(int resId) | 用一个资源更新 Toast 要显示的文本或资源 |
void setText(CharSequence s) | 用一段文本更新 Toast 要显示的文本或资源 |
void setView(View view) | 设置要显示的视图 |
这是我们创建 Toast
用的最多的方式,最后要加 .show()
Toast.makeText(MainActivity.this, "提示的内容", Toast.LENGTH_LONG).show();
因为定制 Toast
的需求很常见,所以我们一般会把他们封装成一个方法
void warnHint(String str, int showTime) {
Toast toast = Toast.makeText(getApplicationContext(), str, showTime);
//设置显示位置
toast.setGravity(Gravity.CENTER_VERTICAL|Gravity.CENTER_HORIZONTAL , 0, 0);
TextView v = (TextView) toast.getView().findViewById(android.R.id.message);
// 设置 TextView 字体颜色
v.setTextColor(Color.RED);
toast.show();
}
四、事件监听机制
Android 提供了两套事件处理机制
-
基于监听的事件处理
这种事件处理方式就是给 Android UI 控件绑定特定的事件监听器
-
基于回调的事件处理
这种事件处理方式就是重写 Android UI 控件的特定事件回调方法,或者重写 Activity 特定的回调方法
一般情况下我们推荐使用 基于回调的事件处理 ,但特殊情况下,只能使用 基于监听的事件处理
1. 基于监听的事件处理机制
事件监听机制中由 事件源 , 事件 , 事件监听器 三类对象组成
三者之间的关系和 基于监听的事件处理机制 的一般流程如下图
- 为某个事件源(组件)设置一个监听器,用于监听用户操作
- 用户的操作,触发了事件源的监听器
- 生成了对应的事件对象
- 将这个事件源对象作为参数传给事件监听器
- 事件监听器对事件对象进行判断,执行对应的事件处理器(对应事件的处理方法)
我们可以使用五种方式给一个 UI 控件或 Activity
添加基于监听的事件
- 直接用匿名内部类 (推荐)
- 使用内部类
- 使用外部类
- 直接使用 Activity 作为事件监听器 (常用)
- 直接绑定到标签
1. 直接用匿名内部类 (推荐)
就是直接使用匿名类调用 setXxxListener()
,重写里面的方法
这是最常用的一种,通常是临时使用一次
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btn_ev = (Button) findViewById(R.id.btn_ev);
// 直接使用匿名内部类
btn_ev.setOnClickListener(new OnClickListener() {
//重写点击事件的处理方法 onClick()
@Override
public void onClick(View v) {
//显示 Toast 信息
Toast.makeText(getApplicationContext(), "简单教程,简单编程", Toast.LENGTH_SHORT).show();
}
});
}
}
2. 使用内部类
添加一个事件内部类,然后实例化一个对象传递给 setXxxListener()
作为对象
这种方法可以复用该事件类,而且可以直接访问外部类的所有界面组件
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btn_ev = (Button) findViewById(R.id.btn_ev);
//直接 new 一个内部类对象作为参数
btn_ev.setOnClickListener(new BtnClickListener());
}
//定义一个内部类,实现 View.OnClickListener 接口,并重写 onClick() 方法
class BtnClickListener implements View.OnClickListener
{
@Override
public void onClick(View v) {
Toast.makeText(getApplicationContext(), "简单教程,简单编程", Toast.LENGTH_SHORT).show();
}
}
}
3. 使用外部类
另外创建一个 Java
文件实现事件类
这种形式用的很少,因为外部类不能直接访问用户界面类中的组件,要通过构造方法将组件传入使用
最直接的影响就是代码不够简洁
public class MsClick implements OnClickListener {
private String msg;
private Context mContext;
//把要提示的文本作为参数传入
public MsClick(Context mContext,String msg)
{
this.msg = msg;
this.mContext = mContext;
}
@Override
public void onClick(View v) {
// 提示传入的信息
Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show();
}
}
4. 直接使用 Activity 作为事件监听器 (常用)
让 Activity
类实现 XxxListener
事件监听接口,然后在 Activity
中定义重写对应的事件处理器方法,最后调用 setXxxListener(this)
比如,Actitity
实现了 OnClickListener
接口,重写 onClick(view)
方法在为某些 UI 控件添加该事件监听对象
//让 Activity 方法实现 OnClickListener 接口
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btn_ev = (Button) findViewById(R.id.btn_ev);
//直接写个 this
btn_ev.setOnClickListener(this);
}
//重写接口中的抽象方法
@Override
public void onClick(View v) {
Toast.makeText(getApplicationContext(), "简单教程,简单编程", Toast.LENGTH_SHORT).show();
}
}
5.直接绑定到标签
就是直接在 xml 布局文件中对应的 Activity 中定义一个事件处理方法
public void msClick(View source)
source 对应事件源( UI 控件类型),接着布局文件中对应要触发事件的 UI 控件设置一个属性
android:onclick = "msClick
2. 基于回调事件处理机制
2.1 什么是回调:
回调就是组件自己会监听自己,如果自己的某些属性改变了,就会调用自己的某些方法,而这些方法又会调用其它组件的某些方法
打个比方,比如我最爱喝 金桔柠檬,我们点好后只要留下 姓 ,然后坐等服务员叫我们就好了,服务员会时时监听我的 金桔柠檬 做好了没,如果做好了,就会说 X 先生,你的金桔柠檬做好了
服务员主动告诉我们 x 先生,金桔柠檬做好了 就是一种回调
Java 可以通过定义回调接口的方式实现:
interface CallBack {
void doSomeThing();
}
class CallBackA implements CallBack {
public void doSomeThing() {
System.out.println("CallerA do something");
}
}
class Caller {
// 持有一个回调接口
private CallBack callback;
public void register(CallBack callback) {
this.callback = callback;
}
public void doSomeThing() {
if (this.callback != null) {
this.callback.doSomeThing();
}
}
}
public class Test {
public static void main(String[] args) {
Caller caller = new Caller();
CallBack callBack = new CallBackA();
caller.register(callBack);
caller.doSomeThing();
}
}
可能有点人会说,这不就是面向接口编程么?还有回调的“回”体现在哪儿?
首先,这段代码的确实现了“将代码作为参数传递的功能”,当我们向传入其他代码的时候,我们可以实现另一个 CallBackB。
那么回调的“回”体现在哪儿呢?我们对上上面稍加改动。
这个例子就很有回调的意思了。。。。。。。。。。。。。。。。
interface CallBack {
void doSomeThing(Caller caller);
}
interface Caller {
void register(CallBack callback);
void close();
void doSomeThing();
}
class CallBackA implements CallBack {
public void doSomeThing(Caller caller) {
System.out.println("CallBackA do something");
caller.close();
}
}
class CallerA implements Caller{
private CallBack callback;
public void register(CallBack callback) {
this.callback = callback;
}
public void doSomeThing() {
if (this.callback != null) {
this.callback.doSomeThing(this);
}
}
public void close() {
System.out.println("CallerA do over");
}
}
public class Test {
public static void main(String[] args) {
Caller caller = new CallerA();
CallBackA callback = new CallBackA();
caller.register(callback);
caller.doSomeThing();
}
}
上面给出的都是同步回调的例子,代码顺序执行,如果 callback 发生阻塞,那么整个程序也就阻塞掉了。如果我们把 CallBackA 的 doSomeThing 方法写成多线程的形式,那么这个回调将会变成异步回调。
interface CallBack {
void doSomeThing(Caller caller);
}
interface Caller {
void register(CallBack callback);
void close();
void doSomeThing();
}
class CallBackA implements CallBack {
public void doSomeThing(Caller caller) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("CallBackA do something");
for (int i = 0; i < 10000000; i++){}
caller.close();
}
}).start();
}
}
class CallerA implements Caller{
private CallBack callback;
public void register(CallBack callback) {
this.callback = callback;
}
public void doSomeThing() {
if (this.callback != null) {
this.callback.doSomeThing(this);
}
}
public void close() {
System.out.println("CallerA do over");
}
public void doOtherThing() {
System.out.println("CallerA do other thing");
}
}
public class Test {
public static void main(String[] args) {
CallerA caller = new CallerA();
CallBackA callback = new CallBackA();
caller.register(callback);
caller.doSomeThing();
caller.doOtherThing();
}
}
可以看到,CallerA 调用了 CallBackA 之后开始 CallBackA 开始工作,然后 CallerA 开始做其他事情,之后 CallBackA 在做完其他事情之后,反过来调用 CallerA 的 close() 方法。这就实现了一个异步回调。
链接: https://ymwdq.github.io/2018/01/24/Java-%E5%9B%9E%E8%B0%83%E6%B5%85%E6%9E%90-1/
2.2 Android 有两个场景可以使用基于回调的事件处理机制
1. 自定义 View
通用的做法是: 继承基础的 UI 控件,重写该控件的事件处理方法
在 xml 布局中使用自定义的 View 时,需要使用 "全限定类名"
Android 中很多 UI 控件都提供了一些是事件回调方法,比如 View,有以下几个方法
回调方法 | 说明 |
---|---|
boolean onTouchEvent(MotionEvent event) | 触摸 UI 控件时触发 |
boolean onKeyDown(int keyCode,KeyEvent event) | 在 UI 控件上按下手指时触发 |
boolean onKeyUp(int keyCode,KeyEvent event) | 在 UI 控件上松开手指时触发 |
boolean onKeyLongPress(int keyCode,KeyEvent event) | 长按组件某个按钮时 |
boolean onKeyShortcut(int keyCode,KeyEvent event) | 键盘快捷键事件发生 |
boolean onTrackballEvent(MotionEvent event) | 在组件上触发轨迹球屏事件 |
void onFocusChanged (boolean gainFocus, int direction, Rect previously FocusedRect) | 当 UI 控件的焦点发生改变 |
例子,自定义 EditText:
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.widget.EditText;
import android.content.Context;
public class MsEditText extends EditText {
private static String TAG = "MsEditText";
public MsEditText(Context context, AttributeSet attrs) {
super(context, attrs);
}
//重写键盘按下触发的事件
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
super.onKeyDown(keyCode,event);
Log.d(TAG, "onKeyDown() 方法被调用");
return true;
}
//重写弹起键盘触发的事件
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
super.onKeyUp(keyCode,event);
Log.d(TAG,"onKeyUp() 方法被调用");
return true;
}
//组件被触摸了
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
Log.d(TAG,"onTouchEvent() 方法被调用");
return true;
}
}
我们可以看到,因为我们直接重写了 EditText 的三个回调方法,当发生点击事件后就不需要添加 事件监听器就可以完成回调
组件会处理对应的事件,即事件由事件源(组件)自身处理
2. 基于回调的事件传播
细心的看一下上表列出的几个事件回调方法,为什么它们的返回值总是 boolean
类型,它有什么用呢?
要回答这个问题,我们就要先搞清楚 Android 中事件处理的流程
- 触发该 UI 的事件监听器
- 触发该 UI 控件提供的回调方法
- 传播到该 UI 组件所在的
Activity
如果三个流程中任意一个返回了 true
就不会继续向外传播,后面的就不会执行了
所以 返回值 boolean 是用来标示这个方法是否已经被完全处理,如果为 false
的话就是没处理完,就会触发该组件所在 Activity
中相关的回调方法
综上,一个事件是否向外传播取决于方法的返回值是时 true
还是 false
传播顺序:
监听器优先
然后到 View 组件自身
最后再到 Activity 任意一个流程返回值 false 继续传播【true 终止传播】
继承View 组件:
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.widget.EditText;
import android.content.Context;
public class MsEditText extends EditText {
private static String TAG = "MsEditText";
public MsEditText(Context context, AttributeSet attrs) {
super(context, attrs);
}
//重写键盘按下触发的事件
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
super.onKeyDown(keyCode,event);
Log.d(TAG, "onKeyDown() 方法被调用");
return false;
}
//重写弹起键盘触发的事件
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
super.onKeyUp(keyCode,event);
Log.d(TAG,"onKeyUp() 方法被调用");
return true;
}
//组件被触摸了
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
Log.d(TAG,"onTouchEvent() 方法被调用");
return true;
}
}
MainActivity.java,包含监听器
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.KeyEvent;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MsEditText et_ev = (MsEditText)findViewById(R.id.et_ev);
et_ev.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if(event.getAction() == KeyEvent.ACTION_DOWN)
{
Log.d("MsEditText","监听器的 onKeyDown() 方法被调用");
}
return false; // 这里是 false,所以会继续传播下去
}
});
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
super.onKeyDown(keyCode, event);
Log.i("MsEditText","Activity 的 onKeyDown() 方法被调用");
return false;
}
}
运行结果:
从运行结果可以看出,我们上面说的三个流程是正确的,传播机制为:
监听器 ---> view 组件的回调方法 ---> Activity的回调方法
3个方法都是: return false
可以说 Android 事件处理机制中的基于回调的事件处理机制的核心就是 事件传播的顺序
四、Notification 状态栏通知
状态栏通知的基本组成
组成元素 | 说明 | 对应方法 | 说明 |
---|---|---|---|
Icon/Photo | 大图标 | setLargeIcon(Bitmap) | 设置左边的大图标 |
Secondary Icon | 小图标 | setSmallIcon(int) | 设置右下角的小图标,在接收到通知的时候顶部也会显示这个小图标 |
Title/Name | 标题 | setContentTitle(Char Sequence) | 设置标题 |
Message | 内容信息 | setContentText(Char Sequence) | 设置内容 |
Timestamp | 通知时间,默认是发出通知的时间,可以通过 setWhen() 设置 | setWhen(long) | 设置通知时间,一般设置的是收到通知时的 System.currentTimeMillis() |
状态通知栏主要涉及到2个类 Notification
和 NotificationManager
类 | 说明 |
---|---|
Notification | 通知信息类,它里面对应了通知栏的各个属性 |
NotificationManager | 是状态栏通知的管理类,负责发通知、清除通知等操作 |
使用的基本流程:
-
获得
NotificationManager
对象NotificationManager mNManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
-
创建一个通知栏的
Builder
构造类Notification.Builder mBuilder = new Notification.Builder(this);
-
对
mBuilde
r 进行相关的设置,比如标题,内容,图标,动作等 -
调用
mBuilder.build()
方法为 notification 赋值 -
调用
NotificationManager.notify()
方法发送通知 -
另外我们还可以调用
NotificationManager.cancel()
方法取消通知
设置相关的方法:
首先我们要创建一个 Builder
Notification.Builder mBuilder = new Notification.Builder(this);
后再调用下述的相关的方法进行设置
方法 | 说明 |
---|---|
setContentTitle(Char Sequence) | 设置标题 |
setContentText(Char Sequence) | 设置内容 |
setSubText(Char Sequence) | 设置内容下面一小行的文字,API 16+ 才可以用 |
setTicker(Char Sequence) | 设置收到通知时在顶部显示的文字信息 |
setWhen(long) | 设置通知时间,一般设置的是收到通知时的 System.currentTimeMillis() |
setSmallIcon(int) | 设置右下角的小图标,在接收到通知的时候顶部也会显示这个小图标 |
setLargeIcon(Bitmap) | 设置左边的大图标 |
setAutoCancel(boolean) | 用户点击 Notification 点击面板后是否让通知取消(默认不取消) |
还可以调用其它方法
-
setDefaults(int)
向通知添加声音、闪灯和振动效果的最简单方法是使用默认 ( defaults ) 属性
可以组合多个属性
属性 说明 Notification.DEFAULT_VIBRATE 添加默认震动提醒 Notification.DEFAULT_SOUND 添加默认声音提醒 Notification.DEFAULT_LIGHTS 添加默认三色灯提醒 Notification.DEFAULT_ALL 添加默认以上3种全部提醒 -
setVibrate(long[])
设置振动方式,比如
setVibrate(new long[] {0,300,500,700});
延迟0ms,然后振动300ms,在延迟500ms,接着再振动700ms
-
setLights(int argb, int onMs, int offMs)
设置三色灯,参数依次是:灯光颜色,亮持续时间,暗的时间
不是所有颜色都可以,这跟设备有关,有些手机还不带三色灯
另外,还需要为 Notification 设置 flags 为 Notification.FLAG_SHOW_LIGHTS 才支持三色灯提醒
-
setSound(Uri)
设置接收到通知时的铃声,可以用系统的,也可以自己设置
获取默认铃声
.setDefaults(Notification.DEFAULT_SOUND)
获取自定义铃声
.setSound(Uri.parse("file:///sdcard/xx/xx.mp3"))
获取Android多媒体库内的铃声
.setSound(Uri.withAppendedPath(Audio.Media.INTERNAL_CONTENT_URI, "5"))
-
setOngoing(boolean)
设置为 ture,表示它为一个正在进行的通知
他们通常是用来表示一个后台任务,用户积极参与(如播放音乐)或以某种方式正在等待,因此占用设备(如一个文件下载,同步操作,主动网络连接)
-
setProgress(int,int,boolean)
设置带进度条的通知
参数依次为:进度条最大数值,当前进度,进度是否不确定
-
如果为确定的进度条:调用
setProgress(max, progress, false)
来设置通知,在更新进度的时候在此发起通知更新progress,并且在下载完成后要移除进度条,通过调用setProgress(0, 0, false)
既可 -
如果为不确定(持续活动)的进度条,这是在处理进度无法准确获知时显示活动正在持续,所以调用
setProgress(0, 0, true)
,操作结束时,调用setProgress(0, 0, false)
并更新通知以移除指示条
-
-
setContentIntent(PendingIntent)
PendingIntent 和 Intent 略有不同,它可以设置执行次数,主要用于远程服务通信、闹铃、通知、启动器、短信中,在一般情况下用的比较少
比如这里通过 Pending 启动 Activity
getActivity(Context, int, Intent, int)
当然还可以启动 Service 或者 Broadcast
PendingIntent 的位标识符(第四个参数) 可以是以下值值之一
值 说明 FLAG_ONE_SHOT 表示返回的 PendingIntent 仅能执行一次,执行完后自动取消 FLAG_NO_CREATE 表示如果描述的 PendingIntent 不存在,并不创建相应的 PendingIntent,而是返回 NULL FLAG_CANCEL_CURRENT 表示相应的 PendingIntent 已经存在,则取消前者,然后创建新的 PendingIntent,这个有利于数据保持为最新的,可以用于即时通信的通信场景 FLAG_UPDATE_CURRENT 表示更新的 PendingIntent 使用示例
//点击后跳转Activity Intent intent = new Intent(context,XXX.class); PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); mBuilder.setContentIntent(pendingIntent)
-
setPriority(int)
设置优先级
优先级 用户 MAX 重要而紧急的通知,通知用户这个事件是时间上紧迫的或者需要立即处理的 HIGH 高优先级用于重要的通信内容,例如短消息或者聊天,这些都是对用户来说比较有兴趣的 DEFAULT 默认优先级用于没有特殊优先级分类的通知 LOW 低优先级可以通知用户但又不是很紧急的事件 MIN 用于后台消息 (例如天气或者位置信息)。最低优先级通知将只在状态栏显示图标,只有用户下拉通知抽屉才能看到内容 对应属性:
Notification.PRIORITY_HIGH
例子:点击按钮显示通知信息
1. 在 MainActivity.java
同一个目录下创建一个通知详情页的 NotifyDetailActivity.java
package cn.twle.android.notification;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class NotifyDetailActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.notify_detail); // 点击通知,这跳转到 notify_detail 页面
}
}
2. 修改 MainActivity.java 文件
点击取消按钮, 调用 setAutoCancel(boolean) 方法,点击 Notification 面板让通知消失
ackage cn.twle.android.notification;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Context mContext;
private NotificationManager mNManager;
private Notification notify1;
Bitmap LargeBitmap = null;
private static final int NOTIFYID_1 = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mContext = MainActivity.this;
//创建大图标的 Bitmap
LargeBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.app_icon_128x128);
mNManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
Button btn_pop_no = (Button) findViewById(R.id.btn_pop_no);
Button btn_clear_no = (Button) findViewById(R.id.btn_clear_no);
btn_pop_no.setOnClickListener(this);
btn_clear_no.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_pop_no:
//定义一个 PendingIntent 点击 Notification 后启动一个 Activity
Intent it = new Intent(mContext, NotifyDetailActivity.class);
PendingIntent pit = PendingIntent.getActivity(mContext, 0, it, 0);
//设置图片,通知标题,发送时间,提示方式等属性
Notification.Builder mBuilder = new Notification.Builder(this);
//标题
mBuilder.setContentTitle("简单教程")
.setContentText("Android 基础教程上线啦") // 内容
.setSubText("简单教程,简单编程") // 内容下面的一小段文字
.setTicker("简单教程上线啦.......") // 收到信息后状态栏显示的文字信息
.setWhen(System.currentTimeMillis()) // 设置通知时间
.setSmallIcon(R.drawable.app_icon_32x32) // 设置小图标
.setLargeIcon(LargeBitmap) // 设置大图标
.setDefaults(Notification.DEFAULT_LIGHTS | Notification.DEFAULT_VIBRATE) //设置默认的三色灯与振动器
.setSound(Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.sms)) //设置自定义的提示音
.setAutoCancel(true) //设置点击后取消Notification
.setContentIntent(pit); //设置PendingIntent
notify1 = mBuilder.build();
mNManager.notify(NOTIFYID_1, notify1);
break;
case R.id.btn_clear_no:
//除了可以根据 ID 来取消 Notification外,还可以调用 ·cancelAll();关闭该应用产生的所有通知
//取消Notification
mNManager.cancel(NOTIFYID_1);
break;
}
}
}
五、AlertDialog 弹出框
对话框是提示用户作出决定或输入额外信息的小窗口。对话框不会填充屏幕,通常用于需要用户采取行动才能继续执行的模式事件
AlertDialog
不同于前面已经学习过的 UI 控件,它不能用 new
方法创造出来,也不能用 XML 创建
我们只能通过 AlertDialog
的内部类 Builder
来创建
AlertDialog.Builder(Context context) AlertDialog.Builder(Context context, int themeResId)
然后调用 AlertDialog
的一些方法进行定制,最后调用 show()
方法来显示
所以,创建一个 AlertDialog
的基本流程是:
创建 AlertDialog.Builder 对象
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
调用
setIcon()
设置图标,setTitle()
或setCustomTitle()
设置标题设置对话框的内容
setMessage()
调用
setPositive/Negative/NeutralButton()
设置 确定,取消,普通 按钮调用
create()
方法创建这个对象,再调用show()
方法将对话框显示出来
使用 AlertDialog
创建几种常见的对话框
下面的创建方式只用于学习目的,一般项目中都是点一个按钮然后触发弹框
1. 普通对话框
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.content.DialogInterface;
import android.content.Context;
import android.app.AlertDialog;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final Context ctx = MainActivity.this;
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setIcon(R.drawable.info)
.setTitle("系统提示")
.setMessage("这是一个最普通的 AlertDialog,\n带有三个按钮,分别是取消,普通和确定");
// 取消按钮
builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Toast.makeText(ctx, "你点击了取消按钮~", Toast.LENGTH_SHORT).show();
}
});
// 确定按钮
builder. setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Toast.makeText(ctx, "你点击了确定按钮~", Toast.LENGTH_SHORT).show();
}
});
// 普通按钮
builder.setNeutralButton("普通按钮", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Toast.makeText(ctx, "你点击了普通按钮~", Toast.LENGTH_SHORT).show();
}
});
AlertDialog alert = builder.create(); // 创建 AlertDialog 对象
alert.show(); // 显示对话框
}
}
2.普通列表对话框
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.content.DialogInterface;
import android.content.Context;
import android.app.AlertDialog;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final String[] lang = new String[]{"Kotlin", "Java", "Python", "PHP", "C#", "Ruby", "Perl"};
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setIcon(R.drawable.info).setTitle("选择你喜欢的开发语言");
builder.setItems(lang, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Toast.makeText(getApplicationContext(), "你选择了" + lang[which], Toast.LENGTH_SHORT).show();
}
});
AlertDialog alert = builder.create();
alert.show();
}
}
3. 单选列表对话框
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.content.DialogInterface;
import android.content.Context;
import android.app.AlertDialog;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final String[] city = new String[]{"北京", "上海", "广州", "深圳", "杭州", "成都", "厦门"};
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setIcon(R.drawable.info).setTitle("选择你想去的城市,只能选一个哦~");
builder.setSingleChoiceItems(city, 0, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Toast.makeText(getApplicationContext(), "你选择了" + city[which], Toast.LENGTH_SHORT).show();
}
});
AlertDialog alert = builder.create();
alert.show();
}
}
4. 多选列表对话框
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.content.DialogInterface;
import android.content.Context;
import android.app.AlertDialog;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final String[] fav = new String[]{"喜剧", "悲剧", "爱情", "动作"};
// 定义一个用来记录个列表项状态的 boolean 数组
final boolean[] checkItems = new boolean[]{false, false, false, false};
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setIcon(R.drawable.info);
builder.setMultiChoiceItems(fav, checkItems, new DialogInterface.OnMultiChoiceClickListener() {
@Override
public void onClick(DialogInterface dialog, int which, boolean isChecked) {
checkItems[which] = isChecked;
}
});
builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String result = "";
for (int i = 0; i < checkItems.length; i++) {
if (checkItems[i])
result += fav[i] + " ";
}
Toast.makeText(getApplicationContext(), "你喜欢:" + result, Toast.LENGTH_SHORT).show();
}
});
AlertDialog dialog = builder.create();
dialog.show();
}
}
六、文件存储和读写
1. 安卓的文件操作模式
在 Java
中,对文件的读写,几乎只要新建文件,就可以写入数据
但 Android 却不一样,因为 Android 是基于 Linu x的,我们在读写文件的时候,还需加上文件的 操作模式
Android 在类 android.content.Context
下定义了 2 个操作模式常量
模式 | 说明 |
---|---|
Context.MODE_PRIVATE | 默认的操作模式,表示该文件是私有文件,只能够被应用本身访问,写入的内容会覆盖原有的数据 |
Context.MODE_APPEND | 会检查文件是否存在,如果存在则把内容追加到文件末尾,否则新建一个文件进行写 |
2. 在APP 包下操作文件
例子:尝试在当前包下创建文件 site.txt
并写入内容
a. 创建一个页面
<?xml version="1.0" encoding="UTF-8" ?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="请输入文件名"/>
<EditText
android:id="@+id/ms_filename"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="文件名"
android:text="site.txt"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="请输入文件内容" />
<EditText
android:id="@+id/ms_filedata"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="简单教程,简单编程"
android:hint="文件内容" />
<Button
android:id="@+id/btn_save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="保存" />
<Button
android:id="@+id/btn_clean"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="清空" />
<Button
android:id="@+id/btn_read"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="读取文件" />
</LinearLayout>
b. 新建文件 FileHelper.java
作为读写 SD 卡的帮助类
import android.content.Context;
import java.io.IOException;
import java.io.FileOutputStream;
import java.io.FileInputStream;
public class FileHelper {
private Context mContext;
public FileHelper() {
}
public FileHelper(Context mContext) {
super();
this.mContext = mContext;
}
/*
* 这里定义的是一个文件保存的方法,写入到文件中,所以是输出流
* */
public void save(String filename, String filecontent) throws Exception {
//这里我们使用私有模式MODE_PRIVATE,创建出来的文件只能被本应用访问,还会覆盖原文件
FileOutputStream output = mContext.openFileOutput(filename, Context.MODE_PRIVATE);
output.write(filecontent.getBytes()); //将String字符串以字节流的形式写入到输出流中
output.close(); //关闭输出流
}
/*
* 这里定义的是文件读取的方法
* */
public String read(String filename) throws IOException {
//打开文件输入流
FileInputStream input = mContext.openFileInput(filename);
byte[] temp = new byte[1024];
StringBuilder sb = new StringBuilder("");
int len = 0;
//读取文件内容:
while ((len = input.read(temp)) > 0) {
sb.append(new String(temp, 0, len));
}
//关闭输入流
input.close();
return sb.toString();
}
}
c. MainActivity 文件
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import java.io.IOException;
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
private EditText ms_filename;
private EditText ms_filedata;
private Button btn_save;
private Button btn_clean;
private Button btn_read;
private FileHelper helper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
helper = new FileHelper(getApplicationContext());
bindViews();
}
private void bindViews() {
ms_filename = (EditText) findViewById(R.id.ms_filename);
ms_filedata = (EditText) findViewById(R.id.ms_filedata);
btn_save = (Button) findViewById(R.id.btn_save);
btn_clean = (Button) findViewById(R.id.btn_clean);
btn_read = (Button) findViewById(R.id.btn_read);
btn_save.setOnClickListener(this);
btn_clean.setOnClickListener(this);
btn_read.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.btn_clean:
ms_filename.setText("");
ms_filedata.setText("");
break;
case R.id.btn_save:
String filename = ms_filename.getText().toString();
String filedetail = ms_filedata.getText().toString();
try
{
helper.save(filename, filedetail);
Toast.makeText(getApplicationContext(), "数据写入成功", Toast.LENGTH_SHORT).show();
}
catch(Exception e){
e.printStackTrace();
Toast.makeText(getApplicationContext(), "数据写入失败", Toast.LENGTH_SHORT).show();
}
break;
case R.id.btn_read:
String detail = "";
try
{
String filename2 = ms_filename.getText().toString();
detail = helper.read(filename2);
}
catch(IOException e){e.printStackTrace();}
Toast.makeText(getApplicationContext(), detail, Toast.LENGTH_SHORT).show();
break;
}
}
}
3. 读取SD 卡文件
文件读写的步骤:
-
读写前判断 SD 卡是是否插入,是否可读写
Enviroment.getExternalStorageState().equals(Enviroment.MEDIA_MOUNTED);
-
获取 SD 卡的外部目录,同时获得 SD 卡路径
Enviroment.getExternalStorageDirectory().getCanonicalPath();
-
使用
FileOutputStream
、FileInputStream
、FileReader
或FileWriter
读写 SD 卡 -
在
AndroidManifest.xml
中添加 SD 权限: 创建删除文件权限 和 读写数据权限<!-- 在 SD 卡中创建与删除文件权限 --> <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/> <!-- 往 SD 卡写入数据权限 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
读写 SD 卡的常用代码:
-
获取SD卡的根目录
String sdcardRoot = Environment.getExternalStorageDirectory().getAbsolutePath();
-
在 SD 卡上创建文件夹目录
public File createDirOnSDCard(String dir) { File dirFile = new File(sdCardRoot + File.separator + dir +File.separator); Log.v("createDirOnSDCard", sdCardRoot + File.separator + dir +File.separator); dirFile.mkdirs(); return dirFile; }
-
在 SD 卡上创建文件
public File createFileOnSDCard(String fileName, String dir) throws IOException { File file = new File(sdCardRoot + File.separator + dir + File.separator + fileName); Log.v("createFileOnSDCard", sdCardRoot + File.separator + dir + File.separator + fileName); file.createNewFile(); return file; }
-
判断文件是否存在于 SD 卡的某个目录
public boolean isFileExist(String fileName, String path) { File file = new File(sdCardRoot + path + File.separator + fileName); return file.exists(); }
-
将数据写入到 SD 卡指定目录文件
public File writeData2SDCard(String path, String fileName, InputStream data) { File file = null; OutputStream output = null; try { createDirOnSDCard(path); //创建目录 file = createFileOnSDCard(fileName, path); //创建文件 output = new FileOutputStream(file); byte buffer[] = new byte[2*1024]; //每次写2K数据 int temp; while((temp = data.read(buffer)) != -1 ) { output.write(buffer,0,temp); } output.flush(); } catch (Exception e) { e.printStackTrace(); } finally{ try { output.close(); //关闭数据流操作 } catch (Exception e2) { e2.printStackTrace(); } } return file; }