《第一行代码 第二版》Android studio java开发学习笔记+源码

持续更新中…

zcy 2021/8/14

文章目录

一、开始启程

1. 认识Android

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

Android系统四大组件分别是Activity、Service、BroadcastReceiver和 ContentProvider

Android系统还自带了这种轻量级、运算速度极快的嵌入式关系型数据库, SQLite数据库,它不仅支持标准 的SQL语法,还可以通过Android封装好的API进行操作

2. 创建项目

image-20210814140036526

Package name:表示项目的包名,Android系统就是通过包名来区分不同应用程序的,因此包名一定要具有唯一性

Language:这里默认选择了Kotlin。在过去,Android应用程序只能使用 Java来进行开发,本书的前两个版本也都是用Java语言讲解的。然而在2017年,Google引入 了一款新的开发语言——Kotlin,并在2019年正式向广大开发者公布了Kotlin First的消息

Minimum API level:设置项目的最低兼容版本,Android 5.0以 上的系统已经占据了超过85%的Android市场份额,因此这里我们将Minimum SDK指定成API 21就可以了

3. 分析第一个Android程序结构

3.1 Project模式的项目结构

image-20210814115507396

.gradle和.idea:放置的都是Android Studio自动生成的一些文件

app:项目中的代码、资源等内容都是放置在这个目录下的,我们后面的开发工作也基本是在这 个目录下进行的

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

gitignore:用来将指定的目录或文件排除在版本控制之外的

build.gradle:项目全局的gradle构建脚本

gradle.properties:全局的gradle配置文件

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

local.properties:指定本机中的Android SDK路径

settings.gradle:指定项目中所有引入的模块

3.2 app目录下的结构

image-20210814135809352

build:包含了一些在编译时自动生成的文件

libs:放置第三方jar包

androidTest:编写测试用例

java:放置我们所有Java代码的地方(Kotlin代码也放在这里)

res:项目中使用到的所有图片、布局、字符串等资源

AndroidManifest.xml:是整个Android项目的配置文件,你在程序中定义的所有四大组件都需要在这个文件里注册

test:编写Unit Test测试用例

接下来分析一下HelloWorld项目究竟是如何运行起来的:

首先打开 AndroidManifest.xml,这段代码表示对MainActivity进行注册,intent-filter里的两行代码表示MainActivity是这个项目的主Activity

<activity android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

打开 MainActivity, 首先MainActivity是继承自AppCompatActivity (AndroidX中提供的一种向下兼容的Activity),MainActivity中有一个onCreate()方法,里面调用了setContentView()方法,给当前的Activity引入了一个activity_main布局

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

打开 activity_main.xml,在 TextView中看到了“Hello World”的字样,因为Android程序的设计讲究逻辑和视图分离,不推荐在Activity中直接编写界面,而是在布局文件中编写界面

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" />

</androidx.constraintlayout.widget.ConstraintLayout>
3.3 res目录下的结构

image-20210814141631508

以“drawable”开头的目录:用来放图片的

以“mipmap”开头的目录:用来放应用图标的

以“values”开头的目录:放字符串、样式、颜色等配置的

以“layout”开头的目录:放布局文件

我们应该如何使用这些资源呢,以字符串为例,这里定义了一个应用程序名的字符串,我们有以下两种方式来引用它

image-20210814142241326

  1. 在代码中通过R.string.app_name可以获得该字符串的引用。
  2. 在XML中通过@string/app_name可以获得该字符串的引用

4. 一些error解决方法

ERROR: SSL peer shut down incorrectly错误解决(Android Studio)

错误信息:ERROR: SSL peer shut down incorrectly

错误原因:是studio工具不支持https请求

方法一:右上角 SDK Manager 进入到窗口里面 → 选择 SDK Update Sites 这个选项 → 勾选下方的 Force https// sources to be fetched using http// 选项 → 重启Android Studio → 点击右上大象图标重新下载

image-20210814140531446

方法二:

将 gradle-wrapper.properties 文件里面的 distributionUrl=https 中的 https 改成 http,然后重新点击上方的按钮大象重新下载gradle文件

image-20210814140708805

二、探究Activity

Activity是一种可以包含用户界面的组件,主要用于和用户进行交互

1. 活动的基本用法

1.1 创建活动

image-20210814162357857

Generate Layout File:会自动创建一个对应的布局文件

Launcher Activity:会自动将这个Activity设置为当前项目的主Activity

项目中的任何Activity都应该重写onCreate()方法,调用setContentView()方法来给当前的Activity加载一个布局,我们一般会传入一个布局文件的id

public class SecondActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
    }
}
1.2 Toast

在程序中可以使用它将一些短小的信息通知给用户,一段时间后自动消失

在activity_main中添加一个button,并赋予id button_1

<Button
    android:id="@+id/button_1"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="click"/>

在 onCreate() 方法中添加如下代码:

通过 findViewById() 获取在布局文件中定义的button_1,该方法返回的是一个继承自View的泛型对象,因此需要向下转型成Button对象

通过调用 setOnClickListener() 方法为按钮注册一个监听器,点击按钮时就会执行监听器中的 onClick() 方法,Toast的功能在 onClick() 方法中编写了

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    Button btn = (Button) findViewById(R.id.button_1);
    btn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Toast.makeText(MainActivity.this,"you clicked btn1",
                           Toast.LENGTH_SHORT).show();
        }
    });
}

Toast的用法:

通过静态方法 makeText() 创建出一个Toast对象,然后调用 show() 将Toast显示出来

第一个参数是Context,就是Toast要求的上下文,Activity本身就是一个Context对象,因此这里直接传入this。第二个参数是Toast显示的文本内容。第三个参数是Toast显示的时长,有两个内置常量可以选择:Toast.LENGTH_SHORT 和 Toast.LENGTH_LONG

1.3 Menu

在res目录下新建一个menu文件夹,在menu文件夹下新建一个菜单文件 main.xml,代码如下,我们用 创建了两个菜单项

<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/add_item"
        android:title="add"></item>
    <item
        android:id="@+id/remove_item"
        android:title="remove"></item>
</menu>

接着回到 MainActivity 来重写 onCreateOptionsMenu() 方法,getMenuInflater() 方法能够得到一 个MenuInflater 对象,再调用它的 inflate() 方法,就可以给当前Activity创建菜单了

inflate() 两个参数:第一个参数指定我们通过哪一个资源文件来创建菜单,第二个参数指定菜单项将添加到哪一个Menu对象当中,这里直接使用 onCreateOptionsMenu() 方法中传入的menu参数

返回 true,表示允许创建的菜单显示出来

@Override
public boolean onCreateOptionsMenu(Menu menu){
    getMenuInflater().inflate(R.menu.main,menu);
    return true;
}

我们继续给菜单定义响应事件

在 MainActivity中重写 onOptionsItemSelected() 方法,调用item.itemId来判断点击的是哪一个菜单项

@Override
public boolean onOptionsItemSelected(MenuItem item){
    switch (item.getItemId()) {
        case R.id.add_item:
            Toast.makeText(this, "You clicked Add",
                           Toast.LENGTH_SHORT).show();
            break;
        case R.id.remove_item:
            Toast.makeText(this, "You clicked Remove",
                           Toast.LENGTH_SHORT).show();
            break;
        default:
    }
    return true;
}

效果图如下

image-20210814172207794

1.4 销毁一个活动

按一下Back键,或者z在代码里使用 finish() 方法

btn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        finishi();
    }
});

2. Intent 在活动之间穿梭

由一个Activity跳转到另一个Activity

2.1 Intent简介

Intent是Android程序中各组件之间进行交互的一种方式,不仅可以指明当前组件想要执行的动作,还可以在不同组件之间传递数据,一般可用于启动Activity、启动Service以及发送广播等场景

2.2 显式Intent

新建 SecondActivity.java 和 acitvity_second.xml,在布局文件里加上按钮id button_2

通过 Intent() 构造函数就可以构建出 Intent 对象的“意图”,第一个参数传入 MainActivity.this 作为上下文,第二个参数传入 SecondActivity.class 作为目标 Activity

Activity类中提供了一个 startActivity() 方法,接收一个Intent参数,专门用于启动Activity

Button btn = (Button) findViewById(R.id.button_1);
btn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Intent intent = new Intent(MainActivity.this,SecondActivity.class);
        startActivity(intent);
    }
});
2.3 隐式Intent

指定了一系列的action和category等信息,交由系统去分析这个Intent,找出合适的Activity去启动

打开 AndroidManifest.xml,通过在标签 下配置 的内容,可以指定 SecondActivity 能够响应的 action 和 category

<activity android:name=".SecondActivity">
    <intent-filter>
        <action android:name="com.example.chapter2.ACTION_START" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>

修改MainActivity中按钮的点击事件,只有 和 中的内容同时匹配Intent构造函数中指定的action和category时,这个Activity才能响应该Intent

btn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Intent intent = new Intent("com.example.chapter2.ACTION_START");
        startActivity(intent);
    }
});

这里没有指定 category 是因为 DEFAULT 是一种默认的 category,调用函数时会自动添加进去

每个Intent中只能指定一个action,但能指定多个 category,在 MainActivity.java 里面调用 intent.addCategory(),再在SecondActivity的 加上一个新的 ,否则会报错

Intent intent = new Intent(MainActivity.this,SecondActivity.class);
intent.addCategory("com.example.chapter2.MY_CATEGORY");
startActivity(intent);
<action android:name="com.example.chapter2.ACTION_START" />
<category android:name="com.example.chapter2.MY_CATEGORY"/>
<category android:name="android.intent.category.DEFAULT" />
2.4 更多隐式Intent的用法

不仅可以启动自己程序内的Activity,还可以启动其他程序的Activity

  • Intent.ACTION_VIEW

    首先指定了Intent的action是Intent.ACTION_VIEW,然后通过==Uri.parse()==方法将一个网址字符串解析成一个Uri对象,再调用Intent的 setData() 方法将这个Uri对象传递进去

    btn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Intent intent = new Intent(Intent.ACTION_VIEW);
            intent.setData(Uri.parse("https://www.baidu.com"));
            startActivity(intent);
        }
    });
    
  • Intent.ACTION_DIAL

    btn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Intent intent = new Intent(Intent.ACTION_DIAL);
            intent.setData(Uri.parse("tel:10086"));
            startActivity(intent);
        }
    });
    

    点击按钮效果如图

    image-20210815104512937

2.5 向下一个activity传递数据

Intent中提供了一系列 putExtra() 方法的重载,可以把我们想要传递的数据暂存在Intent中,在启动另一个Activity后,只需要把这些数据从Intent中取出就可以了

例如要把MainActivity中的字符串传递到SecondActivity中:

使用显式Intent的方式来启动SecondActivity,并通过 putExtra() 方法传递了一个字符串,第一个参数是键,用于之后从 Intent 中取值,第二个参数才是真正要传递的数据

btn1.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        String data = "this is from MainActivity";
        Intent intent = new Intent(MainActivity.this,SecondActivity.class);
        intent.putExtra("extra_data",data);
        startActivity(intent);
    }
});

在 SecondActivity中,调用父类的 getIntent() 方法,获取用于启动 SecondActivity的Intent,然后调用 getStringExtra() 方法传入相应的键值

传递字符串:getStringExtra()

传递整型数据:getIntExtra()

传递布尔值:getBooleanExtra()

public class SecondActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        
        Intent intent = getIntent();
        String data = intent.getStringExtra("extra_data");
        Log.d("SecondActivity",data);
    }
}
2.6 返回数据给上一个活动

Activity有一个用于启动Activity的 startActivityForResult() 方法,它期望在Activity销毁的时候能够返回一个结果给上一个Activity

① 修改MainActivity代码

startActivityForResult() 来启动活动,第一个参数还是Intent,第二个参数是请求码,用于在之后的回调中判断数据的来源

btn1.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Intent intent = new Intent(MainActivity.this,SecondActivity.class);
        startActivityForResult(intent,1);
    }
});

在 SecondActivity被销毁之后会回调上一个Activity的 onActivityResult() 方法,第一个参数是在启动 Activity 时传入的请求码,第二个参数是在返回数据时传入的处理结果;第三个参数即携带着返回数据的Intent

@Override
protected void onActivityResult(int rqCode, int resCode, Intent data) {
    switch (rqCode) {
        case 1:
            if(resCode == RESULT_OK) {
                String returnData = data.getStringExtra("data_return");
                Log.d("MainActivity",returnData);
            }
    }
}

② 修改SecondActivity代码:

构建一个Intent对象,仅用来传递数据,不指定任何意图

后调用了==setResult()==方法,专门用于向上一个Activity返回数据。第一个参数用于向上一个Activity返回处理结果(RESULT_OK 或 RESULT_CANCELED),第二个参数把带有数据的Intent传递回去

Button btn2 = (Button) findViewById(R.id.button_2);
btn2.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Intent intent = new Intent();
        intent.putExtra("data_return","this is from SecondActivity");
        setResult(RESULT_OK,intent);
        finish();
    }
});

如果用户在SecondActivity中并不是通过点击按钮,而是通过按下Back键回到 FirstActivity,通过在SecondActivity中重写 onBackPressed() 方法来解决

@Override
public void onBackPressed(){
    Intent intent = new Intent();
    intent.putExtra("data_return","this is from SecondActivity");
    setResult(RESULT_OK,intent);
    finish();
}

3. 活动的生命周期

3.1 返回栈

Android中的Activity是可以层叠的,新活动覆盖在旧活动上

Android是使用任务(task)来管理Activity的,一个任务就是一组存放在栈里的Activity 的集合,这个栈称作返回栈(back stack)

image-20210815114237461

3.2 Activity状态
  • 运行:位于返回栈的顶部,系统最不愿意回收
  • 暂停:不在栈顶,但仍可见(例如对话框的背后),暂停状态仍是完全存活的,系统不愿意回收
  • 停止:不在栈顶,完全不可见,系统仍保为这个活动保存状态和成员变量,但在内存紧张会被回收
  • 销毁:从返回栈移除,系统倾向回收,节约内存
