Android 介绍
Android 系统架构
Linux内核层,为 Android 设备各种硬件停供底层驱动。
系统运行库层,通过一些 C/C++ 库为 Android 系统提供主要的特性支持。
应用框架层,构建程序的各种 API。
应用层,手机上的各种应用程序。
四大组件:活动(Activity)、服务(Service)、广播接收器(Broadcast Reciver)和内容提供器(Content Provider)。
Android 项目目录
构建 Android 项目后,会出现一下结构:
.gradle 和 .idea
:自动生成文件。app
:存放项目中的代码和资源。build
:主要包含了编译时自动生成的文件。libs
:第三方的 jar 包。androidTest
:编写 Android Test 测试用例。java
:放置 java 代码的地方。res
:项目的一些静态资源等等。AndroidManifest.xml
:整个 Android 项目的配置文件。test
:用来编写 Unit Test 测试用例。.gitignore
:将指定目录或文件排除在版本控制之外。app.iml
:IDEA 项目自动生成文件。build.gradle
:app 模块的 gradle 构建脚本,指定一些项目构建的相关配置。proguard-rules.pro
:指定项目代码的混淆规则。
build
:编译时自动生成的文件。gradle
:gradle wrapper 的配置文件。.gitignore
:将指定目录或文件排除在版本控制之外。build.gradle
:项目全局的 gradle 构建脚本,通常文件中的内容不需要修改。gradle.properties
:全局的 gradle 配置文件,这里配置的属性会影响到项目中所有的 gradle 编译脚本。gradlew 和 gradlew.bat
:CLI 执行 gradle 命令。HelloWorld.iml
:用于标识这是一个 IDEA 项目。local.properties
:用于指定 Android SDK 路径。settings.gradle
:指定项目中引用的模块。
<!-- AndroidMainfest.xml -->
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
其中,<intent-filter>
里面的代码意思是当我们手机点击应用图标的时候,首先启动的就是这个活动。
而 MainActivity.java
中则是包含了应用中所有能看见的东西。Android 程序一般将逻辑和视图分开,在布局文件中写界面,然后在活动中引入。布局文件在 res/layout
目录下。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 引入 activity_main 布局
setContentView(R.layout.activity_main);
}
}
归纳一下 res 目录下的内容:
- 所有
drawabic
开头的文件夹是放图片的。 - 所有
mipmap
开头的文件是放应用图标的。 - 所有
values
开头的文件夹是放字符串、样式、颜色等配置的。 layout
文件夹是用来放置布局的。
<resources>
<string name="app_name">Hello, Android!</string>
</resources>
这里定义了一个应用程序名的字符串,可以通过一下方式进行引用:
- 在代码中用
R.string.hello_world
来获取字符串的引用。 - 在 XML 中通过
@string/hello_world
获得字符串的引用。
详解 build.gradle
Gradle 是一个先进的项目构建工具,使用了一种居于 Groovy 的领域特定语言来声明设置,摒弃了传统的 XML 的各种繁琐配置。
接下来一起看看 app 内的 build.gradle
文件
plugins {
/*
com.android.application 应用程序模块:可直接运行
com.android.library 库模块:作为代码依附于别的应用程序模块
*/
id("com.android.application")
}
android {
/*
android 闭包内可以配置构建项目的各种属性
*/
namespace = "com.example.myapplication4"
compileSdk = 34 // 指定编译版本
defaultConfig {
/*
defaultCOnfig 闭包对项目更多细节进行配置
*/
applicationId = "com.example.myapplication4" // 指定项目包名
minSdk = 24 // 最低兼容版本
targetSdk = 34 // 目标版本
versionCode = 1 // 指定项目的版本号
versionName = "1.0" // 指定项目版本名
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
/*
buildTypes 闭包用于指定安装文件的相关配置
debug 指定测试版本安装文件的配置
release 指定生成正式版本安装文件的配置
*/
release {
isMinifyEnabled = false // 是否对项目代码进行混淆
proguardFiles( // 指定混淆使用的规则文件
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" // 当前项目可编写的特别混淆规则
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
dependencies {
/*
dependencies 指定当前项目所有依赖关系
- 本地依赖、库依赖、远程依赖
*/
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.9.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}
Android 日志工具
Android 中的日志工具类是 Log(android.util.Log),这个类提供了几个方法来打印日志:
Log.v()
:打印最为琐碎的、意义最小的日志信息,对应级别是 verbose。Log.d()
:打印一些调试信息,对应级别为 dubug。Log.i()
:打印一些较为重要的数据,可以分析用户行为的数据,对应级别为 info。Log.e()
:打印错误信息,对应级别为 error。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/*
日志 - Log.d()
第一个参数是tag,一般传入类名,用于过滤。
第二个参数是msg,是想要打印的内容。
*/
Log.d("MainActivity", "onCreate execute");
}
Android 活动
活动(Activity)是一种可以包含用户界面的组件,主要用于用户的交互。
我们选择 no activity
选项后构建项目,在 ./app/java/com/example/activitytest
目录下新建一个活动。
public class FirstActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
然后,我们为活动添加布局。在 ./app/src/main/res
目录下创建 layout
目录,然后新建布局资源。写完布局后,直接将布局引入活动中 setContentView(R.layout.first_layout)
。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<!-- 添加一个 Button
如果在 XML 定义 id,使用 @+id/id_name 语法
如果在 XML 引用 id,使用 @id/id_name 语法
match_partent 表示让当前元素和父元素一样的长度
wrap_content 表示当前元素只要能包含里面的内容就行
text 指定元素内显示的内容
-->
<Button
android:id="@+id/button1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button1" />
</LinearLayout>
注意,所以的活动要在 AndroidMainfest.xml
中进行注册才能生效。当然,我们还需要给程序配置主活动,才能启动应用。
<!-- AndroidMainfest.xml -->
<activity
android:name=".FirstActivity"
android:exported="true"
android:label="This is FirstActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
Toast
Toast 可以将一些短小的信息通知个用户,这些信息会在一段时间后自动消失。
// 获得 Button 实例
Button button1 = (Button) findViewById(R.id.button1);
// 注册 Button 监听按钮点击
button1.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
Toast.makeText(FirstActivity.this, "You clicked Button1",
Toast.LENGTH_SHORT).show();
// Activity 类提供了一个 finish() 方法,可以销毁当前活动
finish();
}
});
Menu
在 ./app/src/main/res
目录下创建 menu
文件夹。
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id = "@+id/add_item"
android:title = "Add"/>
<item
android:id = "@+id/remove_item"
android:title = "Remove"/>
</menu>
然后回到 FirstActivity.java
重写方法,让菜单显示出来。
public boolean onCreateOptionsMenu(Menu menu) {
// 创建 MenuInflater 对象,调用 inflate() 方法创建菜单
// 第一个参数,指定资源文件创建菜单;第二个参数,指定菜单添加到menu中
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
此外,出来让菜单显示外,还要让菜单进行响应。
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int id = item.getItemId();
if (id == R.id.add_item){
Toast.makeText(this, "You clicked Add", Toast.LENGTH_SHORT).show();
} else if (id == R.id.remove_item) {
Toast.makeText(this, "You clicked Remove", Toast.LENGTH_SHORT).show();
}
return true;
}
Intent
在 ./app/java/com/example/activitytest
目录下新建第二个活动。
<!-- second_layout.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<Button
android:id="@+id/button2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button2" />
</LinearLayout>
Intent 是 Android 程序中各组件之间进行交互的一种重要方式,不仅可以指明当前组件想要执行的动作,还可以在不同组件之间传递数据。
Intent 一般用于启动活动、启动服务以及发送广播等场景。
Intent 有多个构造函数的重载,其中一个是 Intent(Context packageContex, Class<?>cls)
,Context 要求提供一个启动活动的上下文,Class 指定想要启动的目标活动。
Activity 类中提供了 startActivity() 方法专门用于启动活动,它接收一个 Intent 参数。
Button button1 = (Button) findViewById(R.id.button1);
button1.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
startActivity(intent);
}
});
上述方法为显示 Intent方法,隐式 Intent 则是通过标签指定。
<action>
标签中指明当前活动可以响应。<category>
标签包含附加信息,指明当前活动能够响应的 Intent 中还可能带有的 category。
<!-- AndroidManifest.xml -->
<activity
android:name=".SecondActivity"
android:exported="true">
<intent-filter>
<action android:name="com.example.activitytest.ACTION_START"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
再对事件进行修改,当然每个 Intent 只能指定一个 action ,却能指定多个 category。
Button button1 = (Button) findViewById(R.id.button1);
button1.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
Intent intent = new Intent("com.example.activitytest.ACTION_START");
startActivity(intent);
}
});
使用隐式 Intent,不仅可以启动自己程序内的活动方法,还可以启动其他程序的活动。
// 通过浏览器开启网页
Button button1 = (Button) findViewById(R.id.button1);
button1.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("http://www.baidu.com"));
startActivity(intent);
}
});
于此,我们还可以在 <intent-filter>
标签中配置 <data>
标签,用于更精确地指定当前活动能够响应什么类型的数据。
<!-- AndroidManifest.xml -->
<activity
android:name=".ThirdActivity"
android:exported="true">
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="http"/>
</intent-filter>
</activity>
我们指定了 Intent.ACTION_VIEW
用于展示 ,现在点击按钮后除了可以选择 Browser
外,还可以选择 ActivityTest
来启动活动。
除此之外,还可以指定其他协议:geo
表示地理位置、tel
表示拨打电话。
// 可以改为打开拨号
Button button1 = (Button) findViewById(R.id.button1);
button1.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_DIAL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
}
});
除了进行活动的创建外,我们还要在活动启动的时候传递数据。
我们在 FirstActivity
中传递数据给 SecondActivity
,然后再在 SecondaActivity
中接收。
// FistActivity
Button button1 = (Button) finViewByID(R.id.button1);
button1.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
String data = "Hello SecondActivity";
intent.putExtra("extar_data", data);
startActivity(intent);
}
})
// SecondActivity
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.second_layout);
Intent intent = getIntent();
String data = intent.getStringExtra("extra_data");
// int data = intent.getIntExtra("extra_data");
assert data != null;
Log.d("SecondActivity", data);
}
当然,我们也可以接收下一个活动销毁时传递的数据。
// FirstActivity
Button button1 = (Button) finViewByID(R.id.button1);
button1.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
intentActivityResultLauncher.launch(intent);
}
});
// 处理回调数据
ActivityResultLauncher<Intent> intentActivityResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback<ActivityResult>() {
@Override
public void onActivityResult(ActivityResult result) {
if (result.getData() != null && result.getResultCode() == RESULT_OK){
String returnedData = result.getData().getStringExtra("data_return");
Log.d("FirstActivity", returnedData);
}
}
});
// SecondActivty
Button button2 = (Button) findViewById(R.id.button2);
button2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent();
intent.putExtra("data_return", "Hello FirstActivity");
// RESULT_OK 或 RESULT_CANCELED
setResult(RESULT_OK, intent);
finish();
}
});
生命周期
Android 中的活动是层叠的,每启动一个活动就会覆盖在原活动之上,我们通过 Back
键销毁最上层的活动,然后下面的一个活动就会重新显示出来。
其实,Android 是使用任务(Task)来管理活动的,一个任务就是一组存放在栈里的活动集合,这个栈也被称为返回栈(Back Stack)。当我们启动一个活动的时候,它就会进入到任务栈中。
每个活动在其生命周期中最多可能会有四种状态:运行状态、暂停状态、停止状态和销毁状态。
Activity 类中定义了 7 个回调方法,覆盖了活动生命周期的每一个环节。
- onCreate():它会在活动第一次被创建的时候调用,完成初始化操作,比如加载布局、绑定事件等等。
- onStart():在活动由不可见变为可见的时候调用。
- onResume():在活动准备好和用户进行交互的时候调用,这时活动位于返回栈的栈顶,并且处于运行状态。
- onPause():这个方法在系统准备去启动或恢复另一个活动的时候调用,通常在这个方法内释放一些资源以及保存一些关键数据。
- onStop():在活动完全不可见的时候调用。
- onDestroy():在活动销毁前调用。
- onRestart():在活动由停止状态变为运行状态之前调用,也就是活动被重新启动了。
上面的方法除了 onRestra() 方法外,两两相对。
- 完整生存周期。在 onCreate() 方法中完成初始化操作,在 onDestroy() 方法中释放内存。
- 可见生存期。在 onStrat() 方法中对资源进行加载,在 onStop() 方法中对资源进行释放。
- 前台生存期。onResume() 方法和 onPause() 方法就是用来与用户接触的。
开始实战,新建一个 ActivityLifeCycleTest 项目,然后新建两个活动,它们的布局如下:
<!-- normal_layout -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="This is a normal activity"/>
</LinearLayout>
<!-- dialog_layout -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="This is a dialog activity"/>
</LinearLayout>
然后我们在 AndroidManifest.xml 中修改活动,指定其主题为对话框模式。
<activity
android:name=".DialogLayoutActivity"
android:exported="true"
android:theme="@style/Theme.AppCompat.Dialog">
</activity>
此外,我们再调整一下主活动的布局,添加按钮来去启动新的活动。这里的直接展示主活动修改后的代码:
package com.example.activitycycletest;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
public static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button1 = (Button) findViewById(R.id.start_normal_activity);
Button button2 = (Button) findViewById(R.id.start_dialog_activity);
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, NormalLayoutActivity.class);
startActivity(intent);
}
});
button2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, DialogLayoutActivity.class);
startActivity(intent);
}
});
}
@Override
protected void onStart() {
super.onStart();
Log.d(TAG, "onStart");
}
@Override
protected void onResume(){
super.onResume();
Log.d(TAG, "onResume");
}
@Override
protected void onPause(){
super.onPause();
Log.d(TAG, "onPause");
}
@Override
protected void onStop(){
super.onStop();
Log.d(TAG, "onStop");
}
@Override
protected void onDestroy(){
super.onDestroy();
Log.d(TAG, "onDestroy");
}
@Override
protected void onRestart(){
super.onRestart();
Log.d(TAG, "onRestart()");
}
}
有时候活动停止太久可能因为内存不够而被系统回收了,这样会导致某些重要的数据会丢失,因为当我们回到该活动的时候系统是调用 onCreate() 方法重新创建了活动。
所以,为了解决这个问题,可以使用 Activity 类提供的 onSaveInstanceState() 回调方法,它可以保证活动被回收前一定会被调用。该方法携带了一个 Bundle 类型的参数,它提供了一系列方法用于保护数据。
@Override
protected void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
String tempData = "Something you just typed";
outState.putString("data_key", tempData);
}
我们修改 onCreate() 方法来恢复保存的数据。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (savedInstanceState != null){
String tempData = savedInstanceState.getString("data_key");
Log.d(TAG, tempData);
}
}
Intent 还可以和 Bundle 结合来进行使用,比如把要传递的数据给到 Bundle 然后再把 Bundle 给到 Intent,等到了目标活动了之后再取出 Bundle 中的数据。
活动的启动模式
在实际项目中,我们根据特定的需求为每个活动指定恰当的启动模式,分别有:standard、singleTop、singleTask 和 singleInstance。
我们可以给 <activity>
标签指定 androod:launchMode
来选择启动模式。
standard:为默认的启动模式,在该模式下每启动一个新活动就会在返回栈中入栈,并处于栈顶的位置。然而系统并不在乎其是否独立存在返回栈中,每次启动系统都会创建该活动的一个新的实例。
singleTop:在启动活动时,系统如果发现返回栈的栈顶是该活动,则认为可以直接使用,就不会新建一个新的实例了。但如果该活动没有在栈顶,就还是会创建新的实例。
singleTask:每次启动该活动时,系统首先会在返回栈中检查是否存在该活动的实例,如果存在则直接使用该实例,并将该活动之上的所有活动全部出栈,否则就创建一个新的实例。
singleInstance:该模式下的活动会启用一个新的返回栈来管理活动,这样就可以实现活动的实例共享。
活动的实践
知道当前是在哪一个活动:
我们在 AcitivityTest 项目上直接 new 一个 BaseActivity 类,并不用注册,我们让它继承 AppcompatActivity,并重写 onCreate() 方法:
public class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
Log.d("BaseActivity", getClass().getSimpleName());
}
}
修改 FirstActivity、SecondActivity 和 ThirdActivity 的继承结构,让它们继承 BaseActivity。
这样我们就可以通过 Logcat 看到我们的活动输出了。
随时随地退出程序:
思路就是我们设置一个专门的集合类对所有活动进行管理。
我们新建一个 ActivityCollector 类作为活动管理器:
package com.example.activitytest;
import android.app.Activity;
import java.util.ArrayList;
import java.util.List;
public class ActivityCollector {
public static List<Activity> activityList = new ArrayList<>();
public static void addActivity(Activity activity){
activityList.add(activity);
}
public static void removeActivity(Activity activity){
activityList.remove(activity);
}
public static void finishAll(){
for (Activity activity : activityList){
if (!activity.isFinishing()){
activity.finish();
}
}
}
}
接下来,我们来修改 BaseActivity 中的代码:
public class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
Log.d("BaseActivity", getClass().getSimpleName());
ActivityCollector.addActivity(this);
}
@Override
protected void onDestroy(){
super.onDestroy();
ActivityCollector.removeActivity(this);
}
}
如果你想直接退出程序,就在那个地方调用 ActivityCollector.finishAll() 方法就可以了。
当然,我们可以在销毁所有活动的代码后再加上杀掉当前进程的代码,以保证程序完全退出:
android.os.Process.killProcess(android.os.Process.myPid());
启动活动的最佳写法
我们修改一下 SecondAcitvity 中的代码:
public void actionStart(Context context, String data1, String data2){
Intent intent = new Intent(context, SecondActivity.class);
intent.putExtra("param1", data1);
intent.putExtra("param2", data2);
context.startActivity(intent);
}
然后,我们在启动的时候调用该函数就好了:
SecondActivity.actionStart(FirstActivity.this, "data1", "data2");
Android 设计
常见控件用法
TextView
<TextView
android:id="@+id/text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="This is TextView"/>
- android:layout_width 和 android:layout_height 用来设置控件的宽和高,通常有三种设置:match_parent、fill_parent 和 wrap_content。
- android:text、android:textSize 和 android:textColor 分别用来指定文本的内容、文本大小和文本颜色。字体用 sp 作为单位。
- android:gravity 用来指定文字对齐方式,有 top、bottom、left、right、center等等,可以用 ”|“ 来指定多个值。
Button
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="This is Button"/>
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 点击后的事务逻辑
}
});
EditText
<EditText
android:id="@+id/edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Type something here"
android:maxLines="2"/>
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button)findViewById(R.id.button);
editText = (EditText) findViewById(R.id.edit_text);
button.setOnClickListener(this);
}
public void onClicke(View v){
if (v.getId() == R.id.button){
String inputText = editText.getText().toString();
Toast.makeText(MainActivity.this, inputText, Toast.LENGTH_SHORT).show();
}
}
- android:hint 指定提示性文本。
- android:maxlines 指定最大行数,如果内容超出限制,文本就会向上滚动。
ImageView
<ImageView
android:id="@+id/image_view"
android:layout_width="warp_content"
android:layout_height="wrap_content"
android:scr="@drawable/img_1"/>
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button)findViewById(R.id.button);
editText = (EditText) findViewById(R.id.edit_text);
imageView = (ImageView) findViewById(R.id.image_view);
button.setOnClickListener(this);
}
public void onClicke(View v){
if (v.getId() == R.id.button){
imageView.setImageResource(R.drawable.img_2);
}
}
ProgressBar
<ProgerssBar
android:id="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?android:attr/progressBarStyleHorizontal"
android:max="100"/>
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button)findViewById(R.id.button);
editText = (EditText) findViewById(R.id.edit_text);
imageView = (ImageView) findViewById(R.id.image_view);
progressBar = (ProgressBar) findViewById(R.id.progress_bar);
button.setOnClickListener(this);
}
public void onClicke(View v){
if (v.getId() == R.id.button){
if (progressBar.getVisibility() == View.GONE){
progressBar.setVisibility(View.VISIBLE);
} else{
progressBar.setVisibility(View.GONE);
}
}
}
public void onClicke(View v){
if (v.getId() == R.id.button){
int progress = progressBar.getProgress();
progerss = progress + 10;
progressBar.setProgress(progress);
}
}
- android:visibility 可以控制控件的现实,它有三个选项:visible、invisible 和 gone。可以通过代码 setVisibility() 方法传入 View.VISIBLE、View.INVISIBLE 和 View.GONE。
- style 可以将圆形进度条改为水平进度条。
- android:max 给进度条设置最大值,然后在代码中动态地更改进度条的进度。
AlertDialog
public void onClicke(View v){
if (v.getId() == R.id.button){
AlertDialog.Builder dialog = new AlertDialog.Builder(MainActivity.this);
dialog.setTitle("This is Dialog");
dialog.setMessage("Something important.");
dialog.setCancelable(false);
dialog.setPositiveButton("ok", new DialogInterface.OnClickLisener(){
@Override
public void onClick(DialogInterface dialog, int which){
}
});
dialog.setNegativeButton("Cancel", new DialogInterface.onClickLisener(){
@Override
public void onClick(DialogInterface dialog, int which){
}
});
dialog.show();
}
}
ProgressDialog
public void onClicke(View v){
if (v.getId() == R.id.button){
ProgressDialog progressDialog = new ProgressDialog(MainActivity.this);
progressDialog.setTitle("This is ProgressDialog");
progressDialog.setMessage("Loading...");
progressDialog.setCancelable(true);
progressDialog.show();
}
}
常见布局
线性布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<Button
android:id="@+id/button1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:text="Button1" />
<Button
android:id="@+id/button2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="Button2" />
<Button
android:id="@+id/button3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:text="Button3" />
<EditText
android:id="@+id/input_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_width="1"
android:hint="Type something" />
</LinearLayout>
- android:orientation 指定排列方向,水平就是参数 vertical,垂直就是参数 horizontal。
- android:layout_gravity 指定控件在布局中的对齐方式。
- android:layout_weight 属性值为 1,表示在水平方向宽度的占比,因为线性布局会将所有的 layout_weight 加起来然后再比上每个空间的宽度,最后划分控件位置。
相对布局
<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="Button1" />
<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="Button2" />
<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerParent="true"
android:text="Button3" />
<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="Button4" />
<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="Button5" />
</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/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerParent="true"
android:text="Button3" />
<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="Button1" />
<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="Button2" />
<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="Button4" />
<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="Button5" />
</RelativeLayout>
- android:layout_alignLeft 让一个控件的左边缘和另一个控件的左边缘对齐。
- android:layout_alignRight 让一个控件的右边缘和另一个控件的右边缘对齐。
- android:layout_alignTop 让一个控件的上边缘和另一个控件的上边缘对齐。
- android:layout_alignBottom 让一个控件的下边缘和另一个控件的下边缘对齐。
帧布局
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:text="This is a TextView" />
<ImageView
android:id="@+id/image_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layoit_gravity="left"
android:src="@ipmap/ic_launcher" />
</FrameLayout>
百分比布局
在 build.gradle.kts 依赖中添加百分比布局:
implementation ("androidx.percentlayout:percentlayout:1.0.0")
我们修改布局:
<?xml version="1.0" encoding="utf-8"?>
<androidx.percentlayout.widget.PercentFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/button1"
android:text="Button 1"
android:layout_gravity="left|top"
app:layout_widthPercent="50%"
app:layout_heightPercent="50%"
/>
<Button
android:id="@+id/button2"
android:text="Button 2"
android:layout_gravity="right|top"
app:layout_widthPercent="50%"
app:layout_heightPercent="50%"
/>
<Button
android:id="@+id/button3"
android:text="Button 3"
android:layout_gravity="left|bottom"
app:layout_widthPercent="50%"
app:layout_heightPercent="50%"
/>
<Button
android:id="@+id/button4"
android:text="Button 4"
android:layout_gravity="right|bottom"
app:layout_widthPercent="50%"
app:layout_heightPercent="50%"
/>
</androidx.percentlayout.widget.PercentFrameLayout>
自定义控件
我们所使用的控件都是直接或间接继承自 View 的,而所有布局呢又是直接或间接继承自 ViewGroup的。模仿苹果的 title 栏目:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/title_bg"
android:orientation="horizontal">
<Button
android:id="@+id/title_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="5dp"
android:background="@drawable/back_bg"
android:text="Back"
android:textColor="#fff" />
<TextView
android:id="@+id/title_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:gravity="center"
android:text="Title Text"
android:textColor="#fff"
adnroid:textSize="24sp" />
<Button
android:id="@+id/title_edit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="5dp"
android:background="@drawable/edit_bg"
android:text="Edit"
android:textColor="#fff" />
</LinearLayout>
<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>
最后,我们需要将系统自带的标题栏隐藏掉,我们在 OnCreate() 中加入以下代码:
ActionBar actionbar = getSupportActionBar();
if (actionbar != null){
actionbar.hide();
}
上述内容里,我们引入了布局,但是如果要更高效地处理控件响应事件,我们需要通过自定义控件的方式来解决:
public TtitleLayout extends LinearLayout{
public TitleLayout(Context context, AttributeSet attrs){
super(context, attrs);
// 可以动态地加载布局文件
LayoutInflater.from(context).inflate(R.layout.title, this);
// 注册点击事件
Button titleBack = (Button) findViewById(R.id.title_back);
Button titleEdit = (Button) findViewById(R.id.title_edit);
titleBack.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v){
((Aticity)getContext).finish();
}
});
titleEdit.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v){
Toast.makeText(getContext(), "You clicked Edit button", Toast.LENGTH_SHORT).show();
}
});
}
}
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.uicustomviews.TitleLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
最常用和最难用的控件
ListView 可以展示大量的数据,譬如QQ聊天记录这些长信息。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
</LinearLayout>
我们用一个 data 数组来测试,通过适配器来传输完成。
private String[] data = {"Apple", "Banna","Orange", "Cherry"};
@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 listView = (ListView) findViewById(R.id.list_view);
listView.setAdapter(adapter);
}
当然,如果只是显示文本的 ListView 就太单调了,我们对 ListView 界面进行一些定制。
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 String getImageId(){
return imageId;
}
我们为 ListView 的子项指定一个我们定义的布局,在 layout 下新建 fruit_item.xml。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/fruit_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp" />
<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>
创建一个自定义的适配器 FruitAdapater 来进行数据的展示:
public class FruitAdapter extends ArrayAdapter<Fruit>{
private int resourceId;
public FruitAdapter(Context context, int textViewResourceId, List<Fruit> objects){
super(context, textViewResourceId, objects);
resourceId = textViewResourceId;
}
@Override
public View getView(int position, View convertView, ViewGroup parent){
Fruit fruit = getItem(position);
View view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
fruitImage.setImageResource(fruit.getImageId());
furitName.setText(fruit.getName());
return view;
}
}
接下来我们在 MainActivity 中修改一下我们的代码:
public class MainActivity extends AppCompatActivity{
private List<Fruit> fruitList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initFruits();
FruitAdapter adapter = new FruitAdapter(MainActivity.this, R.layout.fruit_item, fruitList);
ListView listView = (ListView)findViewById(R.id.list_view);
listView.setAdapter(adapter);
}
private void initFruits(){
for (int i = 0; i < 50; i++){
Fruit apple = new Fruit("Apple", R.drawable.apple_pic);
fruitList.add(apple);
}
}
}
ListView 有很多细节是可以优化的,其中运行效率就是很重要的一点。在 FruitAdapter 的 getView() 方法中,每次都讲布局重新加载了一遍,当 ListView 快速滚动的时候,就会成为性能的瓶颈。
我们发现 getView() 方法中,有一个 convertView 参数,它用于将之前加载好的布局进行缓存。所以我们修改一下 FruitAdapter 中的代码:
@Override
public View getView(int position, View convertView, ViewGroup parent){
Fruit fruit = getItem(position);
View view;
if (convertView == null){
view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
} else{
view = convertView;
}
ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
fruitImage.setImageResource(fruit.getImageId());
furitName.setText(fruit.getName());
return view;
}
接着,我们的代码还是可以优化的,因为每次 getView() 方法中还是会调用 View 的 findViewById() 方法来获取一次控件的实例。我们可以借助 ViewHolder 来对这部分性能进行优化:
@Override
public View getView(int position, View convertView, ViewGroup parent){
Fruit fruit = getItem(position);
View view;
ViewHolder viewHolder;
if (convertView == null){
view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
viewHolder = new ViewHolder();
viewHolder.fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
viewHolder.furitName = (TextView) view.findViewById(R.id.fruit_name);
view.setTag(viewHolder); // 存储 ViewHolder
} else{
view = convertView;
viewHolder = (ViewHolder)view.getTag(); // 重新获取 ViewHolder
}
viewHolder.fruitImage.setImageResource(fruit.getImageId());
viewHodler.fruitName.setText(fruit.getName());
return view;
}
class ViewHolder{
ImageView fruitImage;
TextView fruitName;
}
除了数据的展示,ListView 的子项还得有点击的事件,让我们修改下代码:
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initFruits();
FruitAdapter adapter = new FruitAdapter(MainActivity.this, R.layout.fruit_item, fruitList);
ListView listView = (ListView) findViewById(R.id.list_view);
listView.setAdapter(adapter);
lsitView.setOnItemClickListener(new AdapterView.OnItemClickListener(){
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id){
Fruit fruit = fruitList.get(position);
Toast.makeText(MainActivity.this, fruit.getName(), Toast.LENGTH_SHORT).show();
}
});
}
更强大的滚动控件
Android 提供了一个更加强大的滚动控件 RecyclerView。
首先我们需要添加依赖:
implementation ("androidx.recyclerview:recyclerview:1.1.0")
然后我们修改 activity_main.xml 的代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
同样,我们也准备好 Fruit 类 和 fruit_item.xml 文件。
写下来,我们为 RecyclerView 准备一个适配器:
package com.yikuanzz.recyclerviewtest;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {
private List<Fruit> mFruitList;
static class ViewHolder extends RecyclerView.ViewHolder{
ImageView fruitImage;
TextView fruitName;
public ViewHolder(View view){
super(view);
fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
fruitName = (TextView) view.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(ViewHolder holder, int position) {
Fruit fruit = mFruitList.get(position);
Log.d("F_pos", String.valueOf(position));
holder.fruitImage.setImageResource(fruit.getImageId());
holder.fruitName.setText(fruit.getName());
}
@Override
public int getItemCount() {
return mFruitList.size();
}
}
然后我们修改 MainActivity 的代码:
package com.yikuanzz.recyclerviewtest;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.os.Bundle;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private List<Fruit> fruitList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化数据
initFruits();
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
// 将 recyclerView 放入线性布局中
recyclerView.setLayoutManager(linearLayoutManager);
FruitAdapter fruitAdapter = new FruitAdapter(fruitList);
// 将适配器加载到 recyclerView 中
recyclerView.setAdapter(fruitAdapter);
}
private void initFruits(){
for (int i = 0; i < 30; i++){
Fruit apple = new Fruit("Apple", R.drawable.ff1);
fruitList.add(apple);
Fruit cherry = new Fruit("Cherry ", R.drawable.aa1);
fruitList.add(cherry);
}
}
}
我们用 RecyclerView 做横向滚动的效果。
我们将 fruit_item 的布局进行一些修改:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="100dp"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/fruit_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"/>
<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 进行一点小修改:
package com.yikuanzz.recyclerviewtest;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.os.Bundle;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private List<Fruit> fruitList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化数据
initFruits();
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
// 设置为水平
linearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
// 将 recyclerView 放入线性布局中
recyclerView.setLayoutManager(linearLayoutManager);
FruitAdapter fruitAdapter = new FruitAdapter(fruitList);
// 将适配器加载到 recyclerView 中
recyclerView.setAdapter(fruitAdapter);
}
private void initFruits(){
for (int i = 0; i < 30; i++){
Fruit apple = new Fruit("Apple", R.drawable.ff1);
fruitList.add(apple);
}
}
}
之所以 RecyclerView 能够轻易地完成这些任务,主要还是得益于 LayoutManager ,它提供了一套可拓展的布局排列接口。
那么除了 LinearLayoutManager 之外,RecyclerView 还给我们提供了 GridLayoutManager 和 StaggeredGridLayoutManager 这两种内置的布局排列方式。GridLayoutManager 用于实现网格布局,StaggeredGridLayoutManager 用于实现瀑布流布局。
我们这里看一下瀑布流布局是怎么做的。
首先,我们还是修改一下 fruit_item.xml 中的代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_margin="5dp">
<ImageView
android:id="@+id/fruit_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"/>
<TextView
android:id="@+id/fruit_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:layout_marginTop="10dp"/>
</LinearLayout>
接着修改 MainActivity 中的代码:
package com.yikuanzz.recyclerviewtest;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
import android.os.Bundle;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class MainActivity extends AppCompatActivity {
private List<Fruit> fruitList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化数据
initFruits();
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
StaggeredGridLayoutManager staggeredGridLayoutManager = new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL);
// 将 recyclerView 放入瀑布布局中
recyclerView.setLayoutManager(staggeredGridLayoutManager);
FruitAdapter fruitAdapter = new FruitAdapter(fruitList);
// 将适配器加载到 recyclerView 中
recyclerView.setAdapter(fruitAdapter);
}
private void initFruits(){
for (int i = 0; i < 30; i++){
Fruit apple = new Fruit(getRandomLengthName("Apple"), R.drawable.ff1);
fruitList.add(apple);
}
}
private String getRandomLengthName(String name){
Random random = new Random();
int length = random.nextInt(20)+1;
StringBuilder builder = new StringBuilder();
for (int i = 0; i < length; i++){
builder.append(name);
}
return builder.toString();
}
}
除此之外,我们还要给 RecyclerView 设置点击事件。但是 RecyclerView 并没有提供类似于 setOnItemClickListener() 这样注册监听器方法,需要我们自己给子项具体的 View 去注册点击事件。
接下来,我们修改一下 FruitAdapter 中的代码:
package com.yikuanzz.recyclerviewtest;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {
private List<Fruit> mFruitList;
static class ViewHolder extends RecyclerView.ViewHolder{
View fruitView;
ImageView fruitImage;
TextView fruitName;
public ViewHolder(View view){
super(view);
fruitView = view;
fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
fruitName = (TextView) view.findViewById(R.id.fruit_name);
}
}
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);
final ViewHolder holder = new ViewHolder(view);
holder.fruitView.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
int position = holder.getAdapterPosition();
Fruit fruit = mFruitList.get(position);
Toast.makeText(v.getContext(), "you clicked view" + fruit.getName(), Toast.LENGTH_SHORT).show();
}
});
holder.fruitImage.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
int position = holder.getAdapterPosition();
Fruit fruit = mFruitList.get(position);
Toast.makeText(v.getContext(), "you clicked image" + fruit.getName(), Toast.LENGTH_SHORT).show();
}
});
return holder;
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
Fruit fruit = mFruitList.get(position);
Log.d("F_pos", String.valueOf(position));
holder.fruitImage.setImageResource(fruit.getImageId());
holder.fruitName.setText(fruit.getName());
}
@Override
public int getItemCount() {
return mFruitList.size();
}
}
界面实战-简单聊天界面
我们创建 UIBaseProject 项目来开始我们的实践。
开始之前,我们学习一下怎么制作 Nine-Patch 图片。它是一种被处理过的 png 图片,能够指定哪些区域可以被拉伸、哪些区域不可以。
现在我们有一张图片是 message_left.png
我们将图片放入 drawable 文件中,然后将图片后缀改为 .9.png
,点击后进入图片制作视图。
然后我们给图像四周画上小黑边来设置可以拖拽延伸到地方。
我们将图片反转,用同样的方法得到 message_right.9.png。
好了,我们可以开始编写了。
首先还是导入 recyclerview 的依赖:
implementation ("androidx.recyclerview:recyclerview:1.1.0")
接着我们修改主界面:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#d8e0e8"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/msg_recycler_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<EditText
android:id="@+id/input_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Type something here"
android:maxLines="2" />
<Button
android:id="@+id/send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Send"/>
</LinearLayout>
</LinearLayout>
我们用一个 EditText 来给用户进行输入,Button 来发送信息,而 RecyclerView 就是用来显示聊天的信息内容。
然后,我们定义一下 Msg 实体类:
package com.yikuanzz.uibasepratice;
public class Msg {
public static final int TYPE_RECEIVED = 0;
public static final int TYPE_SENT = 1;
private String content;
private int type;
public Msg(String content, int type){
this.content = content;
this.type = type;
}
public String getContent(){
return content;
}
public int getType(){
return type;
}
}
其中,type 表示消息都类型,TYPE_RECEIVED 表示这是一条收到的信息,TYPE_SENT 表示这是一条发出的消息。
接下来我们写一下 RecyclerView 子项的布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/left_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:background="@drawable/message_left">
<TextView
android:id="@+id/left_msg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="10dp"
android:textColor="#fff"/>
</LinearLayout>
<LinearLayout
android:id="@+id/right_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:background="@drawable/message_right">
<TextView
android:id="@+id/right_msg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="10dp"/>
</LinearLayout>
</LinearLayout>
接下来,我们创建 RecycleView 的适配器,新建类 MsgAdapter:
package com.yikuanzz.uibasepratice;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
public class MsgAdapter extends RecyclerView.Adapter<MsgAdapter.ViewHolder> {
private List<Msg> mMsgList;
static class ViewHolder extends RecyclerView.ViewHolder{
LinearLayout leftLayout;
LinearLayout rightLayout;
TextView leftMsg;
TextView rightMsg;
public ViewHolder(@NonNull View view) {
super(view);
leftLayout = (LinearLayout) view.findViewById(R.id.left_layout);
rightLayout = (LinearLayout) view.findViewById(R.id.right_layout);
leftMsg = (TextView) view.findViewById(R.id.left_msg);
rightMsg = (TextView) view.findViewById(R.id.right_msg);
}
}
public MsgAdapter(List<Msg> msgList){
mMsgList = msgList;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.msg_item, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
Msg msg = mMsgList.get(position);
if (msg.getType() == Msg.TYPE_RECEIVED){
// 如果是收到消息就显示左边布局的信息,将右边布局信息隐藏
holder.leftLayout.setVisibility(View.VISIBLE);
holder.rightLayout.setVisibility(View.GONE);
holder.leftMsg.setText(msg.getContent());
} else if (msg.getType() == Msg.TYPE_SENT){
// 如果是发出消息就显示右边布局的信息,将左边布局信息隐藏
holder.rightLayout.setVisibility(View.VISIBLE);
holder.leftLayout.setVisibility(View.GONE);
holder.rightMsg.setText(msg.getContent());
}
}
@Override
public int getItemCount() {
return mMsgList.size();
}
}
最后我们在 MainAcitvity 将我们的代码写上:
package com.yikuanzz.uibasepratice;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private List<Msg> msgList = new ArrayList<>();
private EditText inputText;
private Button send;
private RecyclerView msgRecyclerView;
private MsgAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化数据和控件绑定
initMsgs();
inputText = (EditText) findViewById(R.id.input_text);
send = (Button) findViewById(R.id.send);
msgRecyclerView = (RecyclerView) findViewById(R.id.msg_recycler_view);
// 线性管理器和适配器
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
msgRecyclerView.setLayoutManager(layoutManager);
adapter = new MsgAdapter(msgList);
msgRecyclerView.setAdapter(adapter);
// 设置点击事件
send.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String content = inputText.getText().toString();
if (!"".equals(content)){
Msg msg = new Msg(content, Msg.TYPE_SENT);
msgList.add(msg);
// 有新信息的时候刷新
adapter.notifyItemInserted(msgList.size() - 1);
// 定位到最后一行
msgRecyclerView.scrollToPosition(msgList.size() - 1);
// 清空输入狂内容
inputText.setText("");
}
}
});
}
private void initMsgs(){
Msg msg1 = new Msg("Hello man.", Msg.TYPE_RECEIVED);
msgList.add(msg1);
Msg msg2 = new Msg("Hello.You Good?", Msg.TYPE_SENT);
msgList.add(msg2);
Msg msg3 = new Msg("This is Tom. Nice talking to you!", Msg.TYPE_RECEIVED);
msgList.add(msg3);
}
}

