Android入门笔记

324 阅读40分钟

前言

尚未接触过Android的推荐先看我的另一篇文章《Android Studio入门》 - 掘金

简要记录了入门时的知识点和遇到的问题,包含但不限于安卓的常用组件、蓝牙、Http请求、定时任务,绘制图表。

简要介绍

安卓的四大组件

应用程序组件描述
Activities活动,用于实现用户界面
Services服务,用于处理与应用程序相关的后台处理
Broadcast Receivers广播接收器,它们处理 Android 操作系统和应用程序之间的通信
Content Providers内容提供者,他们处理数据和数据库管理问题

目录

刚开始需要知道的一些目录用途,其他目录有用到了再说明

image.png

起步

Activities

活动,用于实现用户界面

每个界面需要继承Activity,例如AppCompatActivity

简要说明

以初始化生成的MainActivity.java为例

AndroidMainfest.xml中,注册、声明了这个组件。

  • android:name指定了MainActivity.java的相对路径,也可以写绝对路径com.honyee.myapplication.MainActivity
  • android:export:组件是否暴露给外部应用使用
  • <action ...MAIN" /> MAIN标识了 该Activity是程序的入口
  • <category ...LAUNCHER" /> LAUNCHER标识了 触发时机是程序启动时
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

  <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.HonyeeApplication"
        tools:targetApi="31">
        <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>
    </application>

</manifest>

MainActivity.java中,

  • EdgeToEdge.enable(this):布局策略,尽可能到边缘
  • setContentView(R.layout.activity_main):关联页面布局activity_main.xml
  • R是安卓中一个自动生成的类,充当应用资源(Resources)的索引。安卓应用中的资源包括布局文件(Layouts)、字符串(Strings)、图像(Drawables)、尺寸(Dimensions)等多种类型,R类提供了一种方便的方式来访问这些资源。
public class MainActivity extends AppCompatActivity {

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

        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            // 设置padding,如果主题是有AppBar则可以不设置这个
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });

    }

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

activity_main.xml

  • android:id指定了组件的ID,类型为@+id,值为main

    在代码中可以这样获取这个id:R.id.main

  • match_parent尺寸与父容器相同

  • tools:context=".MainActivity"指定Activity类的相对路径,可改为绝对路径

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

</androidx.constraintlayout.widget.ConstraintLayout>

为Activity添加一个按钮

activity_main.xml中添加按钮

<Button
    android:id="@+id/button"
    android:layout_width="121dp"
    android:layout_height="wrap_content"
    android:text="我是按钮"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    tools:ignore="HardcodedText" />

MainActivity.java中,为按钮添加点击监听

    Button bt = findViewById(R.id.myBtn);
    bt.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Toast.makeText(v.getContext(), "点击了一下", Toast.LENGTH_LONG).show();
        }
    });

image.png

Service

服务是一个后台运行的组件,执行长时间运行且不需要用户交互的任务。即使应用被销毁也依然可以工作。默认情况下,Service 会在应用的主线程(也叫 UI 线程)中运行。这意味着若在 Service 里执行耗时操作,会阻塞主线程,进而引发界面卡顿,甚至出现 ANR(Application Not Responding,应用无响应)错误。

服务基本上包含两种状态

状态描述
StartedAndroid的应用程序组件,如活动,通过startService()启动了服务,则服务是Started状态。一旦启动,服务可以在后台无限期运行,即使启动它的组件已经被销毁。
Bound当Android的应用程序组件通过bindService()绑定了服务,则服务是Bound状态。Bound状态的服务提供了一个客户服务器接口来允许组件与服务进行交互,如发送请求,获取结果,甚至通过IPC来进行跨进程通信。
  • 多次调用startService

    • 只有一个实例:无论调用startService多少次,只要没有调用stopService或者Service自身没有通过stopSelf方法停止,系统通常只会维持一个Service实例。

    • onStartCommand:每次调用startService都会导致ServiceonStartCommand方法被调用。这意味着Service可以在onStartCommand方法中处理每次启动请求带来的任务。

  • Service销毁时机

    • 手动stopService()或者stopSelf()
    • 没有startForeground()设置为前台服务,且没有其他组件startService()或者bindService()

Started

通过startService()启动服务

编写服务

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;

public class MyService extends Service {
 @Override
    public void onCreate() {
        super.onCreate();
        Log.d("MyService", "服务已创建");
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d("MyService", "服务已启动,开始执行任务");
        // 在这里可以添加具体要执行的业务逻辑,比如进行一些数据更新、后台数据获取、文件处理等操作
        // 例如简单地打印一条消息来表示执行了任务
        Log.d("MyService", "执行定时任务相关的操作,这里只是示例");

        // 根据需求决定返回合适的启动命令类型,以下是几种常见情况:
        // START_STICKY:如果服务被异常终止,系统会尝试重新创建服务,并且调用onStartCommand方法时intent为null
        // START_NOT_STICKY:如果服务被异常终止,系统不会自动重新创建服务
        // START_REDELIVER_INTENT:如果服务被异常终止,系统会重新创建服务并重新传递之前的intent
        return START_STICKY;
    }

    @Override
    public IBinder onBind(Intent intent) {
        // 如果服务不需要支持绑定(即不需要与组件进行通信、返回IBinder对象等情况),可以直接返回null
        return null;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d("MyService", "服务已销毁");
    }
}
   

注册服务

AndroidManifest.xmlapplication中注册Service

 <service android:name=".service.MyService"
            android:exported="false"
            />

启动服务

Intent myServiceIntent = new Intent(this, MyService.class);
startService(myServiceIntent);

Bound

通过bindService()绑定服务

编写服务

import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;

public class MyService extends Service {
    private final IBinder myBinder = new MyBinder();

    public class MyBinder extends Binder {
        public MyService getService() {
            return MyService.this;
        }
    }

    public void hello() {
        System.out.println("打印测试");
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d("MyService", "服务已创建");
    }

    @Override
    public IBinder onBind(Intent intent) {
        // 返回Binder对象,客户端通过这个Binder与服务进行交互
        return myBinder;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d("MyService", "服务已销毁");
    }
}

注册服务

AndroidManifest.xmlapplication中注册Service

 <service android:name=".service.MyService"
            android:exported="false"
            />

绑定服务

Intent myServiceIntent = new Intent(this, MyService.class);

ServiceConnection serviceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        // 获取Service
        MyService.MyBinder myBinder = (MyService.MyBinder) service;
        MyService myService = myBinder.getService();
        myService.hello();
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {

    }
};

bindService(myServiceIntent, serviceConnection, Context.BIND_AUTO_CREATE);

IntentService

除了手动创建子线程,Android 还提供了 IntentService 类,它是 Service 的子类,专门用于处理异步请求。IntentService 会在内部创建一个工作线程,将所有请求都放到这个工作线程中处理,处理完一个请求后会自动停止服务。

import android.app.IntentService;
import android.content.Intent;
import android.util.Log;

public class MyIntentService extends IntentService {
    private static final String TAG = "MyIntentService";

    public MyIntentService() {
        super("MyIntentService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        if (intent != null) {
            String action = intent.getAction();
            if ("ACTION_DO_WORK".equals(action)) {
                // 获取传递的数据
                String inputText = intent.getStringExtra("input_text");
                Log.d(TAG, "接收到的数据: " + inputText);
                try {
                    Log.d(TAG, "开始执行任务");
                    Thread.sleep(5000);
                    Log.d(TAG, "任务执行完成");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

AndroidManifest.xml 中注册 IntentService

<service android:name=".MyIntentService" />

启动 IntentService

import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button startServiceButton = findViewById(R.id.start_service_button);
        startServiceButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 创建 Intent 对象
                Intent intent = new Intent(MainActivity.this, MyIntentService.class);
                // 设置 action
                intent.setAction("ACTION_DO_WORK");
                // 传递数据
                intent.putExtra("input_text", inputText);
                // 启动 IntentService
                startService(intent);
            }
        });
    }
}

IntentService 会在处理完所有请求后自动停止服务,通常不需要手动停止。

如果需要提前停止服务,可以调用 stopService 方法

Intent intent = new Intent(MainActivity.this, MyIntentService.class);
stopService(intent);

Broadcast Receivers

广播接收器,它们处理 Android 操作系统和应用程序之间的通信

广播接收器的注册方式,静态注册、动态注册

先创建一个广播接收器

MyBroadcastReceiver.java

package com.honyee.myapplication;

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) {
        Toast.makeText(context, "收到广播消息", Toast.LENGTH_LONG).show();
    }
}

静态注册

AndroidManifest.xml

<application>
    <!-- 省略其他内容 -->
    <receiver
        android:name=".MyBroadcastReceiver"
        android:enabled="true"
        android:exported="false">
        <intent-filter>
            <action android:name="honyee.notify.test" />
        </intent-filter>
    </receiver>