3.3 Activity生存期

完整生存期 ,onCreate —— onDestroy,初始化 —— 释放内存

可见生存期,onStart —— onStop,不可见 —— 可见时调用

前台展示生存期,onResume —— onPause

  • onCreate,初始化,比如加载布局、绑定事件

  • onStart,不可见到可见时调用

  • onResume,和用户交互前进行调用,此时一定位于栈顶,处于运行状态

  • onPause,在系统切换到另一个ACT时调用

  • onStop,完全不可见时调用

  • onDestroy,在销毁前被调用

  • onRestart,从停止变为运行时调用,也就是Activity 被重新启动了

image-20210815115026563

3.4 活动被回收了怎么办

Activity被回收前,系统会调用一个方法==onSaveInstanceState()==来保存用户的数据,在 onPause 和onStop 之间,解决活动被回收临时数据得不到保留的问题

在 MainActivity 中写入函数

@Override
protected void onSaveInstanceState(Bundle outState){
    super.onSaveInstanceState(outState);
    String temp = "something you just typed";
    outState.putString("data_kay",temp);
}

我们一直使用的onCreate()方法也有一个Bundle参数,如果在Activity被系统回收之前,通过onSaveInstanceState()方法保存数据,这个参数就会带有之前保存的全部数据,只需要将数据取出即可

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    if(savedInstanceState != null){
        String temp = savedInstanceState.getString("data_key");
        log.d(TAG,temp);
    }
}

4. 活动的启动模式

在实际项目中我们应该根据特定的需求为每个Activity指定恰当的启动模式,可以在AndroidManifest.xml中通过给标签指定 android:launchMode 属性来选择启动模式

  • standard,默认的启动模式,每次创建都会产生一个新的,放在栈顶,不在乎以前是否创建过(单体可循环,默认情况)

  • singleTop,每次创建时,如果发现栈顶是本身,就不再创建,否则就创建(交替可循环)

    android:launchMode="singleTop"
    
  • singleTask,每次启动,系统首先会在返回栈中检查是否存在该Activity,如果已经存在则直接使用该实例, 并把在这个Activity之上的所有其他Activity统统出栈

  • singleInstance,真正单态模式,自身拥有一个返回栈

三、UI开发的点滴

1. 常用控件的使用方法

1.1 TextView
<TextView
          android:id="@+id/textView"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:gravity="center"
          android:text="This is TextView"/>

android:id 定义唯一标识符

android:text 文本内容

android:textColor 文字的颜色

android:textSize 文字的大小,文字大小使用sp作为单位

android:layout_width 控件的宽度

android:layout_height 控件的高度(match_parent、wrap_content、固定值,单位一般用dp)

android:gravity 文字的对齐方式(top、bottom、start、 end、center、center_vertical、center_horizontal)

1.2 Button
<Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Button" />

android:textAllCaps=“false” 系统就会保留你指定的原始文字内容

在MainActivity中为Button的点击事件注册一个监听器

Button btn1 = (Button) findViewById(R.id.button_1);
btn1.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        //在此处添加逻辑
    }
});

如果不喜欢匿名注册,也可以使用实现接口的方式来进行注册

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    Button btn1 = (Button) findViewById(R.id.button_1);
    btn1.setOnClickListener(this);
}

@Override
public void onClick(View v) {
    switch (v.getId()) {
        case R.id.button_1:
            //在此处添加逻辑
            break;
        default:
            break;
    }
}
1.3 EditText

允许用户在控件里输入和编辑内容,并可以在程序中对这些内容进行处理

<EditText
          android:id="@+id/editText"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:hint="Type something here"
          android:maxLines="2"
          />

android:hint 指定一段提示性的文本

android:maxLines 指定了EditText的最大行数

通过点击按钮获取EditText中输入的内容:

调用EditText的 getText() 方法获取输入的内容,再调用toString() 方法将内容转换成字符串

private EditText editText;
private Button btn;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    editText = (EditText) findViewById(R.id.edit_Text);
    btn = (Button) findViewById(R.id.button_1);
    btn.setOnClickListener(this);
}

@Override
public void onClick(View v) {
    switch (v.getId()) {
        case R.id.button_1:
            String input = editText.getText().toString();
            Toast.makeText(MainActivity.this,input,Toast.LENGTH_SHORT).show();
            break;
        default:
            break;
    }
}

调用EditText的 setText() 方法设置文本框内容

TextView t1 = findViewById(R.id.text_view);
t1.setText("test123);
1.4 ImageView

图片通常是放在以drawable开头的目录下,如果名称是纯数字会红色下划线报错

<ImageView
           android:id="@+id/img_view"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:src="@drawable/img1"
           />

宽和高都设定为wrap_content,保证了不管图片的尺寸是多少,都可以完整地展示出来

在按钮的点击事件里,可以动态地更改ImageView中的图片,通过调用ImageView的 setImageResource() 方法将显示的图片改成 img2

private Button btn;
private ImageView img;
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    btn = (Button) findViewById(R.id.button_1);
    img = (ImageView) findViewById(R.id.img_view); 
    btn.setOnClickListener(this);
}

@Override
public void onClick(View v) {
    switch (v.getId()) {
        case R.id.button_1:
            img.setImageResource(R.drawable.img2);
            break;
        default:
            break;
    }
}
1.5 ProgressBar

在界面上显示一个进度条,会看到屏幕中有一个圆形进度条正在旋转

<ProgressBar
             android:id="@+id/progressBar"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             style="?android:attr/progressBarStyleHorizontal"
             android:max="100"
             />

style 可以将它指定成水平进度条或圆形进度条

android:max 给进度条设置一个最大值

android:visibility 可见属性,可选值有 visible、invisible、gone

setVisibility() 允许传入 View.VISIBLE、 View.INVISIBLE、View.GONE

getVisibility() 来判断ProgressBar是否可见

下面尝试实现一种效果:点击一下按钮让进度条消失,再点击一下按钮让进度条出现

private Button btn;
private ProgressBar prb;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    btn = (Button) findViewById(R.id.button_1);
    prb = (ProgressBar) findViewById(R.id.progress_bar); 
    btn.setOnClickListener(this);
}

@Override
public void onClick(View v) {
    switch (v.getId()) {
        case R.id.button_1:
            if (prb.getVisibility() == View.GONE) {
                prb.setVisibility(View.VISIBLE);
            } else {
                prb.setVisibility(View.GONE);
            }
            break;
        default:
            break;
    }
}

尝试一种效果:给进度条设置一个最大值,然后在代码中动态地更改进度条的进度

@Override
public void onClick(View v) {
    switch (v.getId()) {
        case R.id.button_1:
            int prog = prb.getProgress();
            prog = prog + 10;
            prb.setProgress(prog);
            break;
        default:
            break;
    }
}
1.6 AlertDialog

在当前界面弹出一个对话框,重载 onClick() 函数

首先通过AlertDialog.Builder创建一个AlertDialog实例,为这个对话框设置标题、内容、可否使用Back键关闭对话框等属性

调用 setPositiveButton() 设置确定按钮的点击事件,调用 setNegativeButton() 设置取消按钮的点击事件,最后调用 show() 方法将对话框显示出来

@Override
public void onClick(View v) {
    switch (v.getId()) {
        case R.id.button_1:
            AlertDialog.Builder dialog = new AlertDialog.Builder(MainActivity.this);
            dialog.setTitle("dialog title test");
            dialog.setMessage("dialog message test");
            dialog.setCancelable(false);
            
            dialog.setPositiveButton("YES", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialogInterface, int i) {
                    //设置确定按钮点击事件
                }
            });
            dialog.setNegativeButton("NO", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialogInterface, int i) {
                    //设置取消按钮点击事件
                }
            });
            dialog.show();
            break;
        default:
            break;
    }
}

效果如图:

image-20210816110927342

2. 详解3种基本布局

常用控件和布局的继承结构:

的所有控件都是直接或间接继承自View的,所用的所有布局都是直接或间接继承自ViewGroup的
image-20210816132753036

2.1 LinearLayout

会将它所包含的控件在线性方向上依次排列

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
</LinearLayout>

android:orientation 垂直排列是vertical,水平排列是horizontal

android:layout_gravity 对齐方式,垂直方向(top、center_vertical、bottom)水平方向(left、right、center_horizontal)

android:layout_weight 来指定控件的大小,此时可以将宽度设定为0dp

注意:如果LinearLayout的排列方向是horizontal,控件宽度不能为match_parent,否则单独一个控件就会将整个水平方向占满,如果LinearLayout的排列方向是vertical,控件高度不能为为match_parent

注意:当LinearLayout的排列方向是horizontal时,只有垂直方向上的对齐方式才会生效,同理,是vertical时,只有 水平方向上的对齐方式才会生效

控件对齐方式举例:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="horizontal"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
    <Button
            android:id="@+id/button1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="top"
            android:text="Button 1" />
    <Button
            android:id="@+id/button2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:text="Button 2" />
    <Button
            android:id="@+id/button3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom"
            android:text="Button 3" />
</LinearLayout>

以上按钮效果如图:

image-20210816112249672

设置控件大小比重weight举例:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="horizontal"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
    <EditText
              android:id="@+id/input_message"
              android:layout_width="0dp"
              android:layout_height="wrap_content"
              android:layout_weight="2"
              android:hint="Type something"
              />
    <Button
            android:id="@+id/send"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Send"
            />
</LinearLayout>

效果如图所示:

image-20210816114209446

2.2 RelativeLayout

相对布局,通过相对定位的方式让控件出现在布局的任何位置

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent">
    <Button
            android:id="@+id/button1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentTop="true"
            android:text="Button 1" />
    <Button
            android:id="@+id/button2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_alignParentTop="true"
            android:text="Button 2" />
    <Button
            android:id="@+id/button3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="Button 3" />
    <Button
            android:id="@+id/button4"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_alignParentLeft="true"
            android:text="Button 4" />
    <Button
            android:id="@+id/button5"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_alignParentRight="true"
            android:text="Button 5" />
</RelativeLayout>

android:layout_alignParentLeft 父布局左对齐

android:layout_alignParentTop 父布局上对齐

android:layout_alignParentRight 父布局右对齐

android:layout_alignParentBottom 父布局下对齐

android:layout_centerInParent 父布局中心

以上五个按钮布局效果如下图:

image-20210816114603131

控件可以相对于父布局进行定位,也可以相对于控件进行定位

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent">
    <Button
            android:id="@+id/button3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="Button 3" />
    <Button
            android:id="@+id/button1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_above="@id/button3"
            android:layout_toLeftOf="@id/button3"
            android:text="Button 1" />
    <Button
            android:id="@+id/button2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_above="@id/button3"
            android:layout_toRightOf="@id/button3"
            android:text="Button 2" />
    <Button
            android:id="@+id/button4"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@id/button3"
            android:layout_toLeftOf="@id/button3"
            android:text="Button 4" />
    <Button
            android:id="@+id/button5"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@id/button3"
            android:layout_toRightOf="@id/button3"
            android:text="Button 5" />
</RelativeLayout>

android:layout_above 位于另一个控件的上方

android: layout_below 位于另一个控件的下方

android:layout_toLeftOf 位于另一个控件的左侧

android:layout_toRightOf 位于另一个控件的右侧

android:layout_alignLeft 左边缘和另一个控件的左边缘对齐

android:layout_alignRight 右边缘和另一个控件的右边缘对齐

android:layout_alignTop

android:layout_alignBottom

注意:当一个控件去引用另一个控件的id时,该控件一定要定义在引用控件的后面

image-20210816131447595

2.3 FrameLayout

帧布局,所有的控件都会默认摆放在布局的左上角

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
             android:layout_width="match_parent"
             android:layout_height="match_parent">
    <TextView
              android:id="@+id/textView"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="This is TextView"
              />
    <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Button"
            />
</FrameLayout>

可以看到,文字和按钮都位于布局的左上角,由于Button是在TextView之后添加的,因此按钮压在了文字的上面

image-20210816132053992

android:layout_gravity 对齐方式,和LinearLayout中的用法是相似的

3. 创建自定义组件

3.1 引入自定义布局

为了避免代码的大量重复,不用在每个Activity的布局中都编写一遍同样的代码

以实现一个标题栏自定义组件为例:

在layout目录下新建一个title.xml布局

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <Button
        android:id="@+id/titleBack"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Back" />
    <TextView
        android:id="@+id/titleText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_weight="1"
        android:gravity="center"
        android:text="Title Text"
        android:textSize="24sp" />
    <Button
        android:id="@+id/titleEdit"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Edit"/>
</LinearLayout>

android:background 为布局或控件指定一个背景

android:layout_margin 指定控件在上下左右方向上的间距

android:layout_marginLeft

android:layout_marginTop

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent" >
              <include layout="@layout/title" />
</LinearLayout>

在程序中使用这个标题栏组件,就在activity_main.xml中加入 <include layout="@layout/title" />

最后别忘了在MainActivity中将系统自带的标题栏隐藏掉,调用了 getSupportActionBar() 方法来获得ActionBar的实例,然后再调用它的hide()方法将标题栏隐藏起来

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    ActionBar ab = getSupportActionBar();
    if(ab != null) {
        ab.hide();
    }
}

效果如图:

image-20210816140656616

3.2 创建自定义控件

是如果布局中有一些控件要求能够响应事件,我们还是需要在每个Activity中为这些控件单独编写一次事件注册的代码,这种情况使用自定义控件的方式来解决

新建TitleLayout继承自LinearLayout,成为我们自定义的标题栏控件

在TitleLayout的构造函数中声明了Context、AttributeSet这两个参数,在布局中引入TitleLayout控件时就会调用这个构造函数

通过LayoutInflater的==from()方法可以构建出 一个LayoutInflater对象,然后调用inflate()==方法就可以动态加载一个布局文件(第一个参数是要加载的布局文件的id,第二个参数是给加载好的布局再添加一个父布局)

public class TitleLayout extends LinearLayout {

