一、开始启程 1. 认识Android Android 大致可以分为4层架构:Linux内核层、系统运行库层、应用框架层和应用层
Android系统四大组件分别是Activity、Service、BroadcastReceiver和 ContentProvider
Android系统还自带了这种轻量级、运算速度极快的嵌入式关系型数据库, SQLite数据库,它不仅支持标准 的SQL语法,还可以通过Android封装好的API进行操作
2. 创建项目
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模式的项目结构
.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目录下的结构
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目录下的结构
以“drawable”开头的目录:用来放图片的
以“mipmap”开头的目录:用来放应用图标的
以“values”开头的目录:放字符串、样式、颜色等配置的
以“layout”开头的目录:放布局文件
我们应该如何使用这些资源呢,以字符串为例,这里定义了一个应用程序名的字符串,我们有以下两种方式来引用它
在代码中通过R.string.app_name可以获得该字符串的引用。
在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 → 点击右上大象图标重新下载
方法二:
将 gradle-wrapper.properties 文件里面的 distributionUrl=https 中的 https 改成 http,然后重新点击上方的按钮大象重新下载gradle文件
二、探究Activity Activity是一种可以包含用户界面的组件,主要用于和用户进行交互
1. 活动的基本用法 1.1 创建活动
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
在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 ; }
效果图如下
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
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)
3.2 Activity状态
运行:位于返回栈的顶部,系统最不愿意回收
暂停:不在栈顶,但仍可见(例如对话框的背后),暂停状态仍是完全存活的,系统不愿意回收
停止:不在栈顶,完全不可见,系统仍保为这个活动保存状态和成员变量,但在内存紧张会被回收
销毁:从返回栈移除,系统倾向回收,节约内存
3.3 Activity生存期 完整生存期 ,onCreate —— onDestroy,初始化 —— 释放内存
可见生存期,onStart —— onStop,不可见 —— 可见时调用
前台展示生存期,onResume —— onPause
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)
<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 ; } }
效果如图:
2. 详解3种基本布局 常用控件和布局的继承结构:
的所有控件都是直接或间接继承自View的,所用的所有布局都是直接或间接继承自ViewGroup的
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 >
以上按钮效果如图:
设置控件大小比重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 >
效果如图所示:
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 父布局中心
以上五个按钮布局效果如下图:
控件可以相对于父布局进行定位,也可以相对于控件进行定位
<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时,该控件一定要定义在引用控件的后面
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之后添加的,因此按钮压在了文字的上面
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(); } }
效果如图:
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); }
效果如图
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中定义的一个内部类)
首先定义了一个内部类ViewHolder,主构造函数中传入RecyclerView子项的最外层布局,就可以通过findViewById()方法来获取布局中ImageView和TextView的实例
接着,FruitAdapter中也有一个主构造函数,它用于把要展示的数据源传进来
继续,由于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
使用了initFruits()方法,初始化所有的水果数据
创建一个LinearLayoutManager对象,并将它设置到 RecyclerView当中,用于指定RecyclerView的布局方式,这里是线性布局的意思
创建了FruitAdapter的实例,并将水果数据传入FruitAdapter的构造函数中
调用 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); }
效果如图所示:
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 ); llm.setOrientation(LinearLayoutManager.HORIZONTAL); rv.setLayoutManager(llm); FruitAdapter adp = new FruitAdapter (fruitList); rv.setAdapter(adp); }
效果如图:
5.4 瀑布流布局 除了LinearLayoutManager之外,RecyclerView还给我们提供了另外两种内置的布局排列方式
首先修改一下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中的代码
创建了一个==StaggeredGridLayoutManager==的实例,构造函数第一个参数用于指定布局的列数,传入3表示会把布局分为3列,第二个参数用于指定布局的排列方向,传入 StaggeredGridLayoutManager.VERTICAL表示纵向排列
把创建好的实例设置到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); } }
效果如图:
5.5 RecyclerView的点击事件 RecyclerView并没有提供类似于 setOnItemClickListener()这样的注册监听器方法,需要我们自己给子项具体的View 去注册点击事件
修改==FruitAdapter==中的代码
在 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); } }
在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()); } }); dialog.setNegativeButton("back" , new DialogInterface .OnClickListener() { @Override public void onClick (DialogInterface dialogInterface, int i) { } }); dialog.show(); } }); return holder; }
三、探究Fragment 为了兼顾手机和平板的开发
Fragment是一种可以嵌入在Activity当中的UI片段,它能让程序更加合理和充分地利用大屏幕的空间
可以理解成一个迷你型的Activity
1. Fragment的使用方式 创建一个平板模拟器pixel C
下面我们尝试在一个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" />
结果如图:
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的功能
创建待添加Fragment的实例
获取FragmentManager,在Activity中可以直接调用==getSupportFragmentManager()== 方法获取
开启一个事务,通过调用==beginTransaction()==方法开启
向容器内添加或替换Fragment,一般使用==replace()==方法实现,需要传入容器的id和待添 加的Fragment实例
提交事务,调用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(); } }
效果如下:我们点击一下左侧的按钮,右边的板块就会从绿色变成黄色
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是各自存在于一个独立的类当中的,它们之间并没有那么明显的方式来直接进行交互
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解除关联时调用
五、详解广播机制 Android中的每个应用程序都可以对自己感兴趣的广播进行注册,这样该程序就只会收到自己所关心的广播内容,这些广播可能是来自于系统的,也可能是来自于其他应用程序的
Android提供了一套完整的API,允许应用程序自由地发送和接收广播
Android中的广播主要可以分为两种类型:==标准广播和有序广播==
1. 接受系统广播 我们可以在应用程序中通过监听这些广播来得到各种系统的状态信息,比如手机开机完成、亮屏熄屏、网络变化、电池电量变化、系统时间改变都会发出一条广播
1.1 动态注册监听时间变化 注册 BroadcastReceiver 的方式一般有两种:在代码中注册和在AndroidManifest.xml中注册。其中前者也被称为==动态注册==,后者也被称为==静态注册==
下面尝试实现监听系统网络变化的广播:
修改BroadcastActivity中的代码
定义了一个内部类 ==NetworkChangeReceiver==,这个类是继承 自BroadcastReceiver的,并重写了父类的onReceive()方法
创建了一个==IntentFilter==的实例,并给它添加了一个值为android.net.cnn.CONNECTIVITY_CHANGE 的action,因为当系统网络发生变化时,系统发出的正是一条值为android.net.cnn.CONNECTIVITY_CHANGE的广播
接下来创建一个==NetworkChangeReceiver==的实例,然后调用==registerReceiver()==方法进行注册,这样 TimeChangeReceiver 就会收到所有值为android.net.cnn.CONNECTIVITY_CHANGE 的广播
最后,动态注册的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) { 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) { 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中的代码,在按钮的点击事件里面加入了发送自定义广播的逻辑
构建了一个Intent对象,并把要发送的广播的值传入
调用==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) { 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 >
不过到目前为止,程序发出的都是标准广播 ,现在来尝试一下发送有序广播
修改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) { 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中的代码,在数据被回收之前,将它存储到文件当中
构造==save()==函数,通过openFileOutput()方法能够得到一个==FileOutputStream对象==,然后借助它构建出一个==OutputStreamWriter对 象==,接着再使用OutputStreamWriter构建出一个==BufferedWriter对象==,这样你就可以通过BufferedWriter将文本内容写入文件中
重写了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文件
1.2 从文件中读取数据 Context类中还提供了一个==openFileInput()==方法,用于从文件中读取数据,系统会自动到/data/data//files/目录下加载这个文件,并返回一个FileInputStream对象
在main_activity里加入load()函数:
通过openFileInput()方法获取了一个==FileInputStream==对象
借助它构建出一个==InputStreamReader==对象
再使用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()代码:
调用load()方法读取文件
如果读到内容非空,就调用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); String input = load(); if (!TextUtils.isEmpty(input)){ edit.setText(input); edit.setSelection(input.length()); Toast.makeText(this ,"restoring succeeded" ,Toast.LENGTH_LONG).show(); } } @Override protected void onDestroy () { ...... } public void save (String input) { ...... } public String load () { ...... } }
现在重新启动程序时EditText中能够保留我们上次输入的内容
2. SharedPreferences存储 使用键值对的方式来存储数据
支持多种不同的数据类型存储
2.1 将数据存储到SharedPreferences中 Android中主要提供了以下两种方法用于==得到SharedPreferences对象==:
得到了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
把SQL语句放在CREATE_BOOK字符串里面,integer表示整型,real表示浮点型,text表示文本类型,blob表示二进制类型
在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→插件管理界面
然后进入/data/data/com.example.chapter6/databases/目录下,可以看到已经存在了一个 BookStore.db文件,BookStore.db文件右击→Save As,将它导出到你的计算机
点开Android Studio的左侧边栏的DB Browser工具,点击这个工具左上角的加号按钮,并选择SQLite
后在弹出窗口的Database配置中选择我们刚才导出的BookStore.db文件
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”按钮,这时就会再次弹出创建成功的提示
我们还可以使用同样的方式将 BookStore.db 文件导出到计算机,并覆盖之前的BookStore.db文件,然后在DB Browser中重新导入
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按钮)
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" }) } }); }
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" }); } }); }
3.6 查询数据 query() 查询数据是CRUD中最复杂的一种操作,最短的一个方法重载也需要传入7个参数
调用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中的代码
调用了SQLiteDatabase的query()方法,查询完之后就得到了一个Cursor对象
接着我们调用它的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的打印内容
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中的代码,覆盖了运行时权限的完整流程
先判断用户是不是已经给过我们授权了,借助的是 ==ContextCompat.checkSelfPermission()==方法,返回值和 PackageManager.PERMISSION_GRANTED做比较,相等就说明用户已经授权
如果已经授权,直接执行拨打电话call()方法
如果没有授权,需要调用 ==ActivityCompat.requestPermissions()==方法向用户申请授权(第二个参数把申请的权限名放在数组中,第三个参数是请求码,只要是唯一值就可以了)
调用完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 : } }
2. 访问其他程序中的数据
使用现有的ContentProvider读取和操作相应程序中的数据
创建自己的ContentProvider给程序的数据提供外部访问接口
2.1 ContentResolver的基本用法 如果想要访问ContentProvider中共享的数据,就一定要借助 ==ContentResolver类==,ContentResolver中提供了一系列的方法用于对数据进行增删改查操作
内容URI给ContentProvider中的数据建立 了唯一标识符,它主要由三部分组成:协议声明+authority(包名)+path
content: //com .example.app.provider/table1content: //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)
返回的仍然是一个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中的代码
按照ListView的标准用法对其初始化
关于运行时权限的处理流程
写一个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); 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" />
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中的代码:
首先获取了==NotificationManager==的实例,并创建了一个name为normal通知渠道
使用==NotificationChannel==类构建一个通知渠道
使用一个Builder构造器来创建==Notification对象==
调用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) { NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); int importance = NotificationManager.IMPORTANCE_LOW; NotificationChannel channel = null ; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { channel = new NotificationChannel ("channel_1" , "normal" , importance); manager.createNotificationChannel(channel); } 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(); manager.notify(1 ,notification); } }); }
查看设置→应用和通知→NotificationTest→通知,这里已经出现了一个Normal通知渠道
点击“Send Notice”按钮,下拉系统状态栏可以看到该通知
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 intent = new Intent (NotificationMainActivity.this ,NotificationActivity2.class); PendingIntent pi = PendingIntent.getActivity(NotificationMainActivity.this ,0 ,intent,0 ); ......... 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) .setContentIntent(pi) .build(); manager.notify(1 ,notification); } }); }
点击一下该通知,就会打开NotificationActivity的界面了
这时,系统状态上的通知图标还没有消失
解决的方法有两种:
在 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();
显式地调用NotificationManager的cancel()方法将它取消
1.3 通知的进阶用法 NotificationCompat.Builder中提供了非常丰富的API,以便我们创建出更加多样的通知效果
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中的代码
定义了一个整型变量updateText,用于表示更新TextView这个动作
新增Handler对象,并重写父类的handleMessage()方法,如果Message的what字段的值等于updateText,就将TextView替换
在按钮点击事件中,创建了一个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: 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 () { Message message = new Message (); message.what = UPDATE_TEXT; handler.sendMessage(message); } }).start(); } }); } }
3. 解析异步消息处理机制 异步消息处理主要由4个部分组成:Message、Handler、MessageQueue、Looper
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中的代码
在“Start Service”按钮里,构建了一个Intent对象,并调用 startService()方法来启动MyService
在“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(); }
运行测试
还可以在Settings→System→Advanced→Developer options→Running services中找到它
4.3 Activity和Service进行通信 在启动了Service之后,Activity与Service基本就没有什么关系了
如果想在Activity中指挥Service去干什么,Service就去干什么,就需要借助onBind()
目前我们希望在MyService里提供一个下载功能,然后在Activity中可以决定何时开始下载,以及随时查看下载进度 :创建一个专门的Binder对象来对下载功能进行管理
修改Myservice:
新建了一个==DownloadBinder类==,并让它==继承自Binder==,在它的内 部提供了开始下载以及查看下载进度的方法
在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提供的方法了
先创建了一个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 >
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连接关闭
下面通过一个具体的例子来体验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(); } }); } 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(); 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(new Runnable () { @Override public void run () { responseText.setText(response); } }); } }
要声明一下网络权限,修改 AndroidManifest.xml
<uses-permission android:name ="android.permission.INTERNET" />
服务器返回给我们的就是这种HTML代码
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中的代码
添加了一个sendRequestWithOkHttp()方法
把“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(); }
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
进入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这个网址
准备工作到此结束,接下来在Android程序里去获取并解析这段XML
3.2 Pull解析方式 比较常用的两种:Pull解析和SAX 解析
修改MainActivity中的代码
修改==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() .url("http://10.0.2.2:8088/get_data.xml" ) .build(); Response response = client.newCall(request).execute(); String responseData = response.body().string(); parseXMLWithPull(responseData); } catch (Exception e) { e.printStackTrace(); } } }).start(); }
添加==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(); } }
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 { 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()); id.setLength(0 ); name.setLength(0 ); version.setLength(0 ); } } @Override public void endDocument () throws SAXException { super .endDocument(); } }
修改MainActivity中的代码
添加parseXMLWithSAX()方法
private void parseXMLWithSAX (String xmlData) { try { SAXParserFactory factory = SAXParserFactory.newInstance(); XMLReader xmlReader = factory.newSAXParser().getXMLReader(); ContentHandler handler = new ContentHandler (); xmlReader.setContentHandler(handler); xmlReader.parse(new InputSource (new StringReader (xmlData))); } catch (Exception e) { e.printStackTrace(); } }
修改 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(); parseXMLWithSAX(responseData); } catch (Exception e) { e.printStackTrace(); } } }).start(); }
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" } ]
解析JSON数据也有很多种方法,可以使用官方提供的JSONObject,也可以使用 Google的开源库GSON
4.1 使用JSONObject 修改MainActivity中的代码
将HTTP请求的地址改成http://10.0.2.2:8088/get_data.json
调用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(); parseJSONWithJSONObject(responseData); } catch (Exception e) { e.printStackTrace(); } } }).start(); }
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(); parseJSONWithGSON(responseData); } catch (Exception e) { e.printStackTrace(); } } }).start(); }
十一、Material Design 这个库将Material Design中最具代表性的一些控件和效果进行了封装,使得开发者即使在不了解Material Design的情况下,也能非常轻松地将自己的应用Material化
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);
接着实现一些Toolbar比较常用的功能,比如修改标题栏上显示的文字内容,先在AndroidManifest.xml中指定,给activity增加了一个android:label属性
<application > <activity android:name =".MainActivity" android:label ="Rainbow" > ... </activity > </application >
还可以再添加一些action按钮
提前准备了几张图片作为按钮的图标,放在drawable
res目录→New→Directory,创建一个menu文件夹
右击menu→New→Menu resource file,创建一个toolbar.xml文件
通过标签来定义action按钮,showAsAction来指定按钮的显示位置
<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 >
修改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 ; }
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 >
在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 ; }
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 ; } }); }