</application>

发送广播消息

Button bt = findViewById(R.id.myBtn);
bt.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // 指定action
        Intent intent = new Intent("honyee.notify.test");
        // 添加自定义参数
        intent.putExtra("a", "A");
        // 发送广播消息
        sendBroadcast(intent);
    }
});

正常来说,此时点击按钮应该要收到广播消息,但实际却没有收到,甚至组件都没注册成功(从看debug 断点看)

image.png

谷歌的官方文档解释

Broadcasts overview | Android Developers

Android 8.0

Beginning with Android 8.0 (API level 26), the system imposes additional restrictions on manifest-declared receivers.

If your app targets Android 8.0 or higher, you cannot use the manifest to declare a receiver for most implicit broadcasts (broadcasts that don't target your app specifically). You can still use a context-registered receiver when the user is actively using your app.

意思是,从Android 8.0(API级别26)开始,该系统对声明清单的接收者施加了额外的限制。你不能使用清单为大多数隐式广播,需要指定应用包名。

那么需要改动如下

import android.content.ComponentName;

Button bt = findViewById(R.id.myBtn);
bt.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent("honyee.notify.test");
        intent.putExtra("a", "A");
        // 方式1:指定包名
        intent.setPackage(MyBroadcastReceiver.class.getPackage().getName());
        // 方式2:指定包名和应用名
        //intent.setComponent(
        //        new ComponentName(MyBroadcastReceiver.class.getPackage().getName(),
        //        MyBroadcastReceiver.class.getName())
        //);
        sendBroadcast(intent);
    }
});

image.png

动态注册

动态注册就是在代码中通过registerReceiver注册,此时发送消息就不需要指定包名

package com.honyee.myapplication;

import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

public class MainActivity extends AppCompatActivity {

    MyBroadcastReceiver myBroadcastReceiver;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // ...省略

        Button bt = findViewById(R.id.myBtn);
        bt.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                // 发送消息
                Intent intent = new Intent("honyee.notify.test");
                intent.putExtra("a", "A");
                sendBroadcast(intent);

            }
        });

        // 动态注册
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction("honyee.notify.test");
        myBroadcastReceiver = new MyBroadcastReceiver();
        registerReceiver(myBroadcastReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 记得销毁
        unregisterReceiver(myBroadcastReceiver);
    }
}

Message

Message 类是一个重要的组件,主要用于线程间通信。它常与 Handler、Looper 和 MessageQueue 协同工作,以此实现不同线程之间的数据传递和任务调度。

  • Message:这是一个用于携带数据和执行操作的对象,它能够包含各种类型的数据,像 what(消息标识)、arg1arg2(简单整型数据)以及 obj(任意对象)等。
  • Handler:负责发送和处理 Message。你可以借助 Handler 的 sendMessage() 方法发送消息,利用 handleMessage() 方法处理接收到的消息。
  • Looper:为每个线程创建一个消息循环,负责从 MessageQueue 中取出消息并分发给对应的 Handler 进行处理。
  • MessageQueue:消息队列,用于存储待处理的 Message

使用示例

创建Handler

import android.os.Handler;
import android.os.Message;
import android.util.Log;

public class MyHandler extends Handler {
    private static final String TAG = "MyHandler";

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case 1:
                // 处理消息类型为 1 的情况
                int arg1 = msg.arg1;
                int arg2 = msg.arg2;
                Object obj = msg.obj;
                Log.d(TAG, "Received message with arg1: " + arg1 + ", arg2: " + arg2 + ", obj: " + obj);
                break;
            default:
                super.handleMessage(msg);
        }
    }
}

创建并发送Message

方式1:Message.obtain()

MyHandler myHandler = new MyHandler();

// 创建一个 Message 对象
Message message = Message.obtain();
message.what = 1;
message.arg1 = 10;
message.arg2 = 20;
message.obj = "Hello, Message!";

// 发送消息
myHandler.sendMessage(message);

方式2: HandlerobtainMessage()

MyHandler myHandler = new MyHandler();

// 创建并发送消息
Message message = myHandler.obtainMessage(1, 10, 20, "Hello, Message!");
myHandler.sendMessage(message);

方式3:延迟发送消息

MyHandler myHandler = new MyHandler();

// 创建并延迟 2 秒发送消息
Message message = myHandler.obtainMessage(1, 10, 20, "Hello, Message!");
myHandler.sendMessageDelayed(message, 2000);

注意事项

避免内存泄漏: 若 Handler 是一个内部类,要使用 static 修饰,并且持有一个对外部类的弱引用,防止因 Handler 持有外部类的强引用而导致外部类无法被垃圾回收。

消息复用: 使用 Message.obtain() 或者 Handler.obtainMessage() 方法创建 Message,这样可以复用 Message 对象,减少内存开销。

Content Providers

内容提供者,他们处理数据和数据库管理问题

Intent

Intent 是安卓开发中一种重要的消息传递机制,用于在不同的安卓组件(如 Activity、Service、Broadcast Receiver)之间进行通信和导航。

  • Action:用于描述想要执行的操作类型。安卓系统定义了许多标准动作,如Intent.ACTION_VIEW(用于查看内容,通常用于打开一个网页或者查看一个文件)
  • Data:与动作相关联的数据,用于指定操作的对象。数据通常是一个Uri对象,用于表示资源的位置。例如,在使用Intent.ACTION_VIEW动作时,数据可以是一个网页的网址(如Uri.parse("https://www.example.com"))或者一个文件的路径(如Uri.fromFile(new File("path/to/file")))
  • Category:用于进一步描述 Intent 的特征
  • Extras:传递自定义的额外参数

示例

  • 启动 Activity:

    • 显式启动:当知道要启动的 Activity或者Service的具体类名时,可以使用显式 Intent 来启动。例如,从MainActivity启动SecondActivity
    // 参数:上下文、要启动的类
    Intent intent = new Intent(MainActivity.this, SecondActivity.class);
    // 启动
    startActivity(intent);
    
    • 隐式启动:如果不知道具体的 Activity 类名,而是通过动作和数据来匹配目标 Activity,可以使用隐式 Intent。例如,想要打开一个网页
    Intent intent = new Intent(Intent.ACTION_VIEW);
    Uri uri = Uri.parse("https://www.example.com");
    intent.setData(uri);
    startActivity(intent);
    

Context

Context是一个抽象类,提供了关于应用环境全局信息的接口。可以将Context理解为应用程序的上下文,它包含了应用运行时的各种资源、服务访问权限、组件信息等诸多内容

获取方式:

  • ActivityService 本身就是一个Context
  • Broadcast ReceiveronReceive(Context,Intent)

LiveData

LiveData 官方文档

LiveDataBroadcast Receivers类似,是一个传播消息的组件,而LiveData是一个观察者模式的组件。

举个例子

新建一个AgeViewModel.java,定义了可监听的数据ageLiveData,通过setValue来触发事件

package com.honyee.myapplication;

import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;

public class AgeViewModel extends ViewModel {
    private final MutableLiveData<Integer> ageLiveData = new MutableLiveData<>();

    public MutableLiveData<Integer> getAgeLiveData() {
        return ageLiveData;
    }

    public void updateAge(Integer age) {
        ageLiveData.setValue(age);
    }
}

MainActivity.java中注册观察者,并在按钮点击事件中修改数据

// 获取
AgeViewModel ageViewModel = new ViewModelProvider(this).get(AgeViewModel.class);

// 注册监听
ageViewModel.getAgeLiveData().observe(this, age -> {
    Log.d("viewModel", "age=" + age);
});

Button bt = findViewById(R.id.myBtn);
bt.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // 触发事件
        ageViewModel.updateAge(18);
    }
});

获取布局中的组件

视图组件-官方

获取方式有两种,一种是 findViewById,一种是视图组件

  • findViewById

findViewById 可以直接根据 R.id.xxx来直接获取当前Activity绑定的视图里的组件

  • 视图组件

视图组件是一个根据xml名称自动生成的一个类,例如 activity_main.xml 会生成一个ActivityMainBinding,但是直接查找类是找不到的,在idea中跳转该文件又会直接定位到activity_main.xml

需要在build.gradleandroid项中添加配置来开启视图组件

android {
   buildFeatures {
        viewBinding true
   }
}
import com.honyee.myapplication.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 获取视图组件
        ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
        // 需要getRoot,否则给组件设置事件不生效
        setContentView(binding.getRoot());
        // 根据id获取组件
        Button myBtn = binding.myBtn;
    }
}

布局

ConstraintLayout

约束布局(相对定位)

<androidx.constraintlayout.widget.ConstraintLayout 
    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:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    
    <!--  参考线,设定为垂直线,居中  -->
    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/vertical_guideline"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:orientation="vertical"
        app:layout_constraintGuide_percent="0.5" />
    
    