    public TitleLayout(Context context, AttributeSet attrs) {
        super(context,attrs);
        LayoutInflater.from(context).inflate(R.layout.title,this);
    }
}

接下来我们需要在布局文件 activity_main.xml 中添加这个自定义控件,我们需要指明控件的完整类名,不可以省略

<com.example.chapter2.TitleLayout
     android:layout_width="match_parent"
     android:layout_height="wrap_content" />

下面我们为标题栏中的按钮注册点击事件

public TitleLayout(Context context, AttributeSet attrs) {
    super(context,attrs);
    LayoutInflater.from(context).inflate(R.layout.title,this);
    Button back = (Button) findViewById(R.id.titleBack);
    Button edit = (Button) findViewById(R.id.titleEdit);
    back.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View view) {
            ((Activity)getContext()).finish();
        }
    });
    edit.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View view) {
            Toast.makeText(getContext(),"you clicked edit button",Toast.LENGTH_SHORT).show();
        }
    });
}

这样的话,当我们在每一个布局中引入TitleLayout时,返回按钮和编辑按钮的点击事件就已经自动实现好了

4. listView 最常用的控件

ListView允许用户通过手指上下滑动的方式将屏幕外的数据滚动到屏幕内,同时屏幕上原有的数据会滚动出屏幕,比如查看QQ聊天记录,刷微博

4.1 ListView的简单用法

在 activity_main.xml 中加入

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
    <ListView
         android:id="@+id/listView"
         android:layout_width="match_parent"
         android:layout_height="match_parent" />
</LinearLayout>

这里简单使用一个字符串数组来模拟数据,但是数组中的数据无法直接传递给ListView,还需要借助适配器来完成

ArrayAdapter可以通过泛型来指定要适配的数据类型,然后在ArrayAdapter的构造函数中依次传入参数(Activity 的实例、ListView子项布局的id、数据源)

android.R.layout.simple_list_item_1,是一个 Android内置的布局文件,里面只有一个TextView,作为ListView子项布局的id

最后调用ListView的==setAdapter()==方法,将构建好的适配器对象传递进去

private String[] data = {"Apple", "Banana", "Orange", "Watermelon",
                         "Pear", "Grape", "Pineapple", "Strawberry", "Cherry",
                         "Mango","Apple", "Banana", "Orange", "Watermelon", "Pear", 
                         "Grape","Pineapple", "Strawberry", "Cherry", "Mango"};

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    ArrayAdapter<String> adapter = new ArrayAdapter<String>(MainActivity.this, android.R.layout.simple_list_item_1,data);
    ListView lv = (ListView) findViewById(R.id.list_view);
    lv.setAdapter(adapter);
}

效果如图

image-20210818113107275

4.2 定制ListView页面

因为ListView实用性不强,用RecyclerView都能实现,所以此段先跳过,今后有时间再回来学

5. RecyclerView强大的滚动控件

RecyclerView,能够灵活的实现大数据集的展示,一个增强版的 ListView,能够显示列表、网格、瀑布流等形式,且不同的ViewHolder能够实现item多元化的功能

但是使用起来会稍微麻烦一点

5.1 基本用法

RecyclerView属于新增控件,我们需要在项目的build.gradle中添加RecyclerView库的依赖

打开app/build.gradle文件,在dependencies闭包中添加如下内容,如果你不能确定最新的版本号是多少,可以填入 1.0.0,当有更新的库版本时Android Studio会主动提醒你,填好之后点 sync now

implementation 'androidx.recyclerview:recyclerview:1.0.0'

修改 activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
    <androidx.recyclerview.widget.RecyclerView
              android:id="@+id/recyclerView"
              android:layout_width="match_parent"
              android:layout_height="match_parent" />
</LinearLayout>

新建Fruit类,作为适配器的适配类型

public class Fruit {
    private String name;
    private int imageid;
    public Fruit(String name,int imageid) {
        this.name = name;
        this.imageid = imageid;
    }

    public String getName() {
        return name;
    }

    public int getImageid() {
        return imageid;
    }
}

新建fruit_item.xml,指定自定义布局,注意布局的长和宽都要填 wrap_content

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">
    
    <ImageView
        android:id="@+id/fruit_img"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView
        android:id="@+id/fruit_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:layout_marginLeft="10dp"/>

</LinearLayout>

新建FruitAdapter类,为RecyclerView准备一个适配器,继承自 RecyclerView.Adapter,并将泛型指定为FruitAdapter.ViewHolder(ViewHolder是我们在FruitAdapter中定义的一个内部类)

  1. 首先定义了一个内部类ViewHolder,主构造函数中传入RecyclerView子项的最外层布局,就可以通过findViewById()方法来获取布局中ImageView和TextView的实例

  2. 接着,FruitAdapter中也有一个主构造函数,它用于把要展示的数据源传进来

  3. 继续,由于FruitAdapter是继承自RecyclerView.Adapter的,那么就必须重写 onCreateViewHolder()、onBindViewHolder()、getItemCount() 这3个方法

    • onCreateViewHolder()方法是用于创建ViewHolder实例

    • onBindViewHolder()方法用于对 RecyclerView 子项的数据进行赋值

    • getItemCount()方法用于告诉RecyclerView一共有多少子项

public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {
    private List<Fruit> mFruitList;
    
    static class ViewHolder extends RecyclerView.ViewHolder {
        ImageView fruitimg;
        TextView fruitname;
        public ViewHolder(View v) {
            super(v);
            fruitimg = (ImageView) v.findViewById(R.id.fruit_img);
            fruitname = (TextView) v.findViewById(R.id.fruit_name);
        }
    }
    
    public FruitAdapter(List<Fruit> fruitList) {
        mFruitList = fruitList;
    }
    
    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent,int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item,parent,false);
        ViewHolder holder = new ViewHolder(view);
        return holder;
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        Fruit fruit = mFruitList.get(position);
        holder.fruitimg.setImageResource(fruit.getImageid());
        holder.fruitname.setText(fruit.getName());
    }

    @Override
    public int getItemCount() {
        return mFruitList.size();
    }

}

修改MainActivity中的代码,开始使用RecyclerView

  1. 使用了initFruits()方法,初始化所有的水果数据
  2. 创建一个LinearLayoutManager对象,并将它设置到 RecyclerView当中,用于指定RecyclerView的布局方式,这里是线性布局的意思
  3. 创建了FruitAdapter的实例,并将水果数据传入FruitAdapter的构造函数中
  4. 调用 RecyclerView的setAdapter()方法来完成适配器设置
private List<Fruit> fruitList = new ArrayList<>();

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    initFruits();
    RecyclerView rv = (RecyclerView) findViewById(R.id.recycler_view);
    LinearLayoutManager llm = new LinearLayoutManager(this);
    rv.setLayoutManager(llm);
    FruitAdapter adp = new FruitAdapter(fruitList);
    rv.setAdapter(adp);

}

private void initFruits(){
    Fruit apple = new Fruit("Apple",R.drawable.apple_pic);
    fruitList.add(apple);
    Fruit banana = new Fruit("Banana",R.drawable.banana_pic);
    fruitList.add(banana);
    Fruit orange = new Fruit("Orange",R.drawable.orange_pic);
    fruitList.add(orange);
    Fruit cherry = new Fruit("Cherry",R.drawable.cherry_pic);
    fruitList.add(cherry);
    Fruit grape = new Fruit("Grape",R.drawable.grape_pic);
    fruitList.add(grape);
}

效果如图所示:

image-20210818143742533

5.2 执行过程中遇到的问题

This project uses AndroidX dependencies, but the ‘android.useAndroidX’ property is not enabled.

解决方案:在gradle.properties里面加上一下代码

android.useAndroidX=true
android.enableJetifier=true

把每个activity前面的import android.support.v7.app.AppCompatActivity;改为

import androidx.appcompat.app.AppCompatActivity;
5.3 横向滚动

首先要对fruit_item布局进行修改,如果我们要实现横向滚动的话,应该把fruit_item里的元素改成垂直排列

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="90dp"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/fruit_img"
        android:src="@drawable/apple_pic"
        android:layout_gravity="center_horizontal"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"/>
    <TextView
        android:id="@+id/fruit_name"
        android:textSize="30dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_margin="10dp"/>

</LinearLayout>

接下来修改MainActivity中的代码:

只加入了一行代码,调用LinearLayoutManager的setOrientation()方法 设置布局的排列方向。默认是纵向排列的,我们传入LinearLayoutManager.HORIZONTAL 表示让布局横行排列

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    initFruits();
    RecyclerView rv = (RecyclerView) findViewById(R.id.recycler_view);
    LinearLayoutManager llm = new LinearLayoutManager(this);
    /* insert begin */
    llm.setOrientation(LinearLayoutManager.HORIZONTAL);
    /* insert end */
    rv.setLayoutManager(llm);
    FruitAdapter adp = new FruitAdapter(fruitList);
    rv.setAdapter(adp);
}

效果如图:

image-20210818152255980

5.4 瀑布流布局

除了LinearLayoutManager之外,RecyclerView还给我们提供了另外两种内置的布局排列方式

  • GridLayoutManager:用于实现网格布局

  • StaggeredGridLayoutManager:用于实现瀑布流布局

首先修改一下fruit_item.xml中的代码,宽度改成match_parent

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="5dp">>

    <ImageView
        android:id="@+id/fruit_img"
        android:layout_gravity="center_horizontal"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"/>
    <TextView
        android:id="@+id/fruit_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="10dp"/>

</LinearLayout>

接着修改MainActivity中的代码

  1. 创建了一个StaggeredGridLayoutManager的实例,构造函数第一个参数用于指定布局的列数,传入3表示会把布局分为3列,第二个参数用于指定布局的排列方向,传入 StaggeredGridLayoutManager.VERTICAL表示纵向排列

  2. 把创建好的实例设置到RecyclerView当中

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    initFruits();
    RecyclerView rv = (RecyclerView) findViewById(R.id.recycler_view);
    StaggeredGridLayoutManager sglm = new StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL);

    rv.setLayoutManager(sglm);
    FruitAdapter adp = new FruitAdapter(fruitList);
    rv.setAdapter(adp);

}

可以把fruitname随机加长一些,以更好的看出瀑布流效果

private void initFruits(){
        for(int i=0;i<2;i++){
            Fruit apple = new Fruit(getRandomName("Apple"),R.drawable.apple_pic);
            fruitList.add(apple);
            Fruit banana = new Fruit(getRandomName("Banana"),R.drawable.banana_pic);
            fruitList.add(banana);
            Fruit orange = new Fruit(getRandomName("Orange"),R.drawable.orange_pic);
            fruitList.add(orange);
            Fruit cherry = new Fruit(getRandomName("Cherry"),R.drawable.cherry_pic);
            fruitList.add(cherry);
            Fruit grape = new Fruit(getRandomName("Grape"),R.drawable.grape_pic);
            fruitList.add(grape);
        }
    }

    private String getRandomName(String name) {
        Random rd = new Random();
        int length = rd.nextInt(20)+1;
        StringBuffer builder = new StringBuffer();
        for(int i = 0;i < length;i++{
            builder.append(name);
        }
    }

效果如图:

image-20210818154212999

5.5 RecyclerView的点击事件

RecyclerView并没有提供类似于 setOnItemClickListener()这样的注册监听器方法,需要我们自己给子项具体的View 去注册点击事件

修改FruitAdapter中的代码

  1. 在 ViewHolder 中添加fruitview来保存子项最外层布局的实例
static class ViewHolder extends RecyclerView.ViewHolder {
    View fruitview;
    ImageView fruitimg;
    TextView fruitname;
    public ViewHolder(View v) {
        super(v);
        fruitview = v;
        fruitimg = (ImageView) v.findViewById(R.id.fruit_img);
        fruitname = (TextView) v.findViewById(R.id.fruit_name);
    }
}
  1. 在onCreateViewHolder()方法中注册点击事件,点击事件中先获取了用户点击的position,然后通过position拿到相应的Fruit实例
@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 view) {
            int position = holder.getAdapterPosition();
            Fruit fruit = mFruitList.get(position);
            Toast.makeText(view.getContext(),"you clicked"+fruit.getName(),Toast.LENGTH_SHORT).show();
        }
    });
    return holder;
}

尝试点击recyclerview弹出对话框,并删除该元素

@Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.log_item,parent,false);
        ViewHolder holder = new ViewHolder(view);
        holder.logview.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                int position = holder.getAdapterPosition();
                logData ld = mlogData.get(position);
                AlertDialog.Builder dialog = new AlertDialog.Builder(parent.getContext());
                dialog.setTitle("case intro");
                dialog.setMessage("you clicked"+ld.getTime()+ld.getTitle()+ld.getNeirong());
                dialog.setCancelable(false);

                dialog.setPositiveButton("delete", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        mlogData.remove(ld);
                        notifyItemRemoved(mlogData.size());
                        //notifyItemRangeChanged(mlogData.size());
                    }
                });
                dialog.setNegativeButton("back", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        //设置取消按钮点击事件
                    }
                });
                dialog.show();
            }
        });
        return holder;
    }

三、探究Fragment

为了兼顾手机和平板的开发

Fragment是一种可以嵌入在Activity当中的UI片段,它能让程序更加合理和充分地利用大屏幕的空间

可以理解成一个迷你型的Activity

image-20210818161402633

image-20210818161428167

1. Fragment的使用方式

创建一个平板模拟器pixel C

image-20210818162231355

下面我们尝试在一个Activity当中添加两个 Fragment,并让这两个Fragment平分Activity的空间

新建一个左侧Fragment的布局left_fragment.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
    <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="Button"
            />
</LinearLayout>

新建右侧Fragment的布局right_fragment.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:background="#00ff00"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
    <TextView
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:layout_gravity="center_horizontal"
              android:textSize="24sp"
              android:text="This is right fragment"
              />
</LinearLayout>

新建一个LeftFragment类,并让它继承自Fragment,请一定要使用AndroidX库中的Fragment,使用AndroidX库中的Fragment并不需要在build.gradle文件中添加额外的依赖

