安卓的Handler机制、AsyncTask 、Toast和事件监听机制

一. Handler 的使用方法

1. Handler 作用

在开发中,我们经常会需要做一些耗时的操作:比如下载图片、打开网页、下载视频等。如果将这些耗时的操作放在主线程(UI线程),长时间的阻塞导致应用ANR。必然应该将这些操作放在子线程中处理,这些操作处理过程中,我们需要更新UI界面以告知用户现在具体的进度、状态等信息。

所以:在多线程的应用场景中,将工作线程中需更新UI的操作信息 传递到 UI主线程,从而实现 工作线程对UI的更新处理,最终实现异步消息的处理

示意图

但是,多个线程并发执行UI 主线程,会导致数据更新异常,使用 Handler 的作用时: 多个线程并发更新UI的同时 保证线程安全

2. Handler 相关的名词

Handler 作为主线程和 工作线程的一个媒介

HandlerMessageMessage QueueLooper

示意图

3. 使用方法

在子线程中Handler将消息发送到MessageQueue中,然后Looper不断的从MessageQueue中读取消息,并调用Handler的dispatchMessage发送消息,最后再Handler来处理消息。为了更好的帮助大家一起理解,我画了一个Handler机制的原理图:

解析Android中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()

用户取消线程操作的时候调用,也就是在主线程调用 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 几点注意事项

  1. Task 的实例必须在 UI 线程中创建
  2. execute() 方法必须在 UI 线程中调用
  3. 不要手动调用 onPreExecute() 、doInBackground(params...) 、onPostExecuteonCancelled() 这几个方法
  4. Task 只能执行一次,运行多次会出现异常
  5. 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 提供了两套事件处理机制

  1. 基于监听的事件处理

    这种事件处理方式就是给 Android UI 控件绑定特定的事件监听器

  2. 基于回调的事件处理

    这种事件处理方式就是重写 Android UI 控件的特定事件回调方法,或者重写 Activity 特定的回调方法

一般情况下我们推荐使用 基于回调的事件处理 ,但特殊情况下,只能使用 基于监听的事件处理

1. 基于监听的事件处理机制

事件监听机制中由 事件源 , 事件 , 事件监听器 三类对象组成

三者之间的关系和 基于监听的事件处理机制 的一般流程如下图

  1. 为某个事件源(组件)设置一个监听器,用于监听用户操作
  2. 用户的操作,触发了事件源的监听器
  3. 生成了对应的事件对象
  4. 将这个事件源对象作为参数传给事件监听器
  5. 事件监听器对事件对象进行判断,执行对应的事件处理器(对应事件的处理方法)

我们可以使用五种方式给一个 UI 控件或 Activity 添加基于监听的事件

  1. 直接用匿名内部类 (推荐)
  2. 使用内部类
  3. 使用外部类
  4. 直接使用 Activity 作为事件监听器 (常用)
  5. 直接绑定到标签

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 中事件处理的流程

  1. 触发该 UI 的事件监听器
  2. 触发该 UI 控件提供的回调方法
  3. 传播到该 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是状态栏通知的管理类,负责发通知、清除通知等操作

使用的基本流程:

  1. 获得 NotificationManager 对象

    NotificationManager mNManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
    
  2. 创建一个通知栏的 Builder 构造类

    Notification.Builder mBuilder = new Notification.Builder(this);
    
  3. 对 mBuilder 进行相关的设置,比如标题,内容,图标,动作等

  4. 调用 mBuilder.build() 方法为 notification 赋值

  5. 调用 NotificationManager.notify() 方法发送通知

  6. 另外我们还可以调用 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 点击面板后是否让通知取消(默认不取消)

还可以调用其它方法

  1. setDefaults(int)

    向通知添加声音、闪灯和振动效果的最简单方法是使用默认 ( defaults ) 属性

    可以组合多个属性

    属性说明
    Notification.DEFAULT_VIBRATE添加默认震动提醒
    Notification.DEFAULT_SOUND添加默认声音提醒
    Notification.DEFAULT_LIGHTS添加默认三色灯提醒
    Notification.DEFAULT_ALL添加默认以上3种全部提醒
  2. setVibrate(long[])

    设置振动方式,比如

    setVibrate(new long[] {0,300,500,700});
    

    延迟0ms,然后振动300ms,在延迟500ms,接着再振动700ms

  3. setLights(int argb, int onMs, int offMs)

    设置三色灯,参数依次是:灯光颜色,亮持续时间,暗的时间

    不是所有颜色都可以,这跟设备有关,有些手机还不带三色灯

    另外,还需要为 Notification 设置 flags 为 Notification.FLAG_SHOW_LIGHTS 才支持三色灯提醒

  4. setSound(Uri)

    设置接收到通知时的铃声,可以用系统的,也可以自己设置

    获取默认铃声

    .setDefaults(Notification.DEFAULT_SOUND)
    

    获取自定义铃声

    .setSound(Uri.parse("file:///sdcard/xx/xx.mp3"))
    

    获取Android多媒体库内的铃声

    .setSound(Uri.withAppendedPath(Audio.Media.INTERNAL_CONTENT_URI, "5"))
    
  5. setOngoing(boolean)

    设置为 ture,表示它为一个正在进行的通知

    他们通常是用来表示一个后台任务,用户积极参与(如播放音乐)或以某种方式正在等待,因此占用设备(如一个文件下载,同步操作,主动网络连接)

  6. setProgress(int,int,boolean)

    设置带进度条的通知

    参数依次为:进度条最大数值,当前进度,进度是否不确定

    1. 如果为确定的进度条:调用 setProgress(max, progress, false) 来设置通知,在更新进度的时候在此发起通知更新progress,并且在下载完成后要移除进度条,通过调用 setProgress(0, 0, false) 既可

    2. 如果为不确定(持续活动)的进度条,这是在处理进度无法准确获知时显示活动正在持续,所以调用 setProgress(0, 0, true),操作结束时,调用 setProgress(0, 0, false) 并更新通知以移除指示条

  7. 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)
    
  8. 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 的基本流程是:

  1. 创建 AlertDialog.Builder 对象

    AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
    
  2. 调用 setIcon() 设置图标, setTitle() 或 setCustomTitle() 设置标题

  3. 设置对话框的内容 setMessage()

  4. 调用 setPositive/Negative/NeutralButton() 设置 确定取消普通 按钮

  5. 调用 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 卡文件

文件读写的步骤:

  1. 读写前判断 SD 卡是是否插入,是否可读写

    Enviroment.getExternalStorageState().equals(Enviroment.MEDIA_MOUNTED);
    
  2. 获取 SD 卡的外部目录,同时获得 SD 卡路径

    Enviroment.getExternalStorageDirectory().getCanonicalPath();
    
  3. 使用 FileOutputStream 、FileInputStream 、FileReader 或 FileWriter 读写 SD 卡

  4. 在 AndroidManifest.xml 中添加 SD 权限: 创建删除文件权限 和 读写数据权限

    <!-- 在 SD 卡中创建与删除文件权限 -->
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
    <!-- 往 SD 卡写入数据权限 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

例子链接

读写 SD 卡的常用代码:

  1. 获取SD卡的根目录

    String  sdcardRoot = Environment.getExternalStorageDirectory().getAbsolutePath();

  2. 在 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;  
    }

  3. 在 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;  
    }

  4. 判断文件是否存在于 SD 卡的某个目录

    public boolean isFileExist(String fileName, String path)  
    {  
        File file = new File(sdCardRoot + path + File.separator + fileName);  
        return file.exists();  
    }

  5. 将数据写入到 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;  
    }

七、SharedPreference 保存用户数据

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值