</androidx.constraintlayout.widget.ConstraintLayout>

每个组件基本要素:

  • 宽度、高度
  • 两个方位确定坐标

layout的参数命名规律:

  • layout_constraintTop_toTopOf自身的Top指向目标的Top
  • layout_constraintTop_toBottomOf自身的Top指向目标的Bottom
  • layout_constraintStart_toStartOf自身的Start指向目标的Start
  • layout_constraintEnd_toEndOf自身的End指向目标的End
  • 省略其他

同时指定Start和End的时候,组件将居中,Top和Bottom同理。

LinearLayout

线性布局

可以指定布局方向:

  • 水平方向 android:orientation="horizontal"
  • 垂直方向 android:orientation="vertical"
<?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">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="100dp"
            android:text="第1个" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="100dp"
            android:text="第2个" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="100dp"
            android:text="第3个" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="100dp"
            android:text="第4个" />
    </LinearLayout>

</LinearLayout>

9cdd7f9f851ef107f0009a3acf9400e.jpg

TableLayout

表格布局

TableLayout属性:

stretchColumns: 指定哪一列自动拉伸占满剩余空位,从0开始,如果用*表示所有列均分

<?xml version="1.0" encoding="utf-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/base_table_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:showDividers="beginning|middle|end"
    android:stretchColumns="*">

    <TableRow>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="第1个" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="第2个" />
    </TableRow>

    <TableRow>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="第3个" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="第4个" />
    </TableRow>

</TableLayout>

TableRow属性:

layout_weight:设置列宽权重

<?xml version="1.0" encoding="utf-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/base_table_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:showDividers="beginning|middle|end">

    <TableRow>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:layout_weight="1"
            android:text="第1个" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:layout_weight="1"
            android:text="第2个" />
    </TableRow>

    <TableRow>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:layout_weight="1"
            android:text="第3个" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:layout_weight="1"
            android:text="第4个" />
    </TableRow>

</TableLayout>

其他常用组件

TabLayout

Tab组件 com.google.android.material.tabs.TabLayout

配合TabItem使用 com.google.android.material.tabs.TabItem

一般配合其他布局一起使用,并添加TabItem选中监听,根据position判定选中的TabItem

<?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">

    <com.google.android.material.tabs.TabLayout xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/google_tab_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:tabMode="scrollable">

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="第1个" />

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="第2个" />

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="第3个" />

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="第4个" />
    </com.google.android.material.tabs.TabLayout>

</LinearLayout>

添加事件

binding.googleTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
        @Override
        public void onTabSelected(TabLayout.Tab tab) {
            System.out.println("选中" + tab.getPosition());
        }

        @Override
        public void onTabUnselected(TabLayout.Tab tab) {

        }

        @Override
        public void onTabReselected(TabLayout.Tab tab) {

        }
    });

TextInputLayout

表单组件

可编辑文本框

<?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"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <com.google.android.material.textfield.TextInputLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="姓名"
        app:boxBackgroundColor="@color/white"
        app:endIconMode="clear_text">

        <com.google.android.material.textfield.TextInputEditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:inputType="text" />
    </com.google.android.material.textfield.TextInputLayout>

    <com.google.android.material.textfield.TextInputLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="年龄"
        app:boxBackgroundColor="@color/white"
        app:endIconMode="clear_text">

        <com.google.android.material.textfield.TextInputEditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:inputType="number" />
    </com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

下拉选项

<?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"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <com.google.android.material.textfield.TextInputLayout
        style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense.ExposedDropdownMenu"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="选项"
        android:labelFor="@+id/view_task_type">

        <AutoCompleteTextView
            android:id="@+id/view_task_type"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:inputType="none" />
    </com.google.android.material.textfield.TextInputLayout>

</LinearLayout>
String[] items = new String[100];
for (int i = 0; i < items.length; i++) {
    items[i] = "第" + i + "项";
}
        
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, items);
binding.viewTaskType.setAdapter(adapter);

// 重要:先禁用过滤,再设置初始值
binding.viewTaskType.setThreshold(Integer.MAX_VALUE);
binding.viewTaskType.setText(items[0]);

Spinner

列表选项框

相关属性

  • android:dropDownHorizontalOffset:设置列表框的水平偏移距离
  • android:dropDownVerticalOffset:设置列表框的水平竖直距离
  • android:dropDownSelector:列表框被选中时的背景
  • android:dropDownWidth:设置下拉列表框的宽度
  • android:gravity:设置里面组件的对其方式
  • android:popupBackground:设置列表框的背景
  • android:prompt:设置对话框模式的列表框的提示信息(标题),只能够引用string.xml 中的资源id,而不能直接写字符串
  • android:spinnerMode:列表框的模式,有两个可选值:
    • dropdown:下拉菜单风格的窗口(默认)
    • dialog:对话框风格的窗口
  • 可选属性:android:entries:使用数组资源设置下拉列表框的列表项目

基础使用

string.xml

<resources>
    <string name="app_name">Spinner</string>
    <string-array name="province">
        <item>安徽省</item>
        <item>河北省</item>
        <item>河南省</item>
        <item>湖北省</item>
        <item>湖南省</item>
    </string-array>
</resources>

fragment

<Spinner
    android:entries="@array/province"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/spinner"
    android:layout_centerInParent="true"
    android:layout_marginTop="64dp"/>

自定义选项样式

参考

2.5.3 Spinner(列表选项框)的基本使用 | 菜鸟教程

Android中Spinner的使用及其详细总结(可实现下拉列表)_android spinner用法-CSDN博客

ListView

列表视图

<?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">

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

添加适配器,使用默认布局

String[] items = new String[100];
for (int i = 0; i < items.length; i++) {
    items[i] = "第" + i + "项";
}
ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, items);
binding.listView.setAdapter(adapter);

或者自定义布局和复杂项