是重写了Fragment的onCreateView()方法,然后在这个方法中通过 LayoutInflater的inflate()方法将刚才定义的left_fragment布局动态加载进来

public class LeftFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
        View v = inflater.inflate(R.layout.left_frag,container,false);
        return v;
    }
}

用同样的方法再新建一个RightFragment

public class RightFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
        View v = inflater.inflate(R.layout.right_frag,container,false);
        return v;
    }
}

接下来修改activity_main.xml中的代码,我们使用了标签在布局中添加Fragment,过这里还需要通过android:name属性来显式声明要添加的Fragment类名,注意一定要将类的包名也加上

<fragment
          android:id="@+id/leftFrag"
          android:name="com.example.fragmenttest.LeftFragment"
          android:layout_width="0dp"
          android:layout_height="match_parent"
          android:layout_weight="1" />
<fragment
          android:id="@+id/rightFrag"
          android:name="com.example.fragmenttest.RightFragment"
          android:layout_width="0dp"
          android:layout_height="match_parent"
          android:layout_weight="1" />

结果如图:

image-20210819100927738

2. 动态添加Fragment

可以在程序运行时动态地添加到Activity当中

新建another_right_fragment.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:background="#ffff00"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
    <TextView
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:layout_gravity="center_horizontal"
              android:textSize="24sp"
              android:text="This is another right fragment"
              />
</LinearLayout>

然后新建AnotherRightFragment作为另一个右侧Fragment

public class AnotherRightFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
        View v = inflater.inflate(R.layout.another_right_frag,container,false);
        return v;
    }
}

接下来看一下如何将它动态地添加到Activity当中,修改activity_main.xml,在将右侧Fragment替换成了一个FrameLayout

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="horizontal"
              android:layout_width="match_parent"
              android:layout_height="match_parent" >
    <fragment
              android:id="@+id/leftFrag"
              android:name="com.example.fragmenttest.LeftFragment"
              android:layout_width="0dp"
              android:layout_height="match_parent"
              android:layout_weight="1" />
    <FrameLayout
                 android:id="@+id/rightLayout"
                 android:layout_width="0dp"
                 android:layout_height="match_parent"
                 android:layout_weight="1" >
    </FrameLayout>
</LinearLayout>

修改 MainActivity中的代码,在代码中向FrameLayout里添加内容,从而实现动态添加Fragment的功能

  1. 创建待添加Fragment的实例
  2. 获取FragmentManager,在Activity中可以直接调用getSupportFragmentManager() 方法获取
  3. 开启一个事务,通过调用==beginTransaction()==方法开启
  4. 向容器内添加或替换Fragment,一般使用==replace()==方法实现,需要传入容器的id和待添 加的Fragment实例
  5. 提交事务,调用commit()方法来完成
public class FragmentActivity extends AppCompatActivity implements View.OnClickListener{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_fragment);

        Button left_btn = (Button) findViewById(R.id.left_button);
        left_btn.setOnClickListener(this);
        replaceFragment(new RightFragment());

    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.left_button:
                replaceFragment(new AnotherRightFragment());
                break;
            default:
                break;
        }
    }

    private void replaceFragment(Fragment frag) {
        FragmentManager fm = getSupportFragmentManager();
        FragmentTransaction ft = fm.beginTransaction();
        ft.replace(R.id.rightLayout,frag);
        ft.commit();
    }
}

效果如下:我们点击一下左侧的按钮,右边的板块就会从绿色变成黄色

image-20210819103647370

3. 在Fragment中实现返回栈

按下Back键可以回到上一个Fragment,而不是直接退出程序

修改MainActivity中的代码,在事务提交之前调用了FragmentTransaction的==addToBackStack()==方法

private void replaceFragment(Fragment frag) {
    FragmentManager fm = getSupportFragmentManager();
    FragmentTransaction ft = fm.beginTransaction();
    ft.replace(R.id.rightLayout,frag);
    ft.addToBackStack(null);
    ft.commit();
}

Back,程序回到了RightFragment界面

继续 Back,RightFragment界面也会消失

再次 Back,程序才会退出

4. Fragment和Activity之间的交互

Fragment和Activity是各自存在于一个独立的类当中的,它们之间并没有那么明显的方式来直接进行交互

  • 活动调用碎片:

    FragmentManager提供了一个类似于 findViewById()的方法,专门用于从布局文件中获取Fragment的实例

    RightFragment rf = (RightFragment) getFragmentManager().findFragmentById(R.id.rightLayout);
    
  • 碎片调用活动

    通过调用==getActivity()==方法来得到 和当前Fragment相关联的Activity实例,当Fragment 中需要使用Context对象时,也可以使用getActivity()方法

    MainActivity act = (MainActivity) getActivity();
    

5. Fragment的生命周期

5.1 Fragment的状态

运行状态:所关联的Activity正处于运行状态时

暂停状态:当一个Activity进入暂停状态时

停止状态:当一个Activity进入停止状态时,是完全不可见的

销毁状态:Fragment总是依附于Activity而存在,因此当Activity被销毁时,与它相关联的 Fragment 就会进入销毁状态

5.2 Fragment的回调方法

Fragment提供了一些附加的回调方法:

  • onAttach():当Fragment和Activity建立关联时调用

  • onCreateView():为Fragment创建视图(加载布局)时调用

  • onActivityCreated():确保与Fragment相关联的Activity已经创建完毕时调用

  • onDestroyView():当与Fragment关联的视图被移除时调用

  • onDetach():当Fragment和Activity解除关联时调用

image-20210819110432667

五、详解广播机制

Android中的每个应用程序都可以对自己感兴趣的广播进行注册,这样该程序就只会收到自己所关心的广播内容,这些广播可能是来自于系统的,也可能是来自于其他应用程序的

Android提供了一套完整的API,允许应用程序自由地发送和接收广播

Android中的广播主要可以分为两种类型:标准广播和有序广播

  • 标准广播

    完全异步执行,所有的 BroadcastReceive r几乎会在同一时刻收到这条广播消息,因此它们之间没有任何先后顺序可言

    image-20210819111353893

  • 有序广播

    同步执行,同一时刻只会有一个BroadcastReceiver能够收到这条广播消息,当这个BroadcastReceiver中的逻辑执行完毕后,广播才会继续传递,有先后顺序的

    image-20210819111447735

1. 接受系统广播

我们可以在应用程序中通过监听这些广播来得到各种系统的状态信息,比如手机开机完成、亮屏熄屏、网络变化、电池电量变化、系统时间改变都会发出一条广播

1.1 动态注册监听时间变化

注册 BroadcastReceiver 的方式一般有两种:在代码中注册和在AndroidManifest.xml中注册。其中前者也被称为动态注册,后者也被称为静态注册

下面尝试实现监听系统网络变化的广播:

修改BroadcastActivity中的代码

  1. 定义了一个内部类 NetworkChangeReceiver,这个类是继承 自BroadcastReceiver的,并重写了父类的onReceive()方法
  2. 创建了一个IntentFilter的实例,并给它添加了一个值为android.net.cnn.CONNECTIVITY_CHANGE 的action,因为当系统网络发生变化时,系统发出的正是一条值为android.net.cnn.CONNECTIVITY_CHANGE的广播
  3. 接下来创建一个NetworkChangeReceiver的实例,然后调用==registerReceiver()==方法进行注册,这样 TimeChangeReceiver 就会收到所有值为android.net.cnn.CONNECTIVITY_CHANGE 的广播
  4. 最后,动态注册的BroadcastReceiver一定要取消注册才行,通过调用==unregisterReceiver()==方法
public class BroadcastActivity extends AppCompatActivity {

    private IntentFilter inf;
    private NetworkChangeReceiver ncr;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_broadcast);
        
        inf = new IntentFilter();
        inf.addAction("android.net.cnn.CONNECTIVITY_CHANGE");
        ncr = new NetworkChangeReceiver();
        registerReceiver(ncr,inf);
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        unregisterReceiver(ncr);
    }

    class NetworkChangeReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            Toast.makeText(context,"network changes",Toast.LENGTH_SHORT).show();
        }
    }
}

对上面代码进一步优化,能够准确的告诉用户当前是什么网络状态

class NetworkChangeReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo ni = cm.getActiveNetworkInfo();
        if(ni!=null && ni.isAvailable()) {
            Toast.makeText(context,"network is available",Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(context,"network is unavailable",Toast.LENGTH_SHORT).show();
        }
    }
}

另外,安卓为了保护用户设备的安全隐私,如果程序进行一些对用户来说比较敏感的操作,就必须在配置文件中声明权限,比如这里的访问网络

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
1.2 静态注册实现开机启动

让程序在未启动的情况下也能接收广播

这里我们准备实现一个开机启动的功能,在开机的时候我们的应用程序肯定是没有启动, 因此显然不能使用动态注册的方式来实现,而应该使用静态注册的方式来接收开机广播

右击 →New→Other→Broadcast Receiver,创建的类命名为BootCompleteReceiver,Exported属性允许这个BroadcastReceiver接收本程序以外的广播,Enabled属性表示启用这个 BroadcastReceiver,勾选这两个属性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UBGXXq9s-1629976477557)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20210819145835685.png)]

public class BootCompleteReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        // TODO: This method is called when the BroadcastReceiver is receiving
        // an Intent broadcast.
        Toast.makeText(context,"boot complete",Toast.LENGTH_SHORT).show();
    }
}

另外,静态的BroadcastReceiver一定要在AndroidManifest.xml文件中注册

<application>
<receiver
          android:name=".BootCompleteReceiver"
          android:enabled="true"
          android:exported="true"></receiver>
</application>

我们还需要对 AndroidManifest.xml文件进行修改

由于Android系统启动完成后会发出一条值为android.intent.action.BOOT_COMPLETED 的广播,因此我们在标签中又添加了一个标签,并在里面声明了相应的action

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<application>
    <receiver
              android:name=".BootCompleteReceiver"
              android:enabled="true"
              android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.BOOT_COMPLETED"/>
        </intent-filter>
    </receiver>
</application>

2. 发送自定义广播

2.1 发送标准广播

要先定义一个静态BroadcastReceiver来准备接收此广播,以上方法自动生成一个MyBroadcastReceiver,并重写onReceive()方法

public class MyBroadcastReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        // TODO: This method is called when the BroadcastReceiver is receiving
        // an Intent broadcast.
        Toast.makeText(context,"received in MyBroadcastReceiver",Toast.LENGTH_LONG).show();
    }
}

然后在AndroidManifest.xml中对这个BroadcastReceiver进行修改,指定action,这里让MyBroadcastReceiver接收一条值为 com.example.chapter3.MY_BROADCAST的广播

<receiver
          android:name=".MyBroadcastReceiver"
          android:enabled="true"
          android:exported="true">
    <intent-filter>
        <action android:name="com.example.chapter3.MY_BROADCAST"/>
    </intent-filter>
</receiver>

接下来修改activity_main.xml中的代码,定义了一个按钮,用于作为发送广播的触发点

<Button
        android:id="@+id/btn_send"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Send Broadcast"
        />

然后修改MainActivity中的代码,在按钮的点击事件里面加入了发送自定义广播的逻辑

  1. 构建了一个Intent对象,并把要发送的广播的值传入
  2. 调用==sendBroadcast()==方法将广播发送出去,这样所有监听com.example.broadcasttest.MY_BROADCAST这条广播的BroadcastReceiver就会收到消息了
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_send_broadcast);

    Button send = (Button) findViewById(R.id.btn_send);
    send.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Intent intent = new Intent("com.example.chapter3.MY_BROADCAST");
            sendBroadcast(intent);
        }
    });
}

注意,因为安卓高版本对隐式广播进行了限制,因而用setComponent()才能接收到广播,参数一是你的包名,参数二是你的接收器

Intent intent = new Intent(); 
intent.setComponent(new ComponentName("com.example.myapplication","com.example.myapplication.MyBroadcastReceiver" ));
sendBroadcast(intent); //发送广播
2.2 发送有序广播

新建项目BroadcastTest2,新建AnotherBroadcastReceiver,依然用来接收广播

public class AnotherBroadcastReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        // TODO: This method is called when the BroadcastReceiver is receiving
        // an Intent broadcast.
        Toast.makeText(context,"received in AnotherBroadcastReceiver",Toast.LENGTH_LONG).show();
    }
}

在BroadcastTest2的AndroidManifest.xml中对这个BroadcastReceiver的配置进行修改

AnotherBroadcastReceiver同样接收的是 com.example.broadcasttest.MY_BROADCAST这条广播,重新运行程序,并点击“Send Broadcast”,就会分别弹出两次提示信息,说明应用程序发出的广播是可以被其他应用程序接受到的

<receiver
          android:name=".AnotherBroadcastReceiver"
          android:enabled="true"
          android:exported="true">
    <intent-filter>
        <action android:name="com.example.chapter3.MY_BROADCAST" />
    </intent-filter>
</receiver>

image-20210819161117777

不过到目前为止,程序发出的都是标准广播,现在来尝试一下发送有序广播

修改MainActivity中的代码,将sendBroadcast()方法改成 sendOrderedBroadcast()方法

sendOrderedBroadcast(intent,null);

重新运行程序并点击“Send Broadcast”,两个BroadcastReceiver仍然都可以收到这条广播

下面尝试在注册的时候设定BroadcastReceiver的先后顺序呢,修改 AndroidManifest.xml,通过android:priority属性设置了优先级,优先级高的先收到广播

<receiver
          android:name=".MyBroadcastReceiver"
          android:enabled="true"
          android:exported="true">
    <intent-filter android:priority="100">
        <action android:name="com.example.chapter3.MY_BROADCAST" />
    </intent-filter>
</receiver>

如果在onReceive()方法中调用了==abortBroadcast()==方法,就表示将这条广播截断,后面的将无法再接收到这条广播

public class MyBroadcastReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        // TODO: This method is called when the BroadcastReceiver is receiving
        // an Intent broadcast.
        Toast.makeText(context,"received in MyBroadcastReceiver",Toast.LENGTH_LONG).show();
        abortBroadcast();
    }
}