Android 碎片
碎片(Fragment)是一种可以嵌入在活动当中的 UI 片段,它能让程序更加合理和充分地利用大屏幕的空间,因为在平板上应用得非常广泛。
碎片的使用
我们使用平板模拟器,创建一个 FragmentTest 项目。
然后新建一个左侧碎片布局 left_fragment.xml。
<?xml version="1.0" encoding="utf-8"?>
<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>
再创建一个右侧碎片布局 right_fragment.xml 。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#00ff00">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="This is right fragment"/>
</LinearLayout>
接着我们分别创建一个 LeftFragment 类 和 RightFragment类。
// LeftFragment
public class LeftFragment extends Fragment {
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.left_fragement, container, false);
return view;
}
}
// RightFragment
public class RightFragment extends Fragment {
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.right_fragement, container, false);
return view;
}
}
接下来,我们修改一下 activity_main.xml 中的代码。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<fragment
android:id="@+id/left_fragment"
android:name="com.yikuanzz.fragementtest.LeftFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<fragment
android:id="@+id/right_fragment"
android:name="com.yikuanzz.fragementtest.RightFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
</LinearLayout>
当然碎片的强大之处在于它能够在程序运行时动态地添加到活动当中,我们新建 another_right_fragment.xml 。
<?xml version="1.0" encoding="utf-8"?>
<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="20sp"
android:text="This is another right fragment"/>
</LinearLayout>
然后我们修改 activity_main.xml 中的右碎片为布局:
<FrameLayout
android:id="@+id/right_fragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
</FrameLayout>
在代码中,我们用按钮来添加碎片:
package com.yikuanzz.fragementtest;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button) findViewById(R.id.button);
replaceFragment(new RightFragment());
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
replaceFragment(new AnotherRightFragment());
}
});
}
private void replaceFragment(Fragment fragment){
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.right_layout, fragment);
transaction.commit();
}
}
其实我们发现,动态添加碎片的步骤就是:
- (1)创建待添加的碎片实例。
- (2)获取 FragementManager,在活动中可以调用 getSupportFragmentManager() 方法。
- (3)开启一个事务,调用 beginTransaction() 方法开启。
- (4)向容器内添加或替换碎片,一般使用 replace() 方法实现,需要传入容器的 id 和待添加的碎片实例。
- (5)提交事务,调用 commite() 方法来完成。
FragmentTransaction 提供了一个 addToBackStack() 方法,用于将一个事务添加到返回栈中:
private void replaceFragment(Fragment fragment){
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.right_layout, fragment);
transaction.addToBackStack(null);
transaction.commit();
}
FragmentManager 提供了一个类似于 findViewById() 的方法,专门用于从布局文件中获取碎片的实例:
RightFragment rightFragment = (RightFragment) getFragmentManager().findFragmentById(R.id.right_fragment);
此外,碎片也可以调用活动:
MainActivity activity = (MainActivity) getActivity();
碎片的生命周期
运行状态:当一个碎片是可见的,并且它所关联的活动正处于运行状态时,该碎片也处于运行状态。
暂停状态:当活动进入暂停状态,与它关联的可见碎片就会进入到暂停状态。
停止状态:当活动进入停止状态时,与他关联的碎片会进入到停止状态或者是从活动中移除。
销毁状态:活动被销毁时,与它关联的碎片也就进入到销毁状态。
- onAttach()。当前碎片和活动建立关联的时候调用。
- onCreateView()。为碎片创建视图时调用。
- onActivityCreated()。确保与碎片相关联的活动一定已经创建完毕的时候调用。
- onDestoryView()。当与碎片关联的视图被移除的时候调用。
- onDetach()。当碎片和活动解除关联的时候调用。
注意,在新版本中 onActivityCreated() 方法被弃用,应该使用 onViewCreated() 方法替代。
动态加载布局的技巧
使用限定符(Qualifiers) 来判断程序是使用双页模式还是单页模式。
我们修改 activity_main.xml :
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<fragment
android:id="@+id/left_fragment"
android:name="com.yikuanzz.fragementtest.LeftFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
接着我们新建 layout-large 文件夹,在里面新建一个布局,也叫 activity_main.xml 。
<?xml version="1.0" encoding="utf-8"?>
<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/left_fragment"
android:name="com.yikuanzz.fragementtest.LeftFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<fragment
android:id="@+id/right_fragment"
android:name="com.yikuanzz.fragementtest.RightFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3"/>
</LinearLayout>
这样,如果我们在手机启动这个应用就只会显示左边的碎片,而在使用平板的时候就会显示右边的碎片。
当然我们可以用最小宽度限定符来指定屏幕宽度的临界点,如果屏幕大于这个值,那么设备就加载较大的布局。
我们新建一个 layout-sw600dp 文件夹,然后在这个文件夹下新建 activity_main.xml 布局:
<?xml version="1.0" encoding="utf-8"?>
<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/left_fragment"
android:name="com.yikuanzz.fragementtest.LeftFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<fragment
android:id="@+id/right_fragment"
android:name="com.yikuanzz.fragementtest.RightFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3"/>
</LinearLayout>
碎片实战-简易的新闻应用
新建一个项目叫 FragmentBestPratice ,我们在这个项目上完成我们的实践。
我们的项目步骤大概是这样的:
- 1.准备新闻内容的碎片和碎片渲染;
- 2.准备新闻列表的碎片和渲染;
- 3.准备低分辨率主界面和高分辨率主界面。
这里我们要用到 RecyclerView,所以我们先引入依赖:
implementation ("androidx.recyclerview:recyclerview:1.1.0")
接着我们准备一个 New 实体类来存放新闻:
package com.yikuanzz.fragementbestpratice;
public class News {
private String title;
private String content;
public void setTitle(String title){
this.title = title;
}
public String getContent(){
return content;
}
public void setContent(String content){
this.content = content;
}
}
准备新闻内容的布局
为了处理这个内容,我们需要把新闻内容放入 Fragment 中,所以我们这里写一个 new_content_frag.xml 用来展示新闻的内容:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/visibility_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="invisible">
<TextView
android:id="@+id/news_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="10dp"
android:textSize="20sp"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#000"/>
<TextView
android:id="@+id/news_content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:padding="15dp"
android:textSize="18sp"/>
</LinearLayout>
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_alignParentLeft="true"
android:background="#000"/>
</RelativeLayout>
然后,我们再新建一个 NewsContentFragment 来渲染我们的新闻内容碎片:
package com.yikuanzz.fragementbestpratice;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
public class NewsContentFragment extends Fragment {
private View view;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
view = inflater.inflate(R.layout.news_content_frag, container, false);
return view;
}
public void refresh(String newsTitle, String newsContent){
View visibilityLayout = view.findViewById(R.id.visibility_layout);
visibilityLayout.setVisibility(View.VISIBLE);
TextView newsTitleText = (TextView) view.findViewById(R.id.news_title);
TextView newsContentText = (TextView) view.findViewById(R.id.news_content);
newsTitleText.setText(newsTitle); // 刷新新闻标题
newsContentText.setText(newsContent); // 刷新新闻内容
}
}
接着我们再写一个 news_content.xml 用来引入我们写的 news_content_frag.xml 内容:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".NewsContentActivity">
<fragment
android:id="@+id/news_content_fragment"
android:name="com.yikuanzz.fragementbestpratice.NewsContentFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
之后,我们写一个 Fragment 的启动就好了,我们写在 NewsContentActivity.java 中:
package com.yikuanzz.fragementbestpratice;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
public class NewsContentActivity extends AppCompatActivity {
public static void actionStart(Context context, String newsTitle, String newsContent{
Intent intent = new Intent(context, NewsContentActivity.class);
intent.putExtra("news_title", newsTitle);
intent.putExtra("news_content", newsContent);
context.startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.news_content);
String newsTitle = getIntent().getStringExtra("news_title");
String newsContent = getIntent().getStringExtra("news_content");
NewsContentFragment newsContentFragment =(NewsContentFragment) getSupportFragmentManager().findFragmentById(R.id.news_content_fragment);
newsContentFragment.refresh(newsTitle, newsContent);
}
}
准备新闻列表
这里我们要用到 RecyclerView 所以我们一定写的是 news_title_frag.xml 和 news_title_item.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/news_title_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/news_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="end"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:paddingTop="15dp"
android:paddingBottom="15dp"/>
好了,我们接下来就要准备 NewsTitleFragment 了,因为要启动 RecyclerView 所以我们还要写的就是 Adapter 但是我们可以写入 NewsTitleFragment 中,这样我们还可以判断是双页显示还是单页显示,然后将数据刷新进去:
package com.yikuanzz.fragementbestpratice;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class NewsTitleFragment extends Fragment {
private boolean isTwoPane;
class NewsAdapter extends RecyclerView.Adapter<NewsAdapter.ViewHolder>{
private List<News> mNewsList;
class ViewHolder extends RecyclerView.ViewHolder{
TextView newsTitleText;
public ViewHolder(View view){
super(view);
newsTitleText = (TextView) view.findViewById(R.id.news_title);
}
}
public NewsAdapter (List<News> newsList){
mNewsList = newsList;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.news_title_item, parent, false);
final ViewHolder holder = new ViewHolder(view);
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
News news = mNewsList.get(holder.getAdapterPosition());
if (isTwoPane){
// 如果是双页模式就刷新 NewsContentFragment 中的内容
NewsContentFragment newsContentFragment =
(NewsContentFragment) getChildFragmentManager().
findFragmentById(R.id.news_content_fragment);
newsContentFragment.refresh(news.getTitle(), news.getContent());
} else{
// 如果是双页模式就启动 NewsContentActivity
NewsContentActivity.actionStart(getActivity(), news.getTitle(), news.getContent());
}
}
});
return holder;
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
News news = mNewsList.get(position);
holder.newsTitleText.setText(news.getTitle());
}
@Override
public int getItemCount() {
return mNewsList.size();
}
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.news_title_frag, container, false);
// 绑定 RecyclerView
RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.news_title_recycler_view);
LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(layoutManager);
NewsAdapter newsAdapter = new NewsAdapter(getNews());
recyclerView.setAdapter(newsAdapter);
return view;
}
private List<News> getNews(){
List<News> newsList = new ArrayList<>();
for (int i = 1; i <= 50; i++){
News news = new News();
news.setTitle("This is news title" + i);
news.setContent(getRandomLengthContent("This is news content" + i + "."));
newsList.add(news);
}
return newsList;
}
private String getRandomLengthContent(String content){
Random random = new Random();
int length = random.nextInt(20) + 1;
StringBuilder builder = new StringBuilder();
for (int i = 0; i < length; i++){
builder.append(content);
}
return builder.toString();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
if (getActivity().findViewById(R.id.news_content_layout) != null){
isTwoPane = true;
} else{
isTwoPane = false;
}
}
}
准备单页和双页的布局
我们要修改 activity_main.xml 中的内容,此外我们还新建一个 layput-sw600p 文件夹中,在里面写 activity_main.xml 文件,该文件在分辨率大于值的时候应用:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<fragment
android:id="@+id/news_title_fragment"
android:name="com.yikuanzz.fragementbestpratice.NewsTitleFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
<?xml version="1.0" encoding="utf-8"?>
<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/news_title_fragment"
android:name="com.yikuanzz.fragementbestpratice.NewsTitleFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<FrameLayout
android:id="@+id/news_content_layout"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3" >
<fragment
android:id="@+id/news_content_fragment"
android:name="com.yikuanzz.fragementbestpratice.NewsContentFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
</LinearLayout>
所以,最后我们回顾一下整个项目的启动:
- 1.项目判断设备的分辨率启动不同的 activity_main.xml;
- 2.对应 acivity_main.xml 中的 Fragment 启动;
- 3.NewsTitleFragment 启动并判断单双模式;
- 4.Adapter 根据单双页模式渲染点击事件的布局。
Android 广播
Android 广播主要分为两种类型:
- 标准广播(Normal broadcast):完全异步执行的广播,在广播发出后,所有的广播接收器几乎会在同一时间内接收到这条广播消息。
- 有序广播(Ordered broadcast):同步执行的广播,在广播发出后同一时间只会有一个广播接收器能够收到这条广播消息,只有当这个广播接收器处理完毕后才会传给下一个广播接收器。
动态注册监听网络变化
我们新建一个 BroadCastTest 项目,修改 MainActivity 中的代码:
package com.yikuanzz.broadcasttest;
import androidx.appcompat.app.AppCompatActivity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private IntentFilter intentFilter;
private NetworkChangeReceiver networkChangeReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
intentFilter = new IntentFilter(); // 设置监听的广播类型
intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
networkChangeReceiver = new NetworkChangeReceiver();
registerReceiver(networkChangeReceiver, intentFilter);
}
@Override
protected void onDestroy() {
super.onDestroy(); // 取消动态注册
unregisterReceiver(networkChangeReceiver);
}
class NetworkChangeReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
// 获取系统网络连接服务
ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
if (networkInfo != null && networkInfo.isAvailable()){
Toast.makeText(context, "network is availabel", Toast.LENGTH_SHORT).show();
} else{
Toast.makeText(context, "network is unavailable", Toast.LENGTH_SHORT).show();
}
}
}
}
需要额外说明的是,Android 为了保护用户设备的安全和隐私,如果我们要进行一些操作需要在 AndroidManifest.xml 文件中声明权限:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
静态注册实现开机启动
静态注册可以在程序为启动之前接收广播。
我们新建一个 Broadcast Receiver 叫 BootCompleteReceiver:
package com.yikuanzz.broadcasttest;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
public class BootCompleteReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// TODO: This method is called when the BroadcastReceiver is receiving
// an Intent broadcast.
// throw new UnsupportedOperationException("Not yet implemented");
Toast.makeText(context, "Boot Complete", Toast.LENGTH_SHORT).show();
}
}
然后我们修改 AndroidManifest.xml 文件:
<receiver
android:name=".BootCompleteReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
这样我们开机的时候就可以收到广播了。
自定义广播
标准广播
我们静态注册一个 MyBroadcastReceiver:
package com.yikuanzz.broadcasttest;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
public class MyBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// TODO: This method is called when the BroadcastReceiver is receiving
// an Intent broadcast.
// throw new UnsupportedOperationException("Not yet implemented");
Toast.makeText(context., "received in MyBroadcastReceiver", Toast.LENGTH_SHORT).show();
}
}
我们让它接收一条 com.example.broadcasttest.MY_BROADCAST
广播:
<receiver
android:name=".MyBroadcastReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.example.broadcasttest.MY_BROADCAST" />
</intent-filter>
</receiver>
我们在主活动的布局中添加一个按钮:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
然后在主活动中设置监听器通过按钮发送广播:
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST");
sendBroadcast(intent);
}
});
有序广播
我们验证一下其它应用程序也是能接收到广播。
我们新建一个项目,然后定义 AnotherBroadcastReceiver:
package com.yikuanzz.broadcasttest2;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
public class AnotherBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// TODO: This method is called when the BroadcastReceiver is receiving
// an Intent broadcast.
// throw new UnsupportedOperationException("Not yet implemented")
Toast.makeText(context, "received in AnotherBroadcastReceiver", Toast.LENGTH_SHORT).show();
}
}
我们让它接收一条 com.example.broadcasttest.MY_BROADCAST
广播:
<receiver
android:name=".AnotherBroadcastReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.example.broadcasttest.MY_BROADCAST"/>
</intent-filter>
</receiver>
现在我们启动这个项目后,点击上个项目的广播发送按钮,会发现两个程序都能收到广播。当然,如果要发送有序广播,我们还要修改一下 MainActivity 的代码:
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST");
sendOrderedBroadcast(intent, null);
}
});
我们可以在 AndroiManifest.xml 中设置广播接收优先级:
<intent-filter android:priority="100">
<action android:name="com.example.broadcasttest.MY_BROADCAST" />
</intent-filter>
然后可以在广播接收器中截断广播,不让广播继续传播:
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "received in MyBroadcastReceiver", Toast.LENGTH_SHORT).show();
abortBroadcast();
}
本地广播
本地广播就是只在程序中传播的广播机制。
主要就是用 LocalBroadcastManager 来对广播进行管理。
我们修改 MainAcitvity.java 中的代码:
package com.yikuanzz.broadcasttest;
import androidx.appcompat.app.AppCompatActivity;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private IntentFilter intentFilter;
private LocalReceiver localReceiver;
private LocalBroadcastManager localBroadcastManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
localBroadcastManager = LocalBroadcastManager.getInstance(this);
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.example.brocasttest.LOCAL_BROADCAST");
localBroadcastManager.sendBroadcast(intent); // 发送本地广播
}
});
intentFilter = new IntentFilter();
intentFilter.addAction("com.example.brocasttest.LOCAL_BROADCAST");
localReceiver = new LocalReceiver();
localBroadcastManager.registerReceiver(localReceiver, intentFilter); // 注册监听器
}
@Override
protected void onDestroy() {
super.onDestroy(); // 取消动态注册
localBroadcastManager.unregisterReceiver(localReceiver);
}
class LocalReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "received local broadcast", Toast.LENGTH_SHORT).show();
}
}
}
广播实战-强制下线
我们新建一个 BroadcastBestPractice 项目。
我们新建一个 ActivityCollector 类用于管理所有的活动:
package com.yikuanzz.broadcastbestpratice;
import android.app.Activity;
import java.util.ArrayList;
import java.util.List;
public class ActivityCollector {
public static List<Activity> activityList = new ArrayList<>();
public static void addActivity(Activity activity){
activityList.add(activity);
}
public static void removeActivity(Activity activity){
activityList.remove(activity);
}
public static void finishAll(){
for (Activity activity : activityList){
if (!activity.isFinishing()){
activity.finish();
}
}
}
}
然后创建 BaseActivity 类作为所有活动的父类:
package com.yikuanzz.broadcastbestpratice;
import android.os.Bundle;
import android.os.PersistableBundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
public class BaseActivity extends AppCompatActivity {
@Override
public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) {
super.onCreate(savedInstanceState, persistentState);
ActivityCollector.addActivity(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
ActivityCollector.removeActivity(this);
}
}
接下来,我们新建一个登录界面的活动 LoginActivity 我们编辑它的布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".LoginActivity">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="60dp">
<TextView
android:layout_width="90dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textSize="18sp"
android:text="Account: "/>
<EditText
android:id="@+id/account"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="60dp">
<TextView
android:layout_width="90dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textSize="18sp"
android:text="Password: "/>
<EditText
android:id="@+id/password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"/>
</LinearLayout>
<Button
android:id="@+id/login"
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="Login"/>
</LinearLayout>
然后我们修改 LoginActivity 中代码:
package com.yikuanzz.broadcastbestpratice;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
public class LoginActivity extends BaseActivity {
private EditText accountText;
private EditText passwordText;
private Button login;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
// 绑定组件
accountText = (EditText) findViewById(R.id.account);
passwordText = (EditText) findViewById(R.id.password);
login = (Button) findViewById(R.id.login);
login.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 实现登录
String account = accountText.getText().toString();
String password = passwordText.getText().toString();
if (account.equals("admin") && password.equals("123456")){
Intent intent = new Intent(LoginActivity.this, MainActivity.class);
startActivity(intent);
} else {
Toast.makeText(LoginActivity.this, "account or passsword is invalid", Toast.LENGTH_SHORT).show();
}
}
});
}
}
然后我们在主活动的布局中添加一个按钮用来触发下线功能:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/force_offline"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
接着我们修改主活动的代码:
package com.yikuanzz.broadcastbestpratice;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class MainActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button forceOffline = (Button) findViewById(R.id.force_offline);
forceOffline.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.example.broadcastbestpratice.FORCE_OFFLINE");
sendBroadcast(intent);
}
});
}
}
接下来,我们只要在 BaseActivity 中添加一个广播接收器就可以了:
package com.yikuanzz.broadcastbestpratice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.os.PersistableBundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
public class BaseActivity extends AppCompatActivity {
private ForceOffLineReceiver receiver;
@Override
public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) {
super.onCreate(savedInstanceState, persistentState);
ActivityCollector.addActivity(this);
}
@Override
protected void onResume() {
super.onResume();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction("com.example.broadcastbestpratice.FORCE_OFFLINE");
receiver = new ForceOffLineReceiver();
registerReceiver(receiver, intentFilter);
}
@Override
protected void onPause() {
super.onPause();
if (receiver != null){
unregisterReceiver(receiver);
receiver = null;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
ActivityCollector.removeActivity(this);
}
class ForceOffLineReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle("Warning");
builder.setMessage("You are forced to be offline. Please try to login again.");
builder.setCancelable(false);
builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ActivityCollector.finishAll();
Intent intent = new Intent(context, LoginActivity.class);
context.startActivity(intent);
}
});
builder.show();
}
}
}
之后我们再修改一下 AndroidManifest.xml 让登录界面作为启动活动:
<activity
android:name=".LoginActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
android:exported="true">
</activity>
Android 存储
文件存储
文件存储就是不对存储内容做任何个格式化处理。
Context 类提供了 openFileOutput() 方法,可以用于将数据存储到指定文件中:
- 参数一:文件名,所有文件默认放在
/data/data/<packagename>/files/
目录下。 - 参数二:文件操作模式,MODE_PRIVATE 和 MODE_APPEND。
- MODE_PRIVATE 写入时覆盖。
- MODE_APPEND 追加内容。
该方法返回的是一个 FileOutputStream 对象。
我们看一下实例:
public void save(){
String data = "Data to save";
FileOutputStream out = null;
BufferedWriter writer = null;
try{
out = openFileOutput("data", Context.MODE_PRIVATE);
writer = new BufferedWriter(new OutputStreamWriter(out));
writer.write(data);
} catch (IOException e){
e.printStackTrace();
} finally{
try {
if (writer != null){
writer.close();
}
} catch (IOException e){
e.printStackTrace();
}
}
}
接下来做个例子,创建 FilePerisistenceTest 项目,修改主活动布局的代码,添加一个输入框,我们想让输入框的数据保存下来。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<EditText
android:id="@+id/edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Type something here."/>
</LinearLayout>
package com.yikuanzz.filepersistencetest;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.os.Bundle;
import android.widget.EditText;
import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
public class MainActivity extends AppCompatActivity {
private EditText edit;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
edit = (EditText) findViewById(R.id.edit);
}
@Override
protected void onDestroy() {
super.onDestroy();
String inputText = edit.getText().toString();
save(inputText);
}
public void save(String inputText){
FileOutputStream out = null;
BufferedWriter writer = null;
try{
out = openFileOutput("data", Context.MODE_PRIVATE);
writer = new BufferedWriter(new OutputStreamWriter(out));
writer.write(inputText);
} catch (IOException e){
e.printStackTrace();
} finally{
try{
if (writer != null){
writer.close();
}
} catch (IOException e){
e.printStackTrace();
}
}
}
}
那么,我们上面的例子就完成了数据的写入,现在我们再做一个读取的例子。
我们会用到 openFileInput() 方法。
public String load(){
FileInputStream in = null;
BufferedReader reader = null;
StringBuilder content = new StringBuilder();
try{
int = 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();
}
}
}
}
String inputText = load();
if (!TextUtils.isEmpty(inputText)){
edit.setText(inputText);
edit.setSelection(inputText.length());
Toast.makeText(this, "Restoring succeeded", Toast.LENGTH_SHORT).show();
}
SharedPreference 存储
SharedPreference 是使用键值对的方式来存储数据的。
Context 类中的 getSharedPreferences() 方法
- 参数一:文件名,放在
/data/data/<package name>/shared_prefs/
目录下。 - 参数二:指定操作模式,只有 MODE_PRIVATE。
Activity 类中 gerPreferences() 方法
- 只接收操作模式参数,它自动将当前活动类名作为文件名。
PreferenceManager 类中的 getDefaultSharedPreferences() 方法
- 接收一个 Context 参数,并自动使用当前应用程序包名作为前缀命名,得到一个 SharedPreferences 对象。
- 通过调用 SharedPreferences 对象 edit() 方法获取 SharedPreferences.Editor 对象。
- 向 SharedPreferences.Editor 对象中添加数据。
- 调用 apply() 方法将添加到数据提交。
新建一个项目,开始实践。首先修改 activity_main.xml 中的代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/save_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Save data" />
<Button
android:id="@+id/restore_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Resotre data" />
</LinearLayout>
我们用按钮响应将一些数据存在 SharedPreferences 文件中:
package com.yikuanzz.sharedpreferencestest;
import androidx.appcompat.app.AppCompatActivity;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button save_data = (Button) findViewById(R.id.save_data);
save_data.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SharedPreferences.Editor editor = getSharedPreferences("data", MODE_PRIVATE).edit();
editor.putString("name", "Tom");
editor.putInt("age", 18);
editor.putBoolean("married", false);
editor.apply();
}
});
Button restore_data = (Button) findViewById(R.id.restore_data);
restore_data.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
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("DataInSP", "name is " + name);
Log.d("DataInSP", "age is " + age);
Log.d("DataInSP", "married is " + married);
}
});
}
}
SQListe 数据库存储
Android 提供了 SQLiteOpenHelper 帮助我们使用数据库。
- getReadableDatebase() 和 getWritableDatabase() 用于创建或打开一个现有数据库。
- 构造方法有四个参数:
- Context 上下文。
- 数据库名。
- Cursor 游标。
- 数据库版本号。
- 数据库文件一般放在
/data/data/<package name>/databases/
目录。
我们创建一个项目来实践一下:
package com.yikuanzz.databasetest;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.widget.Toast;
public class MyDatabaseHelper extends SQLiteOpenHelper {
public static final String CREATE_BOOK = "create table Book(" +
"id integer primary key autoincrement," +
"author text," +
"price real," +
"pages integer," +
"name text)";
public static final String CREATE_CATEGORY = "create table Category(" +
"id integer primary key autoincrement," +
"category_name text," +
"category_code integer)";
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);
db.execSQL(CREATE_CATEGORY);
Toast.makeText(mContext, "Create succeeded", Toast.LENGTH_SHORT).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);
}
}
package com.yikuanzz.databasetest;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
private MyDatabaseHelper dbHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 2);
Button createDatabase = (Button) findViewById(R.id.create_database);
createDatabase.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dbHelper.getWritableDatabase();
}
});
}
}
SQLiteDatabase db = dbHepler.getWritableDatabase();
ContentValues values = new ContentValues();
// 添加数据
values.put("name", "MyCode1");
values.put("author", "Yikuanzz");
values.put("pages", 345);
values.put("price", 9.99);
db.insert("Book", null, values);
values.put("name", "MyCode2");
values.put("author", "Yikuanzz");
values.put("pages", 243);
values.put("price", 19.99);
db.insert("Book", null, values);
// 修改数据
values.put("price", 10.99);
db.update("Book", values, "name = ?", new String[] {"MyCode1"});
// 删除数据
db.delte("Book", "pages > ?", new String[] {"300"});
// 查询数据
Cursor cursor = db.query("Book", null, null, null, null, null, null);
if (cursor.moveToFirst()){
do{
String name = cursor.getString(cursor.getColumnIndex("name"));
String author = cursor.getString(cursor.getColumnIndex("author"));
int pages = cursor.getInt(cursor.getColumnIndex("pages"));
double price = cursor.getDouble(cursor.getColumnIndex("price"));
} while(cursor.moveToNext());
}
cursor.close();
LitePal 操作数据库
LitePal 是一个开源的 Android 数据库框架,它采用对象关系映射(ORM)的模式,将常用的数据库功能进行了封装。
首先我们要添加依赖:
implementation("org.litepal.guolindev:core:3.2.3")
在 settings.gradle.kts 中引入镜像:
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.application") version "8.2.2" apply false
}
buildscript {
repositories {
maven{ url = uri("https://maven.aliyun.com/repository/google")}
maven{ url = uri("https://maven.aliyun.com/repository/gradle-plugin")}
maven{ url = uri("https://maven.aliyun.com/repository/public")}
maven{ url = uri("https://maven.aliyun.com/repository/jcenter")}
google()
jcenter()
}
dependencies {
classpath("com.android.tools.build:gradle:7.4.2")
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
然后我们配置 litepal.xml 文件:
<?xml version="1.0" encoding="utf-8"?>
<litepal>
<!--
Define the database name of your application.
By default each database name should be end with .db.
If you didn't name your database end with .db,
LitePal would plus the suffix automatically for you.
For example:
<dbname value="demo" />
-->
<dbname value="BookStore" />
<!--
Define the version of your database. Each time you want
to upgrade your database, the version tag would helps.
Modify the models you defined in the mapping tag, and just
make the version value plus one, the upgrade of database
will be processed automatically without concern.
For example:
<version value="1" />
-->
<version value="1" />
<!--
Define your models in the list with mapping tag, LitePal will
create tables for each mapping class. The supported fields
defined in models will be mapped into columns.
For example:
<list>
<mapping class="com.test.model.Reader" />
<mapping class="com.test.model.Magazine" />
</list>
-->
<list>
</list>
<!--
Define where the .db file should be. "internal" means the .db file
will be stored in the database folder of internal storage which no
one can access. "external" means the .db file will be stored in the
path to the directory on the primary external storage device where
the application can place persistent files it owns which everyone
can access. "internal" will act as default.
For example:
<storage value="external" />
-->
</litepal>
最后再配置一下 LitePalApplication 修改 AndroidManifest.xml:
<manifest>
<application
android:name="org.litepal.LitePalApplication"
...
>
...
</application>
</manifest>
我们定义一个 Java Bean :
package com.yikuanzz.litepaltest;
public class Book {
private int id;
private String author;
private double price;
private int pages;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public int getPages() {
return pages;
}
public void setPages(int pages) {
this.pages = pages;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
然后设置映射关系:
<list>
<mapping class="com.yikuanzz.litepaltest.Book"/>
</list>
这样我们已经可以完成了数据表的创建了,但是我们要进行 CRUD 操作的话,就必须让我们的模型对象继承 LitePalSupport:
public class Book extends LitePalSupport{
...
}
添加数据:
Book book = new Book();
book.setName("The old sea");
book.setAuthor("Adam");
book.setPages(666);
book.setPrice(65.34);
book.save();
Toast.makeText(MainActivity.this, "add data", Toast.LENGTH_SHORT).show();
更新数据:
Book book = new Book();
book.setPrice(9.99);
book.updateAll("name = ? and author = ?", "The old sea", "Adam");
删除数据:
LitePal.deleteAll(Book.class, "price < ?", "15");
查询数据:
List<Book> books = LitePal.findAll(Book.class);
List<Book> books = LitePal.select("name", "author").order("price desc").find(Book.class);
Android 共享
内容提供器(Content Provider)主要用于不同应用程序之间实现数据共享功能,提供了完整的机制,允许一个程序访问另一个程序中的数据,同时保证被访问数据的安全性。
运行时程序
Android 权限有两类,包括普通权限和危险权限。
普通权限不会直接威胁用户的安全和隐私,系统会自动授权。
危险权限可能会触及用户隐私,需要用户手动授权。
我们新建一个项目来学习运行时权限的使用方法。
package com.yikuanzz.runtimepermissiontest;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button make_call = (Button) findViewById(R.id.make_call);
make_call.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 判断用户是否授权
if (ContextCompat.checkSelfPermission(MainActivity.this, android.Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(MainActivity.this, new String[]{android.Manifest.permission.CALL_PHONE}, 1);
} else{
call();
}
}
});
}
private void call(){
try{
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
} catch (SecurityException e){
e.printStackTrace();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == 1){
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
call();
} else{
Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show();
}
}
}
}
然后我们需要声明权限:
uses-permission 则像是一个权限助手,帮助app去向用户请求app需要使用的权限。
uses-feature 的作用更像是一个过滤器,google play 商店会根据该标签来过滤设备。
<uses-feature
android:name="android.hardware.telephony"
android:required="true"/>
<uses-permission android:name="android.permission.CALL_PHONE"/>
访问其他程序中的数据
内容提供器的用法一般有两种,一种是使用现有的内容提供器来读取和操作相应程序中的数据,另一种是创建自己的内容提供器给我们程序的数据提供外部访问接口。
ContentResolver 用法:
通过 Context 的 getContetnResolver() 方法获取类的实例,不同于 SQLiteDatabasse,它的 CRUD 方法不接收表名参数,使用 Uri 参数替代。
内容 URI 给内容提供器中的数据建立了唯一标识符,authority 和 path,大概长这样子:content://com.example.app.provider/table1
。
然后我们将其进行解析:Uri uri = Uri.parse("content://com.example.app.provider/table1")
。
然后我们就可以用 Uri 来查询数据:
Curosr cursor = getContentResolver().query(
uri,
projection,
selection,
selectionArgs,
sortOrder
);
查询操作:
if (cursor != null){
while (cursor.moveToNext()){
String column1 = cursor.getString(cursor.getColumnIndex("column1"));
int column2 = cursor.getInt(cursor.getColumnIndex("column2"));
}
cursor.close();
}
接下来我们做一个读取联系人的项目:
<uses-permission android:name="android.permission.READ_CONTACTS"/>
package com.yikuanzz.contacttest;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Bundle;
import android.os.strictmode.ExplicitGcViolation;
import android.provider.ContactsContract;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
ArrayAdapter<String> adapter;
List<String> contactsList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListView contactsView = (ListView) findViewById(R.id.contacts_view);
adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, contactsList);
contactsView.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(){
try (Cursor cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null)) {
if (cursor != null) {
while (cursor.moveToNext()) {
@SuppressLint("Range") String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
@SuppressLint("Range") String number = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
contactsList.add(displayName + "\n" + number);
}
adapter.notifyDataSetChanged();
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == 1){
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
readContacts();
} else{
Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show();
}
}
}
}
自定义内容提供器
public class MyProvider extends ContentPovider{
...
}
Android 媒体
通知
通知一般是在程序进入到后台的时候才会要使用,可以在活动、广播接收器、服务里面创建。
NotificationManager manager = (NotificationManager)getSystemManager(Context.NOTIFICATION_SERVICE);
通过类构造器创建 Notification 对象:
Notification notification = new NotificationCompt.Builder(context)
.setContentTitle("Content Title")
.setContentText("Content Text")
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.drawable.small_icon)
.setLargetIcon(BitmapFactory.decodeResource(getResources(), R.drawable.large_icon))
.build();
然后我们调用方法让通知显示:
manager.notify(1, notification);
接下来我们做一下实例:
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
package com.yikuanzz.notificationtest;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NotificationCompat;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button sendNotice = (Button) findViewById(R.id.send_notice);
sendNotice.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
NotificationChannel notificationChannel = null;
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
notificationChannel = new NotificationChannel("001", "channel_name", NotificationManager.IMPORTANCE_HIGH);
manager.createNotificationChannel(notificationChannel);
}
Notification notification = new NotificationCompat.Builder(MainActivity.this, "001")
.setContentTitle("Content Title")
.setContentText("Content Text")
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
.build();
manager.notify(1, notification);
}
});
}
}
这个时候,我们点击通知的时候还没有响应,我们可以加上点击功能。
这里新建 NotifcationActivity 和 notification_layout:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".NotificationLayoutActivity">
<TextView
android:layout_centerInParent="true"
android:text="This is notification"
android:textSize="24sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
package com.yikuanzz.notificationtest;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NotificationCompat;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button sendNotice = (Button) findViewById(R.id.send_notice);
sendNotice.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, NotificationLayoutActivity.class);
PendingIntent pi = PendingIntent.getActivity(MainActivity.this, 0, intent, PendingIntent.FLAG_IMMUTABLE);
NotificationChannel notificationChannel = null;
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
notificationChannel = new NotificationChannel("001", "channel_name", NotificationManager.IMPORTANCE_HIGH);
manager.createNotificationChannel(notificationChannel);
}
Notification notification = new NotificationCompat.Builder(MainActivity.this, "001")
.setContentTitle("Content Title")
.setContentText("Content Text")
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
.setContentIntent(pi)
.setAutoCancel(true)
.build();
manager.notify(1, notification);
}
});
}
}
摄像头和相册
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/take_photo"
android:text="Take Photo"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<ImageView
android:id="@+id/picture"
android:layout_gravity="center_horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
我们接下就写调用摄像头的逻辑:
Android 网络
WebView 控件
如果我们需要在应用程序中展示页面,就要用到 WebView 来嵌入网页。
注意,我们加入权限声明。
<uses-permission android:name="android.permission.INTERNET"/>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
Android 服务
服务(Service)是 Android 实现程序后台服务运行的解决方案,适合不需要和用户交互而且是要求长期运行的任务。
服务是依赖于创建该服务的进程的,我们要求在服务内部创建子线程来执行任务。
多线程
class MyThread extends Thread{
@Override
public void run(){
// 处理的逻辑
}
}
线程的启动也很简单,就是:new MyThread().start();
。
一般来说,我们会选择实现 Runnable 接口的方式来定义线程:
class MyThread implements Runnable {
@Override
public void run(){
// 处理的逻辑
}
}
那我们的启动了方式就变成:
MyThread myThread = new MyThread();
new Thread(myThread).start();
当然我们还有匿名函数的启动方式:
new Thread(new Runnable(){
@Override
public void run(){
// 处理的逻辑
}
}).start();
异步消息处理
Android 中的异步消息主要由 4 个部分组成:Message、Handler、MessageQueue 和 Looper。
Message 是在线程之间传递消息,可以在内部携带少量的信息,在不同线程之间交换数据。
Handler 用于发送和处理消息,使用 sendMessage()
将数据传送到 handleMessage()
方法中。
MessageQueue 用于存放所有通过 Handler 发送的消息,每个线程中都只会有一个消息队列的对象。
Looper 是负责管理消息队列的,调用它的 loop()
方法后,就会进入无限的循环中,每当发现 MessageQueue 中存在一条消息,就会将它取出,并传递到 Handler 的 handleMessage() 方法中。
AsyncTask 对异步消息处理有更好的封装,它一般有三个参数:
- Params 执行 AsyncTask 时传入的参数,用于在后台任务中使用。
- Progress 后台任务执行时,如果要在界面上显示进度,就在这里指定泛型作为进度单位。
- Result 对结果进行返回,用这里的泛型作为返回值的类型。
除此之外,我们还需要重写几个方法:
- onPreExecute:界面初始化操作。
- doInBackground(Params) 这里的代码会在子线程中执行,所有耗时任务在这里处理。
- onProgressUpdate(Progress) 后台任务调用 publishProgress(Progress) 方法后,该方法被调用,对 UI 进行操作。
- onPostExecute(Result) 当后台任务执行完毕通过 return 语句进行返回时,这个方法很快就会被调用。
服务用法
我们新建项目后创建一个服务,它的代码是这样的:
package com.yikuanzz.servicetest;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
public class MyService extends Service {
public MyService() {
}
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}
}