<!-- 自定义项的布局文件 layout目录下 `my_list_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="horizontal">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="5dp"
        android:layout_marginBottom="5dp"
        android:text="选项:"/>

    <TextView
        android:id="@+id/my_list_item"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:layout_marginTop="5dp"
        android:layout_marginBottom="5dp" />
</LinearLayout>
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.util.List;

/**
 * 自定义适配器
 */
public class MyArrayAdapter extends ArrayAdapter<MyArrayAdapter.Item> {
    static class Item {
        String name;
        Integer code;
        // ... 省略其他
    }

    public MyArrayAdapter(@NonNull Context context, int resource, @NonNull List<MyArrayAdapter.Item> list) {
        super(context, resource, list);
    }

    @NonNull
    @Override
    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
        MyArrayAdapter.Item item = getItem(position);
        //为每一个子项加载设定的布局
        View view = LayoutInflater.from(getContext()).inflate(R.layout.my_list_item, parent, false);
        TextView createTime = view.findViewById(R.id.my_list_item);
        createTime.setText(item.name);
        return view;
    }
}

ScrollView

滚动视图

<?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">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        >
        <LinearLayout
            android:id="@+id/content"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">
        </LinearLayout>
    </ScrollView>
</LinearLayout>
for (int i = 0; i < 100; i++) {
    TextView textView = new TextView(this);
    textView.setGravity(Gravity.CENTER);
    ViewGroup.LayoutParams layoutParams = new TableLayout.LayoutParams();
    layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
    textView.setLayoutParams(layoutParams);
    textView.setText("第" + i + "项");
    binding.content.addView(textView);
}

基本组件

<Button
    android:layout_width="100dp"
    android:layout_height="40dp"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintStart_toStartOf="parent" />

image.png

可以给组件指定ID

<Button
    android:id="@+id/testBtn"
    android:text="测试"
    android:layout_width="100dp"
    android:layout_height="40dp"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintStart_toStartOf="parent" />

image.png

同时指定Start和End为parent,组件将水平居中,同理,同时指定Top和Bottom将垂直居中

<Button
    android:id="@+id/testBtn"
    android:text="测试"
    android:layout_width="100dp"
    android:layout_height="40dp"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent" />

image.png

通过位置,动态设置组件的宽高。此时需要将宽高设置为0dp

<Button
    android:id="@+id/testBtn"
    android:text="测试"
    android:layout_width="0dp"
    android:layout_height="40dp"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent" />

image.png

通过参考线协助定位(居中效果),此时使用Left 和 Right 之类定位

<Button
    android:id="@+id/testBtn"
    android:text="测试"
    android:layout_width="0dp"
    android:layout_height="40dp"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toLeftOf="@+id/vertical_guideline" />

image.png

例如两个按钮平分一行

<Button
    android:id="@+id/leftBtn"
    android:text="左边"
    android:layout_width="0dp"
    android:layout_height="40dp"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toLeftOf="@+id/vertical_guideline" />

<!-- 也可以是 app:layout_constraintLeft_toRightOf="@+id/vertical_guideline" -->
<Button
    android:id="@+id/rightBtn"
    android:text="右边"
    android:layout_width="0dp"
    android:layout_height="40dp"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintLeft_toRightOf="@id/leftBtn"
    app:layout_constraintRight_toRightOf="parent" />

image.png

Fragment

Fragment 表示应用界面中可重复使用的一部分。Fragment 定义和管理自己的布局,具有自己的生命周期,并且可以处理自己的输入事件。Fragment 不能独立存在,而是必须由 Activity 或另一个 Fragment 托管。Fragment 的视图层次结构会成为宿主的视图层次结构的一部分,或附加到宿主的视图层次结构。

相当于前端的组件Component

生命周期

新旧版本的生命周期有差异,这个是新版的生命周期

image.png

创建Fragment

image.png

或者手动创建,和Activity类似,一个class和一个xml

res/layout下新建一个fragment_home.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">


</androidx.constraintlayout.widget.ConstraintLayout>

新建一个HomeFragment.java

package com.honyee.fragment;
import androidx.fragment.app.Fragment;

public class HomeFragment extends Fragment {
    
}

使用Fragment

在需要用到的地方加入

<fragment
    android:id="@+id/fragment_home"
    android:name="com.honyee.fragment.HomeFragment"
    android:label="首页"/>

我常用的数据处理位置

@Override
public void onHiddenChanged(boolean hidden) {
    super.onHiddenChanged(hidden);
    if (hidden) {
        // 隐藏时,还原数据
        return;
    }
    // 显示时,初始化数据
}

FragmentManager

FragmentManager 类负责在应用的 fragment 上执行一些操作,如添加、移除或替换操作,以及将操作添加到返回堆栈。

通过getSupportFragmentManager()访问FragmentManager 通过getChildFragmentManager()获取子级

FragmentManager

BottomNavigationView + App Bar

底部导航栏 + 顶部应用栏,目前找到两种使用方式,一种是交给组件管理页面的切换,一种是手动管理页面的切换

交给组件管理页面切换

组件管理的Fragment会在切换时销毁当前Fragment的View,然后创建目标Fragment的View,触发下面代码展示的事件。所以页面会被重新渲染,之前渲染的数据会丢失。好处是顶部标题栏可以跟着菜单联动

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;

public class HomeFragment extends Fragment {

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        System.out.println("创建视图");
        return super.onCreateView(inflater, container, savedInstanceState);
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        System.out.println("销毁视图");
    }

    @Override
    public void onStart() {
        super.onStart();
        System.out.println("可见");
    }

    @Override
    public void onResume() {
        super.onResume();
        System.out.println("可见且可交互");
    }

    @Override
    public void onStop() {
        super.onStop();
        System.out.println("不可见");
    }
}

示例

可以通过Android Studio创建一个带有导航栏的项目

image.png

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingTop="?attr/actionBarSize">

    <!--  app:navGraph包含了对应的Fragment页面  -->
    <fragment
        android:id="@+id/nav_host_fragment_activity_main"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toTopOf="@id/nav_view"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/mobile_navigation" />
    
    <!--  app:menu包含了导航栏的菜单项  -->
    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/nav_view"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="0dp"
        android:layout_marginEnd="0dp"
        android:background="?android:attr/windowBackground"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:menu="@menu/bottom_nav_menu" />

</androidx.constraintlayout.widget.ConstraintLayout>

页面容器

<?xml version="1.0" encoding="utf-8"?>
<navigation 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:id="@+id/mobile_navigation"
    app:startDestination="@+id/navigation_home">

    <fragment
        android:id="@+id/navigation_home"
        android:name="com.honyee.myapplication2.ui.home.HomeFragment"
        android:label="@string/title_home" />

    <!-- 省略其他fragment -->
</navigation>

菜单项

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/navigation_home"
        android:icon="@drawable/ic_home_black_24dp"
        android:title="@string/title_home" />
   <!-- 省略其他item -->

</menu>

主题

主题需要是带有标题栏的,不然会报错。例如Android Studio创建的模板内是这样的

AndroidManifest.xml中可配置选用哪种主题(android:theme

image.png

MainActivity

将页面容器和菜单项进行关联

import android.os.Bundle;

import com.google.android.material.bottomnavigation.BottomNavigationView;

import androidx.appcompat.app.AppCompatActivity;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;

import com.honyee.myapplication2.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {

    private ActivityMainBinding binding;

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

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        BottomNavigationView navView = findViewById(R.id.nav_view);
        AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(
                R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications)
                .build();
        NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_activity_main);
        NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
        NavigationUI.setupWithNavController(binding.navView, navController);
    }

}

手动管理页面切换

手动管理页面的显示、隐藏,好处是页面不会重新渲染,问题是顶部标题栏不会跟着菜单联动,目前没找到解决方案

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingTop="?attr/actionBarSize">

    <!--  App Bar  -->
    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        app:title="@string/app_name"
        app:titleTextColor="@color/white"
        tools:ignore="MissingConstraints" />

    <!--  不需要app:navGraph  -->
    <fragment
        android:id="@+id/nav_host_fragment_activity_main"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toTopOf="@id/nav_view"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/toolbar" />

    <!--  app:menu包含了导航栏的菜单项  -->
    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/nav_view"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="0dp"
        android:layout_marginEnd="0dp"
        android:background="?android:attr/windowBackground"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:menu="@menu/bottom_nav_menu" />
</androidx.constraintlayout.widget.ConstraintLayout>

菜单项

菜单项一样

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/navigation_home"
        android:icon="@drawable/ic_home_black_24dp"
        android:title="@string/title_home" />
   <!-- 省略其他item -->

</menu>

MainActivity

public class MainActivity extends AppCompatActivity {

    private ActivityMainBinding binding;

    Map<Integer, Fragment> fragmentMap;

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

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        BottomNavigationView navView = findViewById(R.id.nav_view);

        Fragment homeFragment = new HomeFragment();
        Fragment dashboardFragment = new DashboardFragment();
        Fragment notificationsFragment = new NotificationsFragment();
        fragmentMap = new HashMap<>();
        fragmentMap.put(R.id.navigation_home, homeFragment);
        fragmentMap.put(R.id.navigation_dashboard, dashboardFragment);
        fragmentMap.put(R.id.navigation_notifications, notificationsFragment);

        // 动态加入Fragment,并默认隐藏
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        for (Fragment fragment : fragmentMap.values()) {
            transaction.add(R.id.nav_host_fragment_activity_main, fragment, fragment.getTag());
            transaction.hide(fragment);
        }
        // 只展示首页
        transaction.show(homeFragment);
        transaction.commitAllowingStateLoss();
        // 初始化App Bar
        setSupportActionBar(binding.toolbar);
        binding.toolbar.setTitle(realTimeDataFragment.getTitle());
        
        // 菜单项变动监听:展示点击菜单项对应的Fragment
        binding.navView.setOnItemSelectedListener(new NavigationBarView.OnItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                int itemId = item.getItemId();
                FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
                for (Fragment fragment : fragmentMap.values()) {
                    transaction.hide(fragment);
                }
                Fragment fragment = fragmentMap.get(itemId);
                if (fragment != null) {
                    transaction.show(fragment);
                    transaction.commitAllowingStateLoss();
                    // 修改App Bar
                    binding.toolbar.setTitle(fragment.getTitle());
                }
                return fragment != null;
            }
        });
    }
}

主题

将主题设置为没用App Bar(顶部应用栏)

Theme.MaterialComponents.DayNight.NoActionBar

否则会出现

This Activity already has an action bar supplied by the window decor. Do not request Window.FEATURE_SUPPORT_ACTION_BAR and set windowActionBar to false in your theme to use a Toolbar instead.

简要增强Fragment


import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;

import com.honyee.gas_leak.MainActivity;

import org.greenrobot.eventbus.EventBus;

public abstract class BaseFragment extends Fragment {
    public int getCustomId() {
        return 0;
    }

    public abstract String getTitle();

    public BaseFragment self() {
        return this;
    }

    /**
     * 显示指定Fragment
     *
     * @param targetId
     */
    public void showFragment(int targetId) {
        FragmentActivity fragmentActivity = requireActivity();
        if (fragmentActivity instanceof MainActivity) {
            MainActivity mainActivity = (MainActivity) fragmentActivity;

            BaseFragment targetFragment = mainActivity.getFragment(targetId);
            FragmentManager supportFragmentManager = requireActivity().getSupportFragmentManager();
            FragmentTransaction transaction = supportFragmentManager.beginTransaction();
            transaction.hide(self());
            transaction.show(targetFragment);
            transaction.addToBackStack(null);
            transaction.commit();
        }
    }

    /**
     * 回到上一个页面
     */
    public void popFragment() {
        FragmentManager supportFragmentManager = requireActivity().getSupportFragmentManager();
        supportFragmentManager.popBackStack();
    }

    /**
     * EventBus发送消息
     * @param obj
     */
    public void busPost(Object obj) {
        EventBus.getDefault().post(obj);
    }

}

Dialog

ProgressDialog

等待遮罩,目前已经@Deprecated,但还是能使用,封装成工具类。

import android.app.ProgressDialog;
import android.content.Context;

/**
 * Loading 工具
 */
public class ProgressDialogUtils {
    private static ProgressDialog progressDialog;

    public static void showProgressDialog(Context context, String message) {
        if (progressDialog == null) {
            progressDialog = new ProgressDialog(context);
        }
        progressDialog.setMessage(message);
        progressDialog.setCancelable(false);
        if (!progressDialog.isShowing()) {
            progressDialog.show();
        }
    }

    public static void hideProgressDialog() {
        if (progressDialog != null && progressDialog.isShowing()) {
            progressDialog.dismiss();
        }
    }

}

AlertDialog

ProgressDialog的替代品,需要自行封装【进度条 + 提示语】

  1. drawable中新建一个旋转图标

image.png

image.png

  1. 新建Dialog的布局文件dialog_loading.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/primary.black.opacity">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/loading_container"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:alpha="1"
        android:background="@color/white"
        android:layout_marginLeft="50dp"
        android:layout_marginRight="50dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <androidx.appcompat.widget.AppCompatImageView
            android:id="@+id/loading_img"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:src="@drawable/baseline_autorenew_24"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/loading_message"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="10dp"
            app:layout_constraintTop_toBottomOf="@id/loading_img"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>


</androidx.constraintlayout.widget.ConstraintLayout>
  1. 新建Dialog对应的类
import android.app.Dialog;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.view.Gravity;
import android.view.Window;
import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.LinearInterpolator;
import android.view.animation.RotateAnimation;
import android.widget.ImageView;
import android.widget.TextView;

import com.honyee.gas_leak.R;

public class LoadingDialog extends Dialog {

    public LoadingDialog(Context context) {
        super(context);
        init(context);
    }

    private void init(Context context) {
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.dialog_loading);
        Window window = getWindow();
        if (window != null) {
            // 设置背景透明
            window.setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT));
            // 设置全屏
            window.setLayout(
                    WindowManager.LayoutParams.MATCH_PARENT,
                    WindowManager.LayoutParams.MATCH_PARENT
            );
            window.setGravity(Gravity.CENTER);
        }


        // 添加旋转的loading图片
        ImageView loadingImage = findViewById(R.id.loading_img);


        // 添加旋转动画
        RotateAnimation rotateAnimation = new RotateAnimation(
                0f, 360f,
                Animation.RELATIVE_TO_SELF, 0.5f,
                Animation.RELATIVE_TO_SELF, 0.5f
        );
        // 设置动画持续时间
        rotateAnimation.setDuration(2000);
        // 设置转动速率,这里设置的是匀速转动(线性)
        rotateAnimation.setInterpolator(new LinearInterpolator());
        // 设置动画的循环模式为循环
        rotateAnimation.setRepeatMode(Animation.RESTART);
        // 设置动画的循环模式为循环
        rotateAnimation.setRepeatCount(Animation.INFINITE);
        // 启动动画
        loadingImage.startAnimation(rotateAnimation);

    public void setMessage(String message) {
        TextView loadingMessage = findViewById(R.id.loading_message);
        loadingMessage.setText(message);
    }

    @Override
    public void cancel() {

    }
}
  1. 封装工具类
/**
 * Loading 工具
 */
public class ProgressBarUtils {

    static LoadingDialog loadingDialog;

    public static void show(Context context, String title, String message) {
        if (loadingDialog == null) {
            loadingDialog = new LoadingDialog(context);
        }
        loadingDialog.setMessage(message);
        loadingDialog.show();
    }

    public static void hide() {
        loadingDialog.hide();
    }

}

虚拟键盘

控制虚拟键盘的显示和隐藏,封装成工具类。

import android.content.Context;
import android.view.View;
import android.view.inputmethod.InputMethodManager;

public class KeyboardUtil {
    public static void showKeyboard(View view) {
        InputMethodManager imm = (InputMethodManager) view.getContext()
                .getSystemService(Context.INPUT_METHOD_SERVICE);
        if (imm != null) {
            view.requestFocus();
            imm.showSoftInput(view, 0);
        }
    }

    public static void hideKeyboard(View view) {
        InputMethodManager imm = (InputMethodManager) view.getContext()
                .getSystemService(Context.INPUT_METHOD_SERVICE);
        if (imm != null) {
            imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
        }
    }

}

例如在某个组件失去焦点时,隐藏虚拟键盘

binding.autoCompleteTextView.setOnFocusChangeListener((v, hasFocus) -> {
    if (hasFocus) {
        KeyboardUtil.hideKeyboard(v);
    }
});

Event Bus

GitHub-Event Bus

Broadcast Receivers并不好用,现在推荐使用Event Bus

与Message比较

Message主要用于线程间通信,特别是在子线程和主线程之间传递消息和更新 UI。适合处理简单的、与 UI 交互紧密相关的消息传递,例如网络请求完成后更新界面上的文本信息。性能开销相对较小,因为它是 Android 系统原生的线程间通信机制,底层实现较为简单。对于简单的消息传递,使用 Message 可以获得较好的性能。

Event Bus适用于组件间的解耦通信,包括 ActivityFragmentService 以及不同线程之间的通信。它可以让不同组件之间无需直接引用,通过发布和订阅事件的方式进行数据传递。由于 EventBus 采用了反射机制来实现事件的订阅和发布,因此在性能上会有一定的开销。特别是在频繁发布和订阅事件的情况下,性能可能会受到影响。不过,EventBus 在大多数应用场景下的性能表现仍然是可以接受的。

总之,Message 适用于简单的线程间通信,而 EventBus 更适合处理复杂的组件间解耦通信。

添加依赖

implementation("org.greenrobot:eventbus:3.3.1")

创建Event(就是个DTO)

public class MessageEvent {
    private String message;

    public MessageEvent() {
    }

    public MessageEvent(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

接收消息

@Subscribe(threadMode = ThreadMode.MAIN)  
public void onMessageEvent(MessageEvent event) {
    // Do something
}

注册订阅者

@Override
public void onCreate(Bundle savedInstanceState) {
    EventBus.getDefault().register(this);
}

@Override
public void onDestroy() {
    EventBus.getDefault().unregister(this);
}

发送消息 普通消息

EventBus.getDefault().post(new MessageEvent("honyee message"));

粘性消息

消息发布后,事件还会存在,需要手动移除

// 发布
EventBus.getDefault().postSticky(new MessageEvent("Hello everyone!"));

// 获取粘性事件
MessageEvent stickyEvent = EventBus.getDefault().getStickyEvent(MessageEvent.class);
if(stickyEvent != null) {
    // 移除粘性事件
    EventBus.getDefault().removeStickyEvent(stickyEvent);
    // do something.
}

// 移除粘性事件
MessageEvent stickyEvent = EventBus.getDefault().removeStickyEvent(MessageEvent.class);
if(stickyEvent != null) {
    // do something.
}

蓝牙

新版安卓对权限的管控更严格,这里只记录新版蓝牙的使用方式

首先我当前的版本(build.gradle)是:

android {
    namespace 'com.honyee.gas_leak'
    compileSdk 34

    defaultConfig {
        applicationId "com.honyee.gas_leak"
        minSdk 21
        targetSdk 34
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

添加权限

AndroidManifest.xml中添加权限

    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
    <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="34"/>
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="34"/>
    <uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

简单使用示例

以下示例在MainActivity中编写

获取默认蓝牙适配器

BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

if(bluetoothAdapter == null){
    // 设备不支持蓝牙
}

注册一个蓝牙事件回调

ActivityResultLauncher<Intent> startBlueTooth = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
                result -> {
                    if (result == null) {
                        Log.e("打开蓝牙", "打开失败");
                    } else {
                        if (result.getResultCode() == RESULT_CANCELED) {
                            Log.d("打开蓝牙", "用户取消");
                        } else {
                            whenOpenBluetooth = false;
                        }
                    }
                });

打开蓝牙

// 设备尚未开启蓝牙
if (!bluetoothAdapter.isEnabled()) {
    // 新版启动
    context.startBlueTooth.launch(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE));
}

扫描设备

// 获取已配对的蓝牙设备列表
Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();

连接设备

BluetoothSocket socket = null;
for (BluetoothDevice device : pairedDevices) {
    // 连接指定蓝牙
    if ("honyee_bluetooth_name".equals(device.getName())) {
        try {
            socket = device.createRfcommSocketToServiceRecord(MY_UUID);
            socket.connect();
            break;
        } catch (IOException e) {
            e.printStackTrace();
            try {
                if (socket != null) {
                    socket.close();
                }
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }
}

发送消息

OutputStream outputStream = socket.getOutputStream();
byte[] downBytes = new byte[]{1, 2, 3}
outputStream.write(downBytes);
outputStream.flush();

接收消息

InputStream inputStream = socket.getInputStream();
byte[] buff = new byte[1024];
int readCount = inputStream.read(buff);

实际为了避免read长时间阻塞,改造如下

// 读取超时时间,毫秒
static final int readTimeOut = 2000;
// 循环检查读取间隔,毫秒
static final int sleep = 100;
// 最大读取等待次数
static final int waitMax = readTimeOut / sleep;
    
 /**
 * 读取响应
 * <p>
 * 因为inputStream.read()是阻塞的,
 * 所以先通过因为inputStream.available()判断是否达到期望值再读取,
 * 可控手动超时,避免长时间阻塞
 *
 * @param expectedLength 期望的长度
 */
private static byte[] read(InputStream is, int expectedLength) {
    try {
        // 已等待次数
        int count = 0;
        // 当前可读字节数
        int ava = 0;
        // 等待可读字节数达到期望值
        while (ava < expectedLength && count < waitMax) {
            count++;
            ava = is.available();
            Thread.sleep(sleep);
        }
        if (ava < expectedLength) {
            Log.d("read", "读取超时");
            if (ava > 0) {
                // 清理不需要的数据
                byte[] result = new byte[ava];
                int readCount = is.read(result, 0, ava);
            }
            return null;
        }
        byte[] result = new byte[expectedLength];
        int readCount = is.read(result, 0, expectedLength);
        return result;
    } catch (Exception e) {
        Log.e("send", e.getMessage() == null ? "" : e.getMessage());
        return null;
    }
}

延时任务和定时任务

Timer

定时任务

Timer timer = new Timer();
TimerTask task = new TimerTask() {
    @Override
    public void run() {

    }
}
timer.schedule(task, 0, 5000);

Handler

可以用于异步绘制主线程UI,有sendMessage 和 sendMessageDelayed 两个方法

主要由MessageHandlerMessageQueueLooper四部分组成:

Message,线程之间传递的消息,用于不同线程之间的数据交互。Message中的what字段用来标记区分多个消息,arg1、arg2 字段用来传递int类型的数据,obj可以传递任意类型的字段。

Handler,用于发送和处理消息。其中的sendMessage()用来发送消息,handleMessage()用于消息处理,进行相应的UI操作。

MessageQueue,消息队列(先进先出),用于存放Handler发送的消息,一个线程只有一个消息队列。

Looper,可以理解为消息队列的管理者,当发现MessageQueue中存在消息,Looper就会将消息传递到handleMessage()方法中,同样,一个线程只有一个Looper。

实现一个Handler

import android.os.Handler;
import android.os.Looper;
import android.os.Message;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public class MyHandler extends Handler {

    public MyHandler(@NonNull Looper looper, @Nullable Callback callback) {
        super(looper, callback);
    }

    @Override
    public void handleMessage(@NonNull Message msg) {
        super.handleMessage(msg);
    }
}

发送消息



Handler handler = new MyHandler(Looper.myLooper(),
            data -> {
                System.out.println("消息处理");
                return true;
            });
            
Message msg = Message.obtain(); // 实例化消息对象
msg.what = 1; // 消息标识
msg.obj = "AA"; // 消息内容存放
// 实时发送
handler.sendMessage(msg);

// 每个Message只能被send一次,所以重新创建一个
msg = Message.obtain(); // 实例化消息对象
msg.what = 1; // 消息标识
msg.obj = "AA"; // 消息内容存放
// 延迟发送
handler.sendMessageDelayed(msg, 5000);

SharedPreferences

保存简单的键值对到存储中,类似前端的LocalStorage。

MainActivity的使用示例

String storageName = "bluetooth";
SharedPreferences sharedPreferences = getSharedPreferences(storageName, MODE_PRIVATE);
SharedPreferences.Editor edit = sharedPreferences.edit();
edit.putString("bluetooth_name", "honyee");
edit.apply();

指定文件的操作模式和访问权限

  • MODE_PRIVATE:默认的模式,仅该应用能读写
  • MODE_WORLD_READABLE:已废弃,可被其他应用读,不可写。
  • MODE_WORLD_WRITEABLE:已废弃,可被其他应用读写。
  • MODE_MULTI_PROCESS:该应用的多个进程共享。
  • MODE_PRIVATE | MODE_MULTI_PROCESS:这是一种组合模式,既保证了文件的私有性,又允许在多个进程中访问。
getSharedPreferences("my_prefs", Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS);

RxJava

// todo

数据库

默认是SQLite

SQLiteOpenHelper

安卓原生方法,仅了解,一般使用ORM框架

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class DbHelper extends SQLiteOpenHelper {
    // 数据库版本
    private static final int VERSION = 1;
    //  创建数据库名叫 users
    private static final String DBNAME = "users.db";   

    public DbHelper(Context context) {
        super(context, DBNAME, null, VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        // 新建一张表:站点
        db.execSQL("create table site (_id integer primary key autoincrement, site_name varchar(32), site_code varchar(32))");
    }

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

    }
}

MainActivity的使用示例

 DbHelper dbHelper = new DbHelper(this);
SQLiteDatabase db = dbHelper.getWritableDatabase();

{
    // 插入数据
    ContentValues v = new ContentValues();
    v.put("site_name","站点1号");
    v.put("site_code", "no_1");
    long count = db.insert("site", null, v);
}

{
    // 删除数据
    db.delete("site", "id=?", new String[]{"1"});
}
{
    // 更新数据
    ContentValues v = new ContentValues();
    v.put("site_name", "站点2号");
    db.update("site", v, "id=?", new String[]{"1"});
}
{
    // 查询数据
    Cursor cursor = db.rawQuery("select * from site", new String[0]);
    String[] columnNames = cursor.getColumnNames();
    while (cursor.moveToNext()) {
        int idIndex = cursor.getColumnIndex("id");
        int id = cursor.getInt(idIndex);
        int siteNameIndex = cursor.getColumnIndex("site_name");
        String siteName = cursor.getString(siteNameIndex);
        int siteCodeIndex = cursor.getColumnIndex("site_code");
        String siteCode = cursor.getString(siteCodeIndex);
    }
}

查看数据库文件

  1. View -> Tool Windows -> Device Explorer

image.png

  1. /data/data目录下,可以根据项目的包名,找到应用的目录。也可以直接Ctrl + F搜索项目的包名

image.png

  1. 上面创建的数据库文件名为user.db,还有可以看到SharedPreferences的键值对数据

image.png

Room

一款ORM框架,简化了数据库操作

基本使用

  1. 依赖
dependencies {
    // Room运行时库
    implementation 'androidx.room:room-runtime:2.6.1'
    // Room编译时注解处理器
    annotationProcessor 'androidx.room:room-compiler:2.6.1'
    // 如果使用Kotlin,还需添加Kotlin扩展库
    implementation 'androidx.room:room-ktx:2.6.1'
}
  1. 创建实体类
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.PrimaryKey;

@Entity(tableName = "site")
public class Site {

    @PrimaryKey(autoGenerate = true)
    private Integer id;

    @ColumnInfo(name = "site_name")
    private String siteName;

    @ColumnInfo(name = "site_code")
    private String siteCode;

    // 省略get/set
}

  1. 创建数据访问对象(DAO)
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import androidx.room.Update;

import com.honyee.gas_leak.model.Site;

import java.util.List;

@Dao
public interface SiteDao {

    @Query("select * from site")
    List<Site> queryAll();

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    void insert(Site... sites);

    @Update
    void updateUser(Site site);

    @Query("SELECT * FROM site WHERE id = :id")
    Site queryById(int id);

    @Query("DELETE FROM site WHERE id = :id")
    void deleteById(int id);
}
  1. 创建数据库类
import android.content.Context;

import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;

import com.honyee.gas_leak.model.Site;
import com.honyee.gas_leak.db.room.dao.SiteDao;

@Database(entities = {Site.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {

    private static final String DBNAME = "users.db";

    private static volatile AppDatabase INSTANCE;

    public abstract SiteDao siteDao();

    public static AppDatabase getInstance(Context context) {
        if (INSTANCE == null) {
            synchronized (AppDatabase.class) {
                if (INSTANCE == null) {

                    INSTANCE = Room.databaseBuilder(
                                    context.getApplicationContext(),
                                    AppDatabase.class,
                                    DBNAME)
                            .allowMainThreadQueries()
                            .build();
                }
            }
        }
        return INSTANCE;
    }
}
  1. MainActivity的使用示例
AppDatabase instance = AppDatabase.getInstance(this);

Site site = new Site();
site.setSiteName("站点1号");
site.setSiteCode("编号1号");
instance.siteDao().insert(site);

List<Site> all = instance.siteDao().queryAll();

配合RxJava3使用

dependencies {
    // Room相关依赖
    implementation "androidx.room:room-runtime:2.6.1"
    annotationProcessor "androidx.room:room-compiler:2.6.1"
    // RxJava相关依赖
    implementation 'io.reactivex.rxjava3:rxjava:3.1.10'
    implementation 'io.reactivex.rxjava3:rxandroid:3.0.2'
    // 用于将Room操作适配到RxJava
    implementation "androidx.room:room-rxjava3:2.6.1"
}
// 修改返回值用Single包裹
@Query("SELECT * FROM site WHERE id = :id")
Single<Site> queryById(int id);
Disposable subscribe = instance.siteDao()
        .queryById(all.get(0).getId())
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(site -> {
                    // 查询成功,处理用户对象
                    System.out.println(site);
                },
                throwable -> {
                    // 查询失败,处理异常
                    System.out.println(throwable);
                });

subscribeOn (Schedulers.io ()) 的作用

  • 指定上游操作线程subscribeOn操作符用于指定Observable(被观察者)发送事件的线程,即指定整个RxJava链中上游操作所运行的线程。在这里使用Schedulers.io(),表示将上游的任务,如网络请求、文件读取、数据库操作等耗时操作,放在io线程池中执行。io线程池适用于 I/O 密集型任务,它能有效地管理和复用线程,提高 I/O 操作的效率。
  • 避免阻塞主线程:如果不指定subscribeOn,默认情况下RxJava的操作会在调用subscribe方法的线程中执行,在 Android 应用中,这很可能就是主线程。若在主线程中执行耗时操作,会导致界面卡顿,甚至出现ANR(应用无响应)错误。通过subscribeOn(Schedulers.io()),可以将这些耗时操作转移到后台的io线程执行,保证主线程的流畅运行,使应用的界面能够及时响应用户的操作。

observeOn (AndroidSchedulers.mainThread ()) 的作用

  • 指定下游操作线程observeOn操作符用于指定Observer(观察者)接收事件和处理事件的线程。这里使用AndroidSchedulers.mainThread(),表示将下游的操作切换到 Android 的主线程执行。在 Android 中,更新 UI 的操作必须在主线程进行,所以当需要对从上游获取的数据进行 UI 更新时,就需要使用observeOn(AndroidSchedulers.mainThread())将操作切换到主线程。
  • 确保 UI 操作的安全性:Android 的 UI 组件不是线程安全的,若在非主线程中更新 UI,可能会导致各种意想不到的问题,如界面显示异常、程序崩溃等。observeOn(AndroidSchedulers.mainThread())保证了对 UI 的操作在主线程中进行,符合 Android 的 UI 更新规则,确保了 UI 操作的安全性和稳定性。

例如,在进行网络请求获取数据后更新 UI 的场景中,subscribeOn(Schedulers.io())使网络请求在后台io线程执行,observeOn(AndroidSchedulers.mainThread())使获取到数据后的 UI 更新操作在主线程执行,实现了耗时操作与 UI 操作的正确分离,保证了应用的性能和稳定性。

升级数据库版本

会有修改表、表的需求,需要补充一些更新操作,不然会报错

java.lang.IllegalStateException: Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number. You can simply fix this by increasing the version number.

大致的意思是:你修改了数据库,但是没有升级数据库的版本

此时需要递增版本号

image.png

然后会出现没有升级脚本的错误提示

java.lang.IllegalStateException: A migration from 1 to 2 was required but not found. Please provide the necessary Migration path via RoomDatabase.Builder.addMigration(Migration ...) or allow for destructive migrations via one of the RoomDatabase.Builder.fallbackToDestructiveMigration* methods.

大致的意思是:让我们添加一个addMigration或者调用fallbackToDestructiveMigration完成迁移

  1. 不推荐。使用fallbackToDestructiveMigration,但是历史数据会被清空
Room.databaseBuilder(
        context.getApplicationContext(),
        AppDatabase.class,
        DBNAME)
    .allowMainThreadQueries()
    .fallbackToDestructiveMigration()
    .build();
  1. 推荐。 准备升级脚本,配置addMigrations,例如新增了Template
import androidx.annotation.NonNull;
import androidx.room.migration.Migration;
import androidx.sqlite.db.SupportSQLiteDatabase;

public class MigrationData {
    public static final Migration[] ALL = {
            new Migration(1, 2) {
                @Override
                public void migrate(@NonNull SupportSQLiteDatabase database) {
                    database.execSQL("CREATE TABLE `template` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `template_name` TEXT, `template_code` TEXT);");

                }
            }
    };

}
INSTANCE = Room.databaseBuilder(
        context.getApplicationContext(),
        AppDatabase.class,
        DBNAME)
    .addMigrations(MigrationData.ALL)
    .allowMainThreadQueries()
    .build();

GreenDAO

一款ORM框架,停止更新了。

GreenDAO 是一款开源的面向 Android 的轻便、快捷的 ORM 框架,将 Java 对象映射到 SQLite 数据库中,我们操作数据库的时候,不再需要编写复杂的 SQL语句, 在性能方面,greenDAO 针对 Android 进行了高度优化,最小的内存开销 、依赖体积小 同时还是支持 数据库加密。

Realm

一款ORM框架,没尝试

Realm 是一个手机数据库,是用来替代 SQlite 的解决方案,比 SQlite 更轻量级,速度更快,因为它有一套自己的数据库搜索引擎,并且还具有很多现代数据库的优点,支持 JSON,流式 API 调用,数据变更通知,自动数据同步,简单身份验证,访问控制,事件处理,最重要的是跨平台,目前已经支持 Java、Swift、Object - C、React - Native 等多种实现。

HTTP请求

准备

添加网络权限

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

修改支持http方式

not permitted by network security policy

由于 Android P(版本27以上) 限制了明文流量的网络请求,非加密的流量请求都会被系统禁止掉。如果当前应用的请求是 htttp 请求,而非 https,系统会禁止该请求。 也就是Android9.0以上都要https,不能http了,不然拒绝访问。

解决方式:AndroidManifest.xmlapplication中增加:

android:usesCleartextTraffic="true"

retrofit2

官网 - Retrofit

基础版本

将响应转换为ResponseBody

添加依赖

dependencies {
    implementation 'com.squareup.retrofit2:retrofit:2.11.0'
}

编写请求接口

import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Path;
import retrofit2.http.Query;

public interface MyHttpService {
    @GET("/api/{func}/get")
    Call<ResponseBody> apiGet(@Path("func") String func, @Query("id") Integer id);
}

编写请求测试

 Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://192.168.0.194:1222/")
                .build();

MyHttpService myHttpService = retrofit.create(MyHttpService.class);
Call<ResponseBody> call = myHttpService.apiGet("user", 123);
call.enqueue(new Callback<ResponseBody>() {
    @Override
    public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
        ResponseBody body = response.body();
    }

    @Override
    public void onFailure(Call<ResponseBody> call, Throwable throwable) {

    }
});

进阶版本

基础版只能将结果转为ResponseBody,否则会出现

Could not locate ResponseBody converter for class xxxxxxx
  1. 使用Gson转换器,将响应转换为自定义的响应DTO MyResponse
  2. 使用rxjava来进行异步请求

添加依赖

dependencies {
    implementation 'com.squareup.retrofit2:retrofit:2.11.0'
    
    implementation 'com.squareup.retrofit2:converter-gson:2.11.0'
    implementation 'com.google.code.gson:gson:2.11.0'
    
    implementation 'com.squareup.retrofit2:adapter-rxjava3:2.11.0'
    implementation 'io.reactivex.rxjava3:rxjava:3.1.10'
}

编写请求接口

import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Path;
import retrofit2.http.Query;

public interface MyHttpService {
    @GET("/api/{func}/get")
    Call<MyResponse> apiGet(@Path("func") String func, @Query("id") Integer id);    
    
    @GET("/api/{func}/get")
    Observable<MyResponse> apiGetObservable(@Path("func") String func, @Query("id") Integer id);
}

编写请求测试

添加转换器addConverterFactory(GsonConverterFactory.create())

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("http://192.168.0.194:1222/")
        .addConverterFactory(GsonConverterFactory.create())
        .build();

MyHttpService myHttpService = retrofit.create(MyHttpService.class);

// 转自定义DTO
Call<MyResponse> call = myHttpService.apiGet("user", 123);
call.enqueue(new Callback<MyResponse>() {

    @Override
    public void onResponse(Call<MyResponse> call, Response<MyResponse> response) {
        MyResponse body = response.body();
    }

    @Override
    public void onFailure(Call<MyResponse> call, Throwable throwable) {

    }
});

// 异步请求
 Disposable disposable = myHttpService.apiGetObservable("test", 123)
                .subscribe(myResponse -> {
                    
                });
// 可以通过调用 disposable.dispose()来取消订阅

Glide 图片加载库

Glide是一个快速高效的Android图像加载库,专注于平滑滚动。支持获取、解码以及展示视频静帧、图片和动画 GIF。

最新版本是5.0但是这里用的V4版本

implementation 'com.github.bumptech.glide:glide:5.0.0-rc01'

依赖

implementation 'com.github.bumptech.glide:glide:4.16.0'

基本使用

假定drawable内有个叫process的图片文件

<ImageView
    android:id="@+id/gif_image_view"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
ImageView gifImageView = findViewById(R.id.gif_image_view);
Glide.with(context)
        .load(R.drawable.process)
        .into(gifImageView);

图片占位符

一般用在请求网络图片时

  • placeholder:默认图片,例如正在请求图片的时候展示的图片
  • error: 载入失败时的图片,如果没有设置,还是展示 placeholder
  • fallback:请求的url/modelnull的时候展示的图片,如果没有设置,还是展示placeholder
 RequestOptions requestOptions = new RequestOptions()
                .placeholder(R.drawable.ic_launcher_foreground)
                .error(R.mipmap.ic_launcher)
                .fallback(R.drawable.ic_launcher_foreground)
                .override(100, 100); // 指定加载图片大小
                
ImageView gifImageView = findViewById(R.id.gif_image_view);
Glide.with(context)
        .load(R.drawable.process)
        .apply(requestOptions) // 应用配置
        .into(gifImageView);

过度动画

从占位符到新加载的图片,或从缩略图到全尺寸图像过渡。

交叉淡入(避免占位图还能显示)

        DrawableCrossFadeFactory factory = new DrawableCrossFadeFactory.Builder()
                .setCrossFadeEnabled(true).build();
                
        Glide.with(context)
                .load(R.drawable.process)
                .transition(DrawableTransitionOptions.with(factory)) // 应用配置
                .into(gifImageView);

变换

获取资源并修改它,然后返回被修改后的资源。

通常变换操作是用来完成裁剪或对位图应用过滤器,比如对图片进行圆角配置。

// 圆形
Glide.with(context)
        .load(R.drawable.process)
        .transform(new CircleCrop())
        .into(gifImageView);

// 圆角
Glide.with(context)
        .load(R.drawable.process)
        .transform(new RoundedCorners(100))
        .into(gifImageView);
        
// 圆角,分别配置四个角
Glide.with(context)
        .load(R.drawable.process)
        .transform(new GranularRoundedCorners(10, 20, 30, 40))
        .into(gifImageView);
        
// 旋转
Glide.with(context)
        .load(R.drawable.process)
        .transform(new Rotate(90))
        .into(gifImageView);

部分API说明

方法描述
with指定Context
load图片,可以是drawableurlbyte[]File
intoImageView对象
placeholder默认的图片
error错误时的图片
fallback目标资源为空时的图片
override图片大小
transition过渡动画
transform变形
fitCenter图片居中

全局配置

额外加入依赖

implementation 'com.github.bumptech.glide:glide:4.16.0'

实现配置类

import androidx.annotation.NonNull;

import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.Registry;
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.module.AppGlideModule;

/**
* 需要注解+继承,有且只有一个该配置类
*/
@GlideModule
public class MyGlideModule extends AppGlideModule {

    /**
     * 配置图片缓存的路径和缓存空间的大小
     */
    @Override
    public void applyOptions(@NonNull final Context context, GlideBuilder builder) {
        super.applyOptions(context, builder);
    }

    /**
     * 是否读取配置文件,一般不需要
     */
    @Override
    public boolean isManifestParsingEnabled() {
        return false;
    }

    /**
     * 注册指定类型的源数据,并指定它的图片加载所使用的 ModelLoader
     */
    @Override
    public void registerComponents(@NonNull Context context, @NonNull Glide glide, Registry registry) {
        super.registerComponents(context, glide, registry);
    }

}

如果配置生效,build项目后可以看到

image.png

原先使用的是 Glide,现在改用GlideApp

GlideApp.with(context)
        .load(R.drawable.process)
        .apply(requestOptions) // 应用配置
        .into(gifImageView);

applyOptions方法中,可以如下操作

  1. 设置内存缓存大小
// 设置内存缓存大小为50MB
int memoryCacheSizeBytes = 50 * 1024 * 1024;
builder.setMemoryCache(new LruResourceCache(memoryCacheSizeBytes));
  1. 设置磁盘缓存大小
// 设置磁盘缓存大小为100MB
int diskCacheSizeBytes = 100 * 1024 * 1024;
builder.setDiskCache(new InternalCacheDiskCacheFactory(context, diskCacheSizeBytes));
  1. 设置Bitmap大小
// 设置Bitmap大小为100MB
int bitmapPoolBytes = 100 * 1024 * 1024;
builder.setBitmapPool(new LruBitmapPool(bitmapPoolBytes));
  1. 获取默认缓存大小
MemorySizeCalculator calculator = new MemorySizeCalculator.Builder(context).build();
int defaultMemoryCacheSize = calculator.getMemoryCacheSize();
int arrayPoolSizeInBytes = calculator.getArrayPoolSizeInBytes();
int bitmapPoolSize = calculator.getBitmapPoolSize();
  1. 设置图片质量。Glide默认使用低质量的RGB_565,如果想使用高质量ARGB_8888
// 设置全局的DecodeFormat为PREFER_ARGB_8888
builder.setDefaultRequestOptions(
        new RequestOptions()
                .format(DecodeFormat.PREFER_ARGB_8888)
);

registerComponents方法中,可以如下操作

  1. 替换网络请求组件

Glide 默认使用 HttpURLConnection 做网络请求,在这切换成 Okhttp 请求

如果已经有OkHttp了需要排除,避免重复。

implementation 'com.github.bumptech.glide:okhttp3-integration:4.16.0'
OkHttpClient okHttpClient = new OkHttpClient.Builder().build();
OkHttpUrlLoader.Factory factory = new OkHttpUrlLoader.Factory(okHttpClient);
registry.replace(GlideUrl.class, InputStream.class, factory);

绘制图表

GitHub - MPAndroidChart

添加依赖

repositories {
    maven { url 'https://jitpack.io' }
}

dependencies {
    implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
}

添加一个折线图

<com.github.mikephil.charting.charts.LineChart
    android:id="@+id/lineChart"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

添加数据并绘制

List<Entry> chartsDataList = new ArrayList<>();
int x = 1;
int y = 2;
chartsDataList.add(new Entry(x, y));

// 创建一个数据集
LineDataSet dataSet = new LineDataSet(chartsDataList, "数据名称");
// 设置线条颜色等属性
dataSet.setColor(Color.RED);
dataSet.setLineWidth(2f);
// 创建数据对象并添加数据集
LineData lineData = new LineData(dataSet);
lineChart.setData(lineData);
Description desc = new Description();
desc.setText("图表描述");
lineChart.setDescription(desc);
// 刷新图表
lineChart.invalidate(); 

常见问题和错误

Only the original thread that created a view hierarchy can touch its views

安卓的应用是单线程,且只允许主线程对UI进行更改。例如在定时器Timer中,或者其他线程中,对UI进行更改,则会出现此错误。

此时就需要用到广播,在收到广播后,对UI进行更改。

Button中设置背景、样式失效

例如下面代码,修改按钮颜色textColor,没有生效

<Button
    android:id="@+id/btn_1"
    android:layout_height="wrap_content"
    android:layout_width="match_parent"
    android:text="按钮1"
    android:textSize="20sp"
    android:textColor="#0066FF"
    android:backgroundTint="@null"
    android:background="#FF0000"/>

原因是,项目默认使用主题色

image.png

解决办法:

<Button/> 改成 <android.widget.Button/>即可

<android.widget.Button
    android:id="@+id/btn_1"
    android:layout_height="wrap_content"
    android:layout_width="match_parent"
    android:text="按钮1"
    android:textSize="20sp"
    android:textColor="#0066FF"
    android:backgroundTint="@null"
    android:background="#FF0000"/>

使用color

目录下有个colors.xml文件,里面是自定义的颜色常量

image.png

在xml中使用

<Button
    android:id="@+id/btn_1"
    android:layout_height="wrap_content"
    android:layout_width="match_parent"
    android:text="按钮1"
    android:textSize="20sp"
    android:textColor="@color/red"
    android:backgroundTint="@null"
    android:background="#FF0000"/>

在代码中使用

Button btn = findViewById(R.id.btn_1);
btn.setTextColor(getResources().getColor(R.color.java));