3. 实践:实现强制下线功能

六、数据存储

保存在内存中的数据是处于瞬时状态的,而保存在存储设备中的数据是处于持久状态的。

持久化技术提供了一种机制,可以让数据在瞬时状态和持久状态之间进行转换

1. 文件存储

不对存储的内容进行任何格式化处理,所有数据都是原封不动地保存到文件当中

1.1 将数据存储到文件中

Context类中提供了一个==openFileOutput()==方法,可以用于将数据存储到指定的文件中,第一个参数是文件名,在文件创建的时候使用,第二个参数是文件的操作模式,主要有MODE_PRIVATE和MODE_APPEND两种模式

在布局中加入了一个EditText,用于输入文本内容

<EditText
          android:id="@+id/edit"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"/>

修改MainActivity中的代码,在数据被回收之前,将它存储到文件当中

  1. 构造save()函数,通过openFileOutput()方法能够得到一个FileOutputStream对象,然后借助它构建出一个OutputStreamWriter对 象,接着再使用OutputStreamWriter构建出一个BufferedWriter对象,这样你就可以通过BufferedWriter将文本内容写入文件中
  2. 重写了onDestroy()方法,获取了EditText中输入的内容,并调用save()
public class FileStorageActivity extends AppCompatActivity {

    private EditText edit;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_file_storage);
        edit = (EditText) findViewById(R.id.edit);
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        String input = edit.getText().toString();
        save();
    }
    
    public void save(String input) {
        FileOutputStream out = null;
        BufferedWriter writer = null;
        try {
            out = openFileOutput("data", Context.MODE_PRIVATE);
            writer = new BufferedWriter(new OutputStreamWriter(out));
            writer.write(input);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if(writer != null) {
                    writer.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

证明数据确实已经保存成功:在输入框输入数据之后返回前一个活动 —> Device File Explorer —> /data/data/com.example.chapter6/files/ 目录 —> 已经生成了一 个data文件

image-20210823135949529

image-20210823140449484

1.2 从文件中读取数据

Context类中还提供了一个==openFileInput()==方法,用于从文件中读取数据,系统会自动到/data/data//files/目录下加载这个文件,并返回一个FileInputStream对象

在main_activity里加入load()函数:

  1. 通过openFileInput()方法获取了一个FileInputStream对象
  2. 借助它构建出一个InputStreamReader对象
  3. 再使用InputStreamReader构建出 一个BufferedReader对象,这样我们就可以通过BufferedReader将文件中的数据一行行读取出来,并拼接到StringBuilder对象
public String load() {
    FileInputStream in = null;
    BufferedReader reader = null;
    StringBuffer content = new StringBuffer();
    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();
}

修改onCreate()代码:

  1. 调用load()方法读取文件
  2. 如果读到内容非空,就调用EditText的setText()方法将内容填充,并调用setSelection()方法将输入光标移动到文本的末尾位置
public class FileStorageActivity extends AppCompatActivity {

    private EditText edit;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_file_storage);
        edit = (EditText) findViewById(R.id.edit);

        /***insert begin***/
        String input = load();
        if(!TextUtils.isEmpty(input)){
            edit.setText(input);
            edit.setSelection(input.length());
            Toast.makeText(this,"restoring succeeded",Toast.LENGTH_LONG).show();
        }
        /***insert end***/
    }

    @Override
    protected void onDestroy() {
        ......
    }

    public void save(String input) {
        ......
    }

    public String load() {
        ......
    }
}

现在重新启动程序时EditText中能够保留我们上次输入的内容

image-20210823143058030

2. SharedPreferences存储

使用键值对的方式来存储数据

支持多种不同的数据类型存储

2.1 将数据存储到SharedPreferences中

Android中主要提供了以下两种方法用于得到SharedPreferences对象

  • Context类中的getSharedPreferences()方法,第一个参数用于指定SharedPreferences文件的名称,第二个参数用于指定操作模式

  • Activity类中的getPreferences()方法,只接收一个操作模式参数

得到了SharedPreferences对象之后,就可以开始向SharedPreferences文件中存储数据了,主要:

  • 调用SharedPreferences对象的edit()方法获取一个 SharedPreferences.Editor对象
  • 向SharedPreferences.Editor对象中添加数据,比如添加一个字符串则使用putString()方法,以此类推
  • 调用apply()方法将添加的数据提交

现在开始:

先在avtivity_main.xml中添加一个存储数据按钮

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical" >
    <Button
            android:id="@+id/saveButton"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Save Data"
            />
</LinearLayout>

给按钮注册点击事件,通过 ==getSharedPreferences()==方法指定文件名为data,并得到了 SharedPreferences.Editor 对象

Button savaData = (Button) findViewById(R.id.saveButton);
savaData.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        SharedPreferences.Editor editor = getSharedPreferences("data",MODE_PRIVATE).edit();
        editor.putString("name","Tom");
        editor.putInt("age",18);
        editor.putBoolean("married",false);
        editor.apply();
    }
});

运行一下程序了,点击一下“Save Data”按钮。这时的数据应该已经保存成功了

2.2 从SharedPreferences中读取数据

SharedPreferences对象中提供了一系列的get方法,用于读取存储的数据

先在avtivity_main.xml中添加一个读取数据按钮

<Button
        android:id="@+id/restoreButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Restore Data"
        />

给按钮注册点击事件,来从SharedPreferences文件中读取数据

首先通过==getSharedPreferences()==方法得到 了SharedPreferences对象,然后分别调用它的getString()、getInt()和 getBoolean()方法

Button restoreData = (Button) findViewById(R.id.restoreButton);
restoreData.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        SharedPreferences pref = getSharedPreferences("data",MODE_PRIVATE);
        String name = pref.getString("name","");
        int age = pref.getInt("age",0);
        boolean married = pref.getBoolean("married",false);
        Log.d("MainActivity",name);
        Log.d("MainActivity",name);
        Log.d("MainActivity",name);
    }
});

3. SQLite数据库存储

文件存储和SharedPreferences存储只适用于保存一些简单的数据和键值对,当需要存储大量复杂的关系型数据的时候,Android系统竟然是内置了数据库的

3.1 创建数据库

借助SQLiteOpenHelper帮助类

SQLiteOpenHelper是一个抽象类,我们想要使用它就需要创建一个自己的帮助类去继承它,必须在自己的帮助类里重写onCreate()和 onUpgrade() 这两个方法,分别在这两个方法中实现创建和升级数据库的逻辑

数据库文件会存放在/data/data//databases/目录下

内置方法:

  • getReadableDatabase()

  • getWritableDatabase()

下面开始具体的例子:

新建MyDatabaseHelper类继 承自SQLiteOpenHelper

  1. 把SQL语句放在CREATE_BOOK字符串里面,integer表示整型,real表示浮点型,text表示文本类型,blob表示二进制类型
  2. 在onCreate()方法中又调用了 SQLiteDatabase的==execSQL()==方法去执行这条建表语句
public class MyDataBaseHelper extends SQLiteOpenHelper {

    public static final String CREATE_BOOK = "create table Book("
            +"id integer primary key autoincrement,"
            +"anthor text,"
            +"price real)";
    private Context mContext;
    public MyDataBaseHelper(Context context, String name, SQLiteDatabase.CursorFactory factory,int version) {
        super(context, name, factory, version);
        mContext = context;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_BOOK);
        Toast.makeText(mContext,"create suceceed",Toast.LENGTH_LONG).show();
    }

    @Override
    public  void onUpgrade(SQLiteDatabase db,int oldVersion,int newVersion) {
    }
}

修改activity_main.xml中的代码,加入一个按钮用于创建数据库

<Button
        android:id="@+id/createDatabase"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Create Database"
        />

最后修改MainActivity中的代码,在onCreate()方法中构建了一个MyDatabaseHelper对象,在按钮的点击事件里调用了==getWritableDatabase()==方法

再次点击按钮时,会发现此时已经存在 BookStore.db数据库了,因此不会再创建一次

public class SQLiteActivity extends AppCompatActivity {
    
    private MyDataBaseHelper dbHelper;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sqlite);
        
        dbHelper = new MyDataBaseHelper(this,"BookStore.db",null,1);
        Button createdb = (Button) findViewById(R.id.createDatabase);
        createdb.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                dbHelper.getWritableDatabase();
            }
        });
    }
}

运行代码还需要借助一个叫作 Database Navigator的插件工具,File →Settings→Plugins→插件管理界面

image-20210826192410607

然后进入/data/data/com.example.chapter6/databases/目录下,可以看到已经存在了一个 BookStore.db文件,BookStore.db文件右击→Save As,将它导出到你的计算机

image-20210826193317401

点开Android Studio的左侧边栏的DB Browser工具,点击这个工具左上角的加号按钮,并选择SQLite
image-20210826193006319

后在弹出窗口的Database配置中选择我们刚才导出的BookStore.db文件

image-20210826193926072

image-20210826194342266

3.2 升级数据库

MyDatabaseHelper中的onUpgrade()方法是用于对数据库进行升级的

比如我们想在项目里再添加一张Category表用于记录分类,先在onCreate()方法里多加一条execSQL语句,然后在onUpgrade()方法中执行两条DROP语句,如果发现数据库中已经存在Book表或Category表,就将这两张表删除,然后调用onCreate()方法重新创建

修改自定义类MyDataBaseHelper,加入创建表的SQL语句,并且修改onCreate() 和 onUpgrade()

public static final String CREATE_CATEGORY = "create table Category ("
    +"id integer primary key autoincrement,"
    +"category_name text,"
    +"category_code integer)";
@Override
public void onCreate(SQLiteDatabase db) {
    db.execSQL(CREATE_BOOK);
    db.execSQL(CREATE_CATEGORY);
    Toast.makeText(mContext,"create suceceed",Toast.LENGTH_LONG).show();
}

@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);
}

修改MainActivity中的代码

SQLiteOpenHelper的构造方法里接收的第四个参数表示当前数据库的版本号,之前我们传入的是1,现在只要传入 一个比1大的数,就可以让onUpgrade()方法得到执行了

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

现在重新运行程序,并点击“Create Database”按钮,这时就会再次弹出创建成功的提示

image-20210826201104399

我们还可以使用同样的方式将 BookStore.db 文件导出到计算机,并覆盖之前的BookStore.db文件,然后在DB Browser中重新导入

image-20210826201400994

3.3 添加数据 insert()

Android提供了一系列的辅助性方法,让你在Android中即使不用编写SQL语句,也能轻松完成所有的操作

调用SQLiteOpenHelper的getReadableDatabase()或 getWritableDatabase()方法是可以用于创建和升级数据库的,不仅如此,这两个方法还都会返回一个SQLiteDatabase对象,借助这个对象就可以对数据进行CRUD操作

修改 activity_main.xml中的代码

<Button
        android:id="@+id/addData"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Add Data"
        />

修改MainActivity中的代码

先获取了SQLiteDatabase对象,然后使用 ContentValues对要添加的数据进行组装,这里只对Book表里其中2列的数据进行了组装,id那一列并没给它赋值

private MyDataBaseHelper dbHelper;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_sqlite);

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

    Button addData = (Button) findViewById(R.id.addData);
    addData.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            SQLiteDatabase db = dbHelper.getWritableDatabase();
            ContentValues values = new ContentValues();
            // 开始组装第一条数据
            values.put("anthor","Dan Brown");
            values.put("price",16.96);
            db.insert("Book",null,values);
            //组装第二条数据
            values.put("anthor","Caiyi");
            values.put("price",2000);
            db.insert("Book",null,values);
        }
    });
}

重新下载db文件,重新在DB broser里面配置db文件

双击Book表格,可以看到数据(这里是因为我点了三下addData按钮)

image-20210827111045098

3.4 更新数据 update()

我们现在尝试更新数据库,降低一本书的价格

修改activity_main.xml中的代码

<Button
        android:id="@+id/updateData"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Update Data"
        />

修改 MainActivity中的代码

insert() 第三、第四个参数来指定具体更新哪几行。第三个参数对应的是SQL语句的where部分,表示更新所有name等于?的行,?是一 个占位符,可以通过第四个参数提供的一个字符串数组为第三个参数中的每个占位符指定相应的内容

private MyDataBaseHelper dbHelper;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_sqlite);

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

    Button upDateData = (Button) findViewById(R.id.updateData);
    upDateData.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            SQLiteDatabase db = dbHelper.getWritableDatabase();
            ContentValues values = new ContentValues();
            values.put("price",530);
            db.update("Book",values,"anthor=?",new String[] {"Caiyi"})
        }
    });
}

image-20210827114422048

3.5 删除数据 delete

修改activity_main.xml中的代码,添加一个按钮用于删除数据

<Button
        android:id="@+id/deleteData"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Delete Data"
        />

修改MainActivity中的代码

private MyDataBaseHelper dbHelper;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_sqlite);

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

    Button deleteData = (Button) findViewById(R.id.deleteData);
    deleteData.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            SQLiteDatabase db = dbHelper.getWritableDatabase();
            db.delete("Book","price<?",new String[] {"100"});
        }
    });
}

image-20210827115034719

3.6 查询数据 query()

查询数据是CRUD中最复杂的一种操作,最短的一个方法重载也需要传入7个参数

image-20210827115225173

调用query()方法后会返回一个Cursor对象,查询到的所有数据都将从这个对象中取出

修改activity_main.xml中的代码,添加查询按钮

<Button
        android:id="@+id/queryData"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Query Data"
        />

修改MainActivity中的代码

  1. 调用了SQLiteDatabase的query()方法,查询完之后就得到了一个Cursor对象
  2. 接着我们调用它的moveToFirst()方法,将数据的指针移动到第一行的位置,然后进入一个循环当中,去遍历查询到的每一行数据,在这个循环中可以通过==cursor.getColumnIndex()方法获取某一列在表中对应的位置索引,传入cursor.getString()==中,就可以得到从数据库中读取到的数据了
private MyDataBaseHelper dbHelper;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_sqlite);

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

    Button queryData = (Button) findViewById(R.id.queryData);
    queryData.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            SQLiteDatabase db = dbHelper.getWritableDatabase();
            Cursor cursor = db.query("Book",null,null,null,null,null,null);
            if(cursor.moveToFirst()) {
                do {
                    int a = cursor.getColumnIndex("anthor");
                    int b = cursor.getColumnIndex("price");
                    String anthor = cursor.getString(a);
                    double price = cursor.getDouble(b);
                    Log.d("SQLiteActivity","author"+anthor);
                    Log.d("SQLiteActivity","price"+price);
                } while (cursor.moveToNext());
            }
            cursor.close();
        }
    });
}

点击“Query Data”按钮,查看Logcat的打印内容

image-20210827125348314

3.7 使用SQL操作数据库

直接使用SQL来完成前面几个小节中学过的CRUD操作

添加数据:

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

更新数据:

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

删除数据:

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

查询数据:

val cursor = db.rawQuery("select * from Book", null)

七、跨程序共享数据ContentProvider

ContentProvider主要用于在不同的应用程序之间实现数据共享

1. 运行时权限

Android 6.0系统中加入了运行时权限功能,用户不需要在安装软件的时候一次性授权所有申请的权限,而是可以在软件的使用过程中再对某一项权限申请进行授权

权限大致归成了两类,一类是普通权限,一类是危险权限

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L4vqSf2r-1630165349239)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20210827153529812.png)]

如何在程序运行时申请权限:

使用CALL_PHONE(危险权限)这个权限来作为示例

CALL_PHONE这个权限是编写拨打电话功能的时候需要声明的

修改activity_main.xml布局文件,增加拨打电话按钮

<Button
        android:id="@+id/makeCall"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Make Call" />

修改 MainActivity中的代码,构建一个隐式Intent,Intent的action指定为 Intent.ACTION_CALL

public class PermissionActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_permission);

        Button makeCall = (Button) findViewById(R.id.makeCall);
        makeCall.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                try {
                    Intent intent = new Intent(Intent.ACTION_CALL);
                    intent.setData(Uri.parse("tel:13896796126"));
                    startActivity(intent);
                } catch (SecurityException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

修改AndroidManifest.xml文件,在其中声明如下权限

<uses-permission android:name="android.permission.CALL_PHONE"/>

在Android 6.0或者更高版本系统的手机上运行不起来,在使用危险权限时必须进行运行时权限处理

修改MainActivity中的代码,覆盖了运行时权限的完整流程

  1. 先判断用户是不是已经给过我们授权了,借助的是 ==ContextCompat.checkSelfPermission()==方法,返回值和 PackageManager.PERMISSION_GRANTED做比较,相等就说明用户已经授权
  2. 如果已经授权,直接执行拨打电话call()方法
  3. 如果没有授权,需要调用 ==ActivityCompat.requestPermissions()==方法向用户申请授权(第二个参数把申请的权限名放在数组中,第三个参数是请求码,只要是唯一值就可以了)
  4. 调用完requestPermissions()之后,系统会弹出一个权限申请的对话框,用户可以选择同意或拒绝我们的权限申请。不论是哪种结果,最终都会回调到 ==onRequestPermissionsResult()==方法中
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_permission);

    Button makeCall = (Button) findViewById(R.id.makeCall);
    makeCall.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            if(ContextCompat.checkSelfPermission(PermissionActivity.this, Manifest.permission.CALL_PHONE)!= PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(PermissionActivity.this,new String[]{Manifest.permission.CALL_PHONE},1);
            } else {
                    call();
            }
        }
    });
}

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

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

image-20210827161330871

image-20210827161420582

2. 访问其他程序中的数据

  • 使用现有的ContentProvider读取和操作相应程序中的数据
  • 创建自己的ContentProvider给程序的数据提供外部访问接口
2.1 ContentResolver的基本用法

如果想要访问ContentProvider中共享的数据,就一定要借助 ContentResolver类,ContentResolver中提供了一系列的方法用于对数据进行增删改查操作

内容URI给ContentProvider中的数据建立 了唯一标识符,它主要由三部分组成:协议声明+authority(包名)+path

content://com.example.app.provider/table1
content://com.example.app.provider/table2

还需要将它解析成Uri对象才可以作为参数传入

Uri uri = Uri.parse("content://com.example.app.provider/table1")

现在就可以使用这个Uri对象查询table1表中的数据了

Cursor cursor = getContentResolver.query(
    uri,
    projection,
    selection,
    selectionArgs,
    sortOrder)

image-20210827162553904

返回的仍然是一个Cursor对象,这时我们就可以将数据从Cursor对象中逐个读取出来

if(cursor!=null){
    while (cursor.moveToNext()) {
        String column1 = cursor.getString(cursor.getColumnIndex("column1"));
        int column2 = cursor.getInt(cursor.getColumnIndex("column2"));
    }
    cursor.close();
}

剩下的增加、修改、删除操作更简单,仍然是将待添加的数据组装到ContentValues中,然后调用ContentResolver的insert()方法

ContentValues values = new ContentValues();
values.put("column","text");
values.put("colume",1);
getContentResolver().insert(Uri,values);
2.2 读取系统联系人

先在通讯录创建两个联系人

修改activity_main.xml中的代码,放置了一个ListView

<ListView
          android:id="@+id/contactsView"
          android:layout_width="match_parent"
          android:layout_height="match_parent" >
</ListView>

修改MainActivity中的代码

  1. 按照ListView的标准用法对其初始化
  2. 关于运行时权限的处理流程
  3. 写一个readContacts()方法读取系统联系人信息,ContactsContract.CommonDataKinds.Phone类已经帮我们做好了封装,提供了一个 CONTENT_URI常量,而这个常量就是使用Uri.parse()方法解析出来的结果
public class ContentResolverActivity extends AppCompatActivity {

    ArrayAdapter<String> adapter;
    List<String> contactList = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_content_resolver);

        //配置配适器
        ListView cv = (ListView) findViewById(R.id.contactsView);
        adapter = new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,contactList);
        cv.setAdapter(adapter);

        //运行时权限申请
        if(ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)!= PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.READ_CONTACTS},1);
        } else {
            readContacts();
        }
    }

    private void readContacts() {
        Cursor cursor = null;
        try {
            //查询联系人数据
            cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null,null,null,null);
            if(cursor != null) {
                while(cursor.moveToNext()) {
                    
                    //获取联系人姓名
                    int a = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME);
                    String name = cursor.getString(a);
                    
                    //获取联系人手机号
                    int b = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
                    String num = cursor.getString(b);
                    //把两个信息加入到listview的数组中
                    contactList.add(name+"\n"+num);
                }
                adapter.notifyDataSetChanged();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if(cursor!=null) {
                cursor.close();
            }
        }
    }

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

修改 AndroidManifest.xml中的代码,读取系统联系人的权限不能忘记声明

<uses-permission android:name="android.permission.READ_CONTACTS" />

image-20210827174841109

image-20210827174904738

3. 创建自己的ContentProvider

等要用到了再来学

八、手机多媒体

1. 使用通知

当某个应用程序想向用户发送提示消息,而该应用又不在前台运行时

1.1 通知的基本用法

在activity_main.xml中添加按钮

<Button
        android:id="@+id/sendNotice"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Send Notice" />

修改 MainActivity中的代码:

  1. 首先获取了NotificationManager的实例,并创建了一个name为normal通知渠道
  2. 使用NotificationChannel类构建一个通知渠道
  3. 使用一个Builder构造器来创建Notification对象
  4. 调用NotificationManager的==notify()==方法就可以让通知显示出来了
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_notification_main);

    Button sendNotice = (Button) findViewById(R.id.sendNotice);
    sendNotice.setOnClickListener(new View.OnClickListener() {
        @RequiresApi(api = Build.VERSION_CODES.O)
        @Override
        public void onClick(View view) {

            // 1. 先需要一个NotificationManager对通知进行管理,通过getSystemService()获取
            NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

            // 2. 使用NotificationChannel类构建一个通知渠道
            int importance = NotificationManager.IMPORTANCE_LOW;//重要性 HIGH LOW MIN
            NotificationChannel channel = null;//生成channel
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
                channel = new NotificationChannel("channel_1", "normal", importance);
                manager.createNotificationChannel(channel);//添加channel
            }

            // 3. 使用一个Builder构造器来创建Notification对象
            Notification notification = new Notification.Builder(NotificationMainActivity.this,"channel_1")
                .setCategory(Notification.CATEGORY_MESSAGE)
                .setSmallIcon(R.mipmap.ic_launcher)
                .setContentTitle("This is a content title")
                .setContentText("This is a content text")
                .setAutoCancel(true)
                .build();

            // 4. 调用NotificationManager的notify()方法就可以让通知显示出来了
            manager.notify(1,notification);
        }
    });
}

查看设置→应用和通知→NotificationTest→通知,这里已经出现了一个Normal通知渠道

image-20210827222632268

点击“Send Notice”按钮,下拉系统状态栏可以看到该通知

image-20210827222550572

1.2 添加点击事件

如果还想要实现通知的点击效果,这就涉及了一个新的概念—— PendingIntent(Intent倾向于立即执行某个动作,PendingIntent倾向于在某个合适的时机执行某个动作,可以把PendingIntent简单地理解为延迟执行的Intent)

新建NotificationActivity2,在activity_notification2.xml中加入textview

<TextView
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:layout_centerInParent="true"
          android:textSize="24sp"
          android:text="This is notification layout"
          />

下面我们修改MainActivity中的代码,让用户点击它的时候可以启动另一个Activity

将构建好的Intent对象传入==PendingIntent的getActivity()方法,得到PendingIntent的实例,接着在NotificationCompat.Builder中调用setContentIntent()==方法,把它作为参数传入

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_notification_main);

    Button sendNotice = (Button) findViewById(R.id.sendNotice);
    sendNotice.setOnClickListener(new View.OnClickListener() {
        @RequiresApi(api = Build.VERSION_CODES.O)
        @Override
        public void onClick(View view) {

            // 将构建好的Intent对象传入PendingIntent的getActivity()方法里
            Intent intent = new Intent(NotificationMainActivity.this,NotificationActivity2.class);
            PendingIntent pi = PendingIntent.getActivity(NotificationMainActivity.this,0,intent,0);

            .........

            // 3. 使用一个Builder构造器来创建Notification对象
            Notification notification = new Notification.Builder(NotificationMainActivity.this,"channel_1")
                .setCategory(Notification.CATEGORY_MESSAGE)
                .setSmallIcon(R.mipmap.ic_launcher)
                .setContentTitle("This is a content title")
                .setContentText("This is a content text")
                .setSmallIcon(R.mipmap.ic_launcher)
                /***insert***/
                .setContentIntent(pi)  
                /***insert***/
                .build();

            // 4. 调用NotificationManager的notify()方法就可以让通知显示出来了
            manager.notify(1,notification);
        }
    });
}

点击一下该通知,就会打开NotificationActivity的界面了

image-20210827231730163

这时,系统状态上的通知图标还没有消失

解决的方法有两种:

  1. 在 NotificationCompat.Builder中再连缀一个setAutoCancel()方法

    Notification notification = new Notification.Builder(NotificationMainActivity.this,"channel_1")
        .setCategory(Notification.CATEGORY_MESSAGE)
        .setSmallIcon(R.mipmap.ic_launcher)
        .setContentTitle("This is a content title")
        .setContentText("This is a content text")
        .setContentIntent(pi)
        .setAutoCancel(true)
        .build();
    
  2. 显式地调用NotificationManager的cancel()方法将它取消

    manager.cancel(1)
    
1.3 通知的进阶用法

NotificationCompat.Builder中提供了非常丰富的API,以便我们创建出更加多样的通知效果

  • .setPriority(Notification.PRIORITY_MAX) 还有MIN LOW HIGH MAX DEFAULT

  • setStyle(),替代setContentText(),允许我们构建出富文本的通知内容(长文本信息,显示一张大图片等)

    notification = NotificationCompat.Builder(this, "normal")
        ...
        .setStyle(new Notification.BigTextStyle().bigText("Learn how to build notifications, send and sync data, and use voice actions. Get the official Android IDE and developer tools to build apps for Android."))
        .build();
    

    image-20210827232956137

2. 调用摄像头

修改activity_main.xml中的代码,Button是用于打开摄像头进行拍照的,而ImageView则是用于将拍到的图片显示出来

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent" >
    <Button
            android:id="@+id/takePhotoBtn"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Take Photo" />
    <ImageView
               android:id="@+id/imageView"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:layout_gravity="center_horizontal" />
</LinearLayout>

修改MainActivity中的代码

(用到了再回来学)

3. 播放多媒体文件

(用到了再回来学)

九、探究service

实现程序后台运行的解决方案,它非常适合执行那些不需要和用户交互而且还要求长期运行的任务

Service不是运行在一个独立的进程当中的,而是依赖于创建Service时所在的应用程序进程。当某个应用程序进程被杀掉时,所有依赖于该进程的Service也会停止运行

1. Android多线程编程

定义一个线程只需要新建一个类继承自Thread,然后重写父类的run()方法,使用继承的方式耦合性有点高,我们会更多地选择使用实现Runnable接口的方式来定义一个线程

public class MyThread implements Runnable{
    @Override
    public void run(){
        //处理的具体逻辑
    }
}

启动线程的方法

MyThread myThread = new MyThread();
new Thread(myThread).start();

如果你不想专门再定义一个类去实现Runnable接口,也可以使用Lambda(匿名类)的方式

new Thread(new Runnable() {
    @Override
    public void run() {
        //具体处理逻辑
    }
}).start();

2. 在子线程中更新UI

如果想要更新应用程序里的UI元素,必须在主线程中进行,否则就会出现异常

新建AndroidThreadTest项目,修改activity_main.xml中的代码,我们希望在点击“Button”后可以把TextView中显示的字符串改成"Nice to meet you"

<Button
        android:id="@+id/change_Text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Change Text" />
<TextView
          android:id="@+id/textView"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:layout_centerInParent="true"
          android:text="Hello world"
          android:textSize="20sp" />

在子线程中更新UI程序崩溃,Android提供了一套异步消息处理机制,完美地解决了这个问题

修改MainActivity中的代码

  1. 定义了一个整型变量updateText,用于表示更新TextView这个动作
  2. 新增Handler对象,并重写父类的handleMessage()方法,如果Message的what字段的值等于updateText,就将TextView替换
  3. 在按钮点击事件中,创建了一个Message对象,并将它的what字段的指定为updateText,调用Handler的sendMessage()方法将这条 Message发送出去。很快Handler就会收到这条Message,并在handleMessage()方法中对它进行处理。此时handleMessage()方法中的代码就是在主线程当中运行的了
public class ThreadActivity extends AppCompatActivity {

    public static final int UPDATE_TEXT = 1;

    private TextView text;

    private Handler handler = new Handler() {

        public void handleMessage(Message msg) {
            switch (msg.what) {
                case UPDATE_TEXT:
                    // 在这里可以进行UI操作
                    text.setText("Nice to meet you");
                    break;
                default:
                    break;
            }
        }

    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        text = (TextView) findViewById(R.id.textView);
        Button changeText = (Button) findViewById(R.id.change_Text);
        changeText.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
//                        text.setText("Nice to meet you");
                        Message message = new Message();
                        message.what = UPDATE_TEXT;
                        handler.sendMessage(message); // 将Message对象发送出去
                    }
                }).start();
            }
        });
    }
}

image-20210828192759661

3. 解析异步消息处理机制

异步消息处理主要由4个部分组成:Message、Handler、MessageQueue、Looper

image-20210828193908280

4. 服务的基本用法

4.1 定义一个Service

新建一个ServiceTest项目,然后右击com.example.servicetest→New→Service→Service

Exported:表示是否将这个Service暴露给外部其他程序访问

Enabled:表示是否启用这个Service

onBind()方法是Service中唯一的抽象方法,所以必须在子类里实现

public class MyService extends Service {
    public MyService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        throw new UnsupportedOperationException("Not yet implemented");
    }
}

重写Service中的另外一些方法:

  • onCreate():在Service创建的 时候调用
  • onStartCommand():在每次Service启动的时候调用
  • onDestroy():在Service销毁的时候调用
@Override
public void onCreate() {
    super.onCreate();
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    return super.onStartCommand(intent, flags, startId);
}

@Override
public void onDestroy() {
    super.onDestroy();
}

每一个Service都需要在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/Theme.Chapter7">
    <service
            android:name=".MyService"
            android:enabled="true"
            android:exported="true"></service>
    ......
</application>

这样的话,就已经将一个Service完全定义好了

4.2 启动和停止Service

主要是借助Intent来实现的

先修改activity_main.xml中的代码,加入启动和停止按钮

<Button
        android:id="@+id/start_service"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Start Service" />

<Button
        android:id="@+id/stop_service"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Stop Service" />

修改MainActivity中的代码

  1. 在“Start Service”按钮里,构建了一个Intent对象,并调用 startService()方法来启动MyService
  2. 在“Stop Service”按钮里,调用stopService()方法来停止MyService
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_service);
    Button startService = (Button) findViewById(R.id.start_service);
    Button stopService = (Button) findViewById(R.id.stop_service);

    startService.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Intent startIntent = new Intent(ServiceActivity.this, MyService.class);
            startService(startIntent); // 启动服务
        }
    });

    stopService.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Intent stopIntent = new Intent(ServiceActivity.this, MyService.class);
            stopService(stopIntent); // 停止服务
        }
    });
}

在MyService的几个方法中加入打印日志,证实Service已经成功启动或者停止

@Override
public void onCreate() {
    super.onCreate();
    Log.d("MyService", "onCreate executed");
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    Log.d("MyService", "onStartCommand executed");
    return super.onStartCommand(intent, flags, startId);
}

@Override
public void onDestroy() {
    Log.d("MyService", "onDestroy executed");
    super.onDestroy();
}

运行测试

image-20210828231656092

还可以在Settings→System→Advanced→Developer options→Running services中找到它

4.3 Activity和Service进行通信

在启动了Service之后,Activity与Service基本就没有什么关系了

如果想在Activity中指挥Service去干什么,Service就去干什么,就需要借助onBind()

目前我们希望在MyService里提供一个下载功能,然后在Activity中可以决定何时开始下载,以及随时查看下载进度:创建一个专门的Binder对象来对下载功能进行管理

修改Myservice:

  1. 新建了一个DownloadBinder类,并让它继承自Binder,在它的内 部提供了开始下载以及查看下载进度的方法
  2. 在MyService中创建了DownloadBinder的实例
public class MyService extends Service {

    public MyService() {
    }

    private DownloadBinder mBinder = new DownloadBinder();
    class DownloadBinder extends Binder {
        public void startDownload() {
            Log.d("MyService", "startDownload executed");
        }
        public int getProgress() {
            Log.d("MyService", "getProgress executed");
            return 0;
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }

   ...........

}

修改activity_main.xml,新增activity用来绑定和取消绑定Service的两个按钮

<Button
        android:id="@+id/bind_service"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Bind Service" />

<Button
        android:id="@+id/unbind_service"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Unbind Service" />

修改MainActivity中的代码,当一个Activity和Service绑定了之后,就可以调用该Service里的Binder提供的方法了

  1. 先创建了一个ServiceConnection的匿名类实现,并在里面重写了 onServiceConnected()、onServiceDisconnected()
private MyService.DownloadBinder downloadBinder;
private ServiceConnection connection = new ServiceConnection() {
    @Override
    public void onServiceDisconnected(ComponentName name) {
    }
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        downloadBinder = (MyService.DownloadBinder) service;
        downloadBinder.startDownload();
        downloadBinder.getProgress();
    }
};

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_service);
    Button startService = (Button) findViewById(R.id.start_service);
    Button stopService = (Button) findViewById(R.id.stop_service);
    Button bindService = (Button) findViewById(R.id.bind_service);
    Button unbindService = (Button) findViewById(R.id.unbind_service);

    ......

    bindService.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Intent bindIntent = new Intent(ServiceActivity.this, MyService.class);
            bindService(bindIntent, connection, BIND_AUTO_CREATE); // 绑定服务
        }
    });

    unbindService.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            unbindService(connection); // 解绑服务
        }
    });
}

十、网络技术

在手机端使用HTTP和服务器进行网络交互,并对服务器返回的数据进行解析

1. WebView

借助它我们就可以在自己的应用程序里嵌入一个浏览器

修改activity_main.xml中的代码,这个控件就是用来显示网页

<WebView
         android:id="@+id/webView"
         android:layout_width="match_parent"
         android:layout_height="match_parent" />

修改MainActivity中的代码:

  • setJavaScriptEnabled()让 WebView支持JavaScript脚本
  • setWebViewClient()目标网页仍然在当前WebView中显示,而不是打开系统浏览器
  • loadUrl()将网址传入,即可展示相应网页的内容
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_web_view);

    android.webkit.WebView webView = (android.webkit.WebView) findViewById(R.id.webView);
    webView.getSettings().setJavaScriptEnabled(true);
    webView.setWebViewClient(new WebViewClient());
    webView.loadUrl("http://www.ecnu.edu.cn");
}

而访问网络是需要声明权限的

<uses-permission android:name="android.permission.INTERNET" />

err_cleartext_not_permitted的解决方案:

在AndroidManifest.xml里加上android:usesCleartextTraffic=“true”

<application
    android:usesCleartextTraffic="true">
    ......
</application>

image-20210830104608867

2. HTTP访问网络

发送HTTP请求→接收服务器响应→解析返回数据→最终页面展示

2.1 使用HttpURLConnection

获取HttpURLConnection的实例

URL url = new URL("http://www.baidu.com");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();

设置一下HTTP请求所使用的方法

connection.setRequestMethod("GET");

设置连接超时、读取超时的毫秒数

connection.setConnectTimeout(180000);
connection.setReadTimeout(180000);

获取到服务器返回的输入流

InputStream in = connection.getInputStream();

将这个HTTP连接关闭

connection.disconnect();

下面通过一个具体的例子来体验HttpURLConnection

修改activity_main.xml,Button 用于发送HTTP请求,TextView用于将服务器返回的数据显示出来

<Button
        android:id="@+id/send_request"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Send Request" />

<ScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent" >

    <TextView
              android:id="@+id/response_text"
              android:layout_width="match_parent"
              android:layout_height="wrap_content" />
</ScrollView>

修改MainActivity中的代码

public class HTTP extends AppCompatActivity {

    TextView responseText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_http);
        
        Button sendRequest = (Button) findViewById(R.id.send_request);
        responseText = (TextView) findViewById(R.id.response_text);
        sendRequest.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                sendRequestWithHttpURLConnection();
                //sendRequestWithOkHttp();
            }
        });
    }

    private void sendRequestWithHttpURLConnection() {
        // 开启线程来发起网络请求
        new Thread(new Runnable() {
            @Override
            public void run() {
                HttpURLConnection connection = null;
                BufferedReader reader = null;
                try {
                    Log.d("MainActivity", "HttpURLConnection connecting ");
                    URL url = new URL("http://www.sei.ecnu.edu.cn");
                    connection = (HttpURLConnection) url.openConnection();
                    connection.setRequestMethod("GET");
                    connection.setConnectTimeout(180000);
                    connection.setReadTimeout(180000);
                    InputStream in = connection.getInputStream();
                  
                    // 下面对获取到的输入流进行读取
                    // 用BufferedReader对服务器返回的流进行读取
                    reader = new BufferedReader(new InputStreamReader(in));
                    StringBuilder response = new StringBuilder();
                    String line;
                    while ((line = reader.readLine()) != null) {
                        response.append(line);
                    }
                    showResponse(response.toString());
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (reader != null) {
                        try {
                            reader.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                    if (connection != null) {
                        connection.disconnect();
                    }
                }
            }
        }).start();
    }

    private void showResponse(final String response) {
        // 为什么要用runOnUiThread(),因为不允许在子线程中进行UI操作
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                // 在这里进行UI操作,将结果显示到界面上
                responseText.setText(response);
            }
        });
    }
}

要声明一下网络权限,修改 AndroidManifest.xml

<uses-permission android:name="android.permission.INTERNET" />

服务器返回给我们的就是这种HTML代码

image-20210830110940737

2.2 使用 OkHttp

有许多出色的网络通信库都可以替代原生的HttpURLConnection,而其中OkHttp无疑是做得最出色的一个

先在项目中添加OkHttp库的依赖

dependencies {
    ...
    implementation 'com.squareup.okhttp3:okhttp:4.1.0'
}

创建一个OkHttpClient的实例

OkHttpClient client = new OkHttpClient();

发起一条HTTP请求,就需要创建一个Request对象

Request request = new Request.Builder()
    .url("http://115.29.231.93:8080/CkeditorTest/get_data.json")
    .build();

newCall()方法来创建一个Call对象,并调用它的execute()方法发送请求并获取服务器返回的数据

Response response = client.newCall(request).execute();

Response对象就是服务器返回的数据了,可以使用如下写法来得到返回的具体内容

String responseData = response.body().string();

如果是发起一条POST请求稍微复杂一点,先构建一个Request Body 对象来存放待提交的参数,然后在Request.Builder中调用一下post()方法,并将RequestBody对象传入

let us rty:

修改上面的MainActivity中的代码

  1. 添加了一个sendRequestWithOkHttp()方法

  2. 把“Send Request”按钮的点击事件改为这个函数

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_http);
    Button sendRequest = (Button) findViewById(R.id.send_request);
    responseText = (TextView) findViewById(R.id.response_text);
    
    sendRequest.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            sendRequestWithOkHttp();
        }
    });
}

.......
    
private void sendRequestWithOkHttp() {
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                OkHttpClient client = new OkHttpClient();
                Request request = new Request.Builder()
                    .url("http://ecnu.edu.cn")
                    .build();
                Response response = client.newCall(request).execute();
                String responseData = response.body().string();
                showResponse(responseData);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }).start();
}

image-20210830112832371

3. 解析xml格式数据

我们可以向服务器提交数据,也可以从服务器上获取数据

一般我们会在网络上传输一些格式化后的数据,在网络上传输数据时最常用的格式有两种:XML和JSON

3.1 搭建一个apache服务器

下面搭建一个最简单的Web服务器,在这个服务器上提供一段XML文本,然后我们在程序里去访问这个服务器,再对得到的XML文本进行解析

首先下载一个Apache服务器的安装包:http://httpd.apache.org

安装和配置方法:

https://www.cnblogs.com/yerenyuan/p/5460336.html

https://www.cnblogs.com/zhaoqingqing/p/4969675.html

image-20210830122326336

进入D:\Apache\htdocs目录下,新建一个名为get_data.xml的文件

<apps>
    <app>
        <id>1</id>
        <name>Google Maps</name>
        <version>1.0</version>
    </app>
    <app>
        <id>2</id>
        <name>Chrome</name>
        <version>2.1</version>
    </app>
    <app>
        <id>3</id>
        <name>Google Play</name>
        <version>2.3</version>
    </app>
</apps>

在浏览器中访问http://127.0.0.1:8088/get_data.xml这个网址

image-20210830122610278

准备工作到此结束,接下来在Android程序里去获取并解析这段XML

3.2 Pull解析方式

比较常用的两种:Pull解析和SAX 解析

修改MainActivity中的代码

  1. 修改sendRequestWithOkHttp(),将HTTP请求的地址改成了http://10.0.2.2:8088/get_data.xml,10.0.2.2对于模拟器来说就是计算机本机的IP地址。把showResponse(responseData)改为parseXMLWithPull(responseData)
private void sendRequestWithOkHttp() {
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                OkHttpClient client = new OkHttpClient();
                Request request = new Request.Builder()
                    /***edit begin***/
                    .url("http://10.0.2.2:8088/get_data.xml")
                    /***edit end***/
                    .build();
                Response response = client.newCall(request).execute();
                String responseData = response.body().string();
                /***edit begin***/
                parseXMLWithPull(responseData);
                /***edit end***/
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }).start();
}
  1. 添加parseXMLWithPull(String xmlData)
private void parseXMLWithPull(String xmlData) {
    try {
        XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
        XmlPullParser xmlPullParser = factory.newPullParser();
        xmlPullParser.setInput(new StringReader(xmlData));
        int eventType = xmlPullParser.getEventType();
        String id = "";
        String name = "";
        String version = "";
        while (eventType != XmlPullParser.END_DOCUMENT) {
            String nodeName = xmlPullParser.getName();
            switch (eventType) {
                    // 开始解析某个结点
                case XmlPullParser.START_TAG: {
                    if ("id".equals(nodeName)) {
                        id = xmlPullParser.nextText();
                    } else if ("name".equals(nodeName)) {
                        name = xmlPullParser.nextText();
                    } else if ("version".equals(nodeName)) {
                        version = xmlPullParser.nextText();
                    }
                    break;
                }
                    // 完成解析某个结点
                case XmlPullParser.END_TAG: {
                    if ("app".equals(nodeName)) {
                        Log.d("HTTP", "id is " + id);
                        Log.d("HTTP", "name is " + name);
                        Log.d("HTTP", "version is " + version);
                    }
                    break;
                }
                default:
                    break;
            }
            eventType = xmlPullParser.next();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

image-20210830124003605

3.3 SAX解析方式

新建一个 ContentHandler 类继承自DefaultHandler,并重写父类的5个方法

startDocument()在开始XML解析的时候调用

startElement()在开始解析某个节点的时候调用

characters()方法会在获取节点中内容的时候调用

endElement()方法会在完成解析某个节点的时候调用

endDocument()方法会在完成整个XML解析的时候调用

public class ContentHandler extends DefaultHandler {

    private String nodeName;

    private StringBuilder id;

    private StringBuilder name;

    private StringBuilder version;

    @Override
    public void startDocument() throws SAXException {
        id = new StringBuilder();
        name = new StringBuilder();
        version = new StringBuilder();
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        // 记录当前结点名
        nodeName = localName;
    }

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        // 根据当前的结点名判断将内容添加到哪一个StringBuilder对象中
        if ("id".equals(nodeName)) {
            id.append(ch, start, length);
        } else if ("name".equals(nodeName)) {
            name.append(ch, start, length);
        } else if ("version".equals(nodeName)) {
            version.append(ch, start, length);
        }
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        if ("app".equals(localName)) {
            Log.d("ContentHandler", "id is " + id.toString().trim());
            Log.d("ContentHandler", "name is " + name.toString().trim());
            Log.d("ContentHandler", "version is " + version.toString().trim());
            // 最后要将StringBuilder清空掉
            id.setLength(0);
            name.setLength(0);
            version.setLength(0);
        }
    }

    @Override
    public void endDocument() throws SAXException {
        super.endDocument();
    }

}

修改MainActivity中的代码

  1. 添加parseXMLWithSAX()方法
private void parseXMLWithSAX(String xmlData) {
    try {
        SAXParserFactory factory = SAXParserFactory.newInstance();
        XMLReader xmlReader = factory.newSAXParser().getXMLReader();
        ContentHandler handler = new ContentHandler();
        // 将ContentHandler的实例设置到XMLReader中
        xmlReader.setContentHandler(handler);
        // 开始执行解析
        xmlReader.parse(new InputSource(new StringReader(xmlData)));
    } catch (Exception e) {
        e.printStackTrace();
    }
}
  1. 修改 sendRequestWithOkHttp()
private void sendRequestWithOkHttp() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    OkHttpClient client = new OkHttpClient();
                    Request request = new Request.Builder()
                            // 指定访问的服务器地址是电脑本机
                            .url("http://10.0.2.2:8088/get_data.json")
                            .build();
                    Response response = client.newCall(request).execute();
                    String responseData = response.body().string();
                    /***edit begin***/
                    parseXMLWithSAX(responseData);
                    /***edit end***/
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

image-20210830130410878

4. 解析JSON格式数据

JSON的主要优势在于它的体积更小,在网络上传输的时候更省流量

在D:\Apache\Apache\htdocs目录中新建一个get_data.json的文件

[
  {
    "id": "5",
    "version": "5.5",
    "name": "Clash of Clans"
  },
  {
    "id": "6",
    "version": "7.0",
    "name": "Boom Beach"
  },
  {
    "id": "7",
    "version": "3.5",
    "name": "Clash Royale"
  }
]

image-20210830130810204

解析JSON数据也有很多种方法,可以使用官方提供的JSONObject,也可以使用 Google的开源库GSON

4.1 使用JSONObject

修改MainActivity中的代码

  1. 将HTTP请求的地址改成http://10.0.2.2:8088/get_data.json

  2. 调用parseJSONWithJSONObject()方法来解析数据

private void parseJSONWithJSONObject(String jsonData) {
    try {
        JSONArray jsonArray = new JSONArray(jsonData);
        for (int i = 0; i < jsonArray.length(); i++) {
            JSONObject jsonObject = jsonArray.getJSONObject(i);
            String id = jsonObject.getString("id");
            String name = jsonObject.getString("name");
            String version = jsonObject.getString("version");
            Log.d("HTTP", "id is " + id);
            Log.d("HTTP", "name is " + name);
            Log.d("HTTP", "version is " + version);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}
private void sendRequestWithOkHttp() {
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                OkHttpClient client = new OkHttpClient();
                Request request = new Request.Builder()
                    .url("http://10.0.2.2:8088/get_data.json")
                    .build();
                Response response = client.newCall(request).execute();
                String responseData = response.body().string();
                //showResponse(responseData);
                //parseXMLWithPull(responseData);
                //parseXMLWithSAX(responseData);
                parseJSONWithJSONObject(responseData);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }).start();
}

image-20210830154039860

4.2 使用GSON

Google提供的GSON开源库可以让解析JSON数据的工作简单到让你不敢想象的地步,它的强大之处就在于可以将一段JSON格式的字符串自动映射成一个对象

在项目中添加GSON库的依赖

dependencies {
    ...
    implementation 'com.google.code.gson:gson:2.8.5'
}

比如说一段JSON格式的数据

{
    "name":"Tom",
    "age":20
}

那我们就可以定义一个Person类,并加入name和age这两个字段

Gson gson = new Gson();
Person person = gson.fromJson(jsonData,Person.class);

下面真正地尝试一下

首先新增一个App类,加入 id、name、version这3个字段

public class App {
    
    private String id;
    private String name;
    private String version;

    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getVersion() {
        return version;
    }
    public void setVersion(String version) {
        this.version = version;
    }
}

然后修改MainActivity中的代码

private void parseJSONWithGSON(String jsonData) {
    Gson gson = new Gson();
    List<App> appList = gson.fromJson(jsonData, new TypeToken<List<App>>() {}.getType());
    for (App app : appList) {
        Log.d("MainActivity", "id is " + app.getId());
        Log.d("MainActivity", "name is " + app.getName());
        Log.d("MainActivity", "version is " + app.getVersion());
    }
}
private void sendRequestWithOkHttp() {
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                OkHttpClient client = new OkHttpClient();
                Request request = new Request.Builder()
                    .url("http://10.0.2.2:8088/get_data.json")
                    .build();
                Response response = client.newCall(request).execute();
                String responseData = response.body().string();
                //showResponse(responseData);
                //parseXMLWithPull(responseData);
                //parseXMLWithSAX(responseData);
                //parseJSONWithJSONObject(responseData);
                parseJSONWithGSON(responseData);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }).start();
}

image-20210830155100896

十一、Material Design

这个库将Material Design中最具代表性的一些控件和效果进行了封装,使得开发者即使在不了解Material Design的情况下,也能非常轻松地将自己的应用Material化

1. Toolbar

Toolbar的强大之处在于,它不仅继承了ActionBar的所有功能,而且灵活性很高,可以配合其他控件完成一些Material Design的效果

打开res/values/styles.xml文件,把parent主题改为NoActionBar

<style name="Theme.Chapter11" parent="Theme.MaterialComponents.DayNight.NoActionBar">

现在我们已经将ActionBar隐藏起来了,那么接下来看一看如何使用Toolbar,修改activity_main.xml中的代码

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/purple_500"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
</FrameLayout>

修改MainActivity,调用setSupportActionBar()方法并将Toolbar的实例传入

Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);

image-20210902134205461

接着实现一些Toolbar比较常用的功能,比如修改标题栏上显示的文字内容,先在AndroidManifest.xml中指定,给activity增加了一个android:label属性

<application>
    <activity
        android:name=".MainActivity"
        android:label="Rainbow">
        ...
    </activity>
</application>

还可以再添加一些action按钮

  1. 提前准备了几张图片作为按钮的图标,放在drawable

  2. res目录→New→Directory,创建一个menu文件夹

    右击menu→New→Menu resource file,创建一个toolbar.xml文件

    通过标签来定义action按钮,showAsAction来指定按钮的显示位置

    • always:永远显示在Toolbar中,如果屏幕空间不够则不显示

    • ifRoom:屏幕空间足够的情况下显示在 Toolbar中,不够的话就显示在菜单当中

    • never:表示永远显示在菜单当中

    <menu xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">
        <item
            android:id="@+id/backup"
            android:icon="@drawable/pic1"
            android:title="Backup"
            app:showAsAction="always" />
        <item
            android:id="@+id/delete"
            android:icon="@drawable/pic2"
            android:title="Delete"
            app:showAsAction="ifRoom" />
        <item
            android:id="@+id/settings"
            android:icon="@drawable/pic3"
            android:title="Settings"
            app:showAsAction="never" />
    </menu>
    
  3. 修改MainActivity中的代码

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.toolbar,menu);
        return true;
    }
    
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.backup:
                Toast.makeText(this,"backup",Toast.LENGTH_SHORT).show();
                break;
            case R.id.delete:
                Toast.makeText(this,"delete",Toast.LENGTH_SHORT).show();
                break;
            case R.id.settings:
                Toast.makeText(this,"settings",Toast.LENGTH_SHORT).show();
                break;
            default:
        }
        return true;
    }
    

    image-20210902144747108

2. 滑动菜单

2.1 DrawerLayout

它是一个布局,在布局中允许放入两个直接子控件:第一个子控件是主屏幕中显示的内容,第二个子控件是滑动菜单中显示的内容

对activity_main.xml中的代码做如下修改,第一个子控件是FrameLayout,用于作为主屏幕中显示的内容,第二个子控件是一个TextView,用于作为滑动菜单中显示的内容

layout_gravity这个属性是必须指定的,告诉DrawerLayout滑动菜单是在屏幕的左边还是右边,start表示会根据系统语言进行判断

<androidx.drawerlayout.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/drawerLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="@color/purple_500"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
    </FrameLayout>
    
    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:background="#FFF"
        android:text="This is menu"
        android:textSize="30sp" />
</androidx.drawerlayout.widget.DrawerLayout>

image-20210902135338889

在Toolbar的最左边加入一个导航按钮,点击按钮将滑动菜单展示出来

准备了一张导航按钮的图标ic_menu.png,将它放在了 drawable-xxhdpi目录,修改MainActivity中的代码

private DrawerLayout dl;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ......

    dl = (DrawerLayout) findViewById(R.id.drawerLayout);
    ActionBar actionBar = getSupportActionBar();
    if(actionBar != null) {
        actionBar.setDisplayHomeAsUpEnabled(true);
        actionBar.setHomeAsUpIndicator(R.drawable.ic_menu);
    }
}

......

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        ......
        case android.R.id.home:
            dl.openDrawer(GravityCompat.START);
            break;
        default:
    }
    return true;
}

image-20210902145439209

2.2 NavigationView

在滑动菜单页面定制任意的布局

首先要将这个库引入项目中,app/build.gradle,第一行就是Material库,第二行是一个开源项目 CircleImageView,它可以用来轻松实现图片圆形化的功能

dependencies {
	...
	implementation 'com.google.android.material:material:1.1.0'
	implementation 'de.hdodenhof:circleimageview:3.0.1'
}

将res/values/styles.xml文件中 AppTheme的parent主题改成Theme.MaterialComponents.Light.NoActionBar

<style name="Theme.Chapter11" parent="Theme.MaterialComponents.Light.NoActionBar">

在menu文件夹下创建一个nav_menu.xml文件,准备好我们的menu

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <group android:checkableBehavior="single">
        <item
            android:id="@+id/navCall"
            android:icon="@drawable/img"
            android:title="Call" />
        <item
            android:id="@+id/navFriends"
            android:icon="@drawable/img_1"
            android:title="Friends" />
        <item
            android:id="@+id/navLocation"
            android:icon="@drawable/img_2"
            android:title="Location" />
        <item
            android:id="@+id/navMail"
            android:icon="@drawable/ic_menu"
            android:title="Mail" />
    </group>
</menu>

将之前的TextView换成了NavigationView

<androidx.drawerlayout.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/drawerLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="@color/purple_500"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
    </FrameLayout>

    <com.google.android.material.navigation.NavigationView
        android:id="@+id/navView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        app:menu="@menu/nav_menu"/>

</androidx.drawerlayout.widget.DrawerLayout>

修改 MainActivity中的代码,处理菜单项的点击事件

private DrawerLayout dl;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    ......

    NavigationView nav = (NavigationView) findViewById(R.id.navView);
    nav.setCheckedItem(R.id.navCall); //默认选项
    nav.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
        @Override
        public boolean onNavigationItemSelected(@NonNull MenuItem item) {
            switch (item.getItemId()){
                case R.id.navCall:
                    dl.closeDrawers();
                    break;
                case R.id.navFriends:
                    //写执行事件
                    break;
                case R.id.navLocation:
                    //写执行事件
                    break;
                case R.id.navMail:
                    //写执行事件
                    break;
            }
            return true;
        }
    });
}

image-20210902152701808

  • 11
    点赞
  • 97
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值