到这里终于开始主要接触别人的轮子,其实到这里已经有能力做一些简单App,不过一些常用的轮子还是要抽出来学一下,当然里面也会贯穿一些常用知识点.
下一步,看一下如何让App联网通信,在学习网络应用之前,要先明确一点,只有UI线程可以刷屏,同时,网络很耗时,所以不能放在UI线程,所以这就有前后分离,比如下面这个例子.
private class PlayThread extends Thread {
@Override
public void run(){
while(true){
Calendar calendar = Calendar.getInstance();
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int minute = calendar.get(Calendar.MINUTE);
int second = calendar.get(Calendar.SECOND);
String time = String.format("%02d:%02d:%02d", hour, minute, second);
// mHandler.sendEmptyMessage(0);
Message message = Message.obtain();
message.what = SET_TXT;
message.obj = time;
mHandler.sendMessage(message);
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 创建处理器对象mHandler
private Handler mHandler = new Handler(Looper.getMainLooper()){
public void handleMessage(Message message){
// 收到Msg
if (message.what == SET_TXT){
tv_hello.setText(message.obj.toString());
}
}
};
当然因为他很简单,他也可以用runOnUiThread直接在内部往外调用.
private class PlayThread extends Thread {
@Override
public void run(){
while(true){
Calendar calendar = Calendar.getInstance();
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int minute = calendar.get(Calendar.MINUTE);
int second = calendar.get(Calendar.SECOND);
String time = String.format("%02d:%02d:%02d", hour, minute, second);
runOnUiThread(() -> {
tv_hello.setText(time);
});
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
除了这种方法,安卓还提供了WorkManager,需要修改build.gradle引入.
dependencies {
implementation libs.appcompat
implementation libs.material
implementation libs.activity
implementation libs.constraintlayout
testImplementation libs.junit
androidTestImplementation libs.ext.junit
androidTestImplementation libs.espresso.core
// WorkManager库各版本见 https://mvnrepository.com/artifact/androidx.work/work-runtime
implementation 'androidx.work:work-runtime:2.10.0'
}
使用WordManager可以把复杂的任务独立成一个类里,并由系统安排他合适的时间执行,异步获得数据,当然,任务不是UI线程,他依然不可以刷新标签.
先创建一个包含生成随机数能力的Worker.
public class RandomNumberWorker extends Worker {
public static final String KEY_MIN_VALUE = "min_value";
public static final String KEY_MAX_VALUE = "max_value";
public static final String KEY_RESULT = "random_result";
public RandomNumberWorker(
@NonNull Context context,
@NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
@NonNull
@Override
public Result doWork() {
// 获取输入数据
int min = getInputData().getInt(KEY_MIN_VALUE, 0);
int max = getInputData().getInt(KEY_MAX_VALUE, 100);
// 生成随机数
Random random = new Random();
int randomNumber = random.nextInt(max - min + 1) + min;
// 创建输出数据
Data outputData = new Data.Builder()
.putInt(KEY_RESULT, randomNumber)
.build();
// 返回结果
return Result.success(outputData);
}
}
之后在其他线程(比如UI线程)安排这个任务,并在可用时获得他的返回.
// 创建输入数据
Data inputData = new Data.Builder()
.putInt(RandomNumberWorker.KEY_MIN_VALUE, 10)
.putInt(RandomNumberWorker.KEY_MAX_VALUE, 50)
.build();
// 创建约束条件
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build();
// 创建一次性工作请求
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(RandomNumberWorker.class)
.setInputData(inputData)
.setConstraints(constraints)
.build();
// 观察工作状态
WorkManager.getInstance(this).getWorkInfoByIdLiveData(workRequest.getId())
.observe(this, new Observer<WorkInfo>() {
@Override
public void onChanged(WorkInfo workInfo) {
if (workInfo != null) {
if (workInfo.getState() == WorkInfo.State.SUCCEEDED) {
// 任务完成,获取输出数据
int result = workInfo.getOutputData().getInt(
RandomNumberWorker.KEY_RESULT, 0);
tvHello.setText("随机数: " + result);
} else if (workInfo.getState() == WorkInfo.State.FAILED) {
tvHello.setText("任务失败");
}
}
}
});
// 启动作业
WorkManager.getInstance(this).enqueue(workRequest);
tvHello.setText("任务已启动...");
除了一次性任务外,也可以创建周期性任务.
// 创建周期性工作请求 (最小间隔15分钟)
PeriodicWorkRequest periodicWorkRequest =
new PeriodicWorkRequest.Builder(RandomNumberWorker.class,
15, TimeUnit.MINUTES)
.setInputData(new Data.Builder()
.putInt(RandomNumberWorker.KEY_MIN_VALUE, 100)
.putInt(RandomNumberWorker.KEY_MAX_VALUE, 200)
.build())
.build();
下面说网络通信,最常见就是调用各种API,大多数API返回都是JSON,所以也先引入一个JSON库.
// gson库各版本见 https://mvnrepository.com/artifact/com.google.code.gson/gson
implementation 'com.google.code.gson:gson:2.13.0'
// okhttp库各版本见 https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
为了上网,还得添加权限.
<uses-permission android:name="android.permission.INTERNET" />
一般情况下,比如我写了一个简单的图片上传,并通过返回的来判断是否成功.
private boolean uploadImage(String imageUri, String serverFileName) {
OkHttpClient client = new OkHttpClient();
File file = new File(imageUri);
Gson gson = new Gson();
if (!file.exists()) {
return false;
}
// 如果未指定服务器文件名,则使用原文件名
String uploadFileName = (serverFileName == null || serverFileName.isEmpty())
? file.getName() : serverFileName;
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("filename",uploadFileName)
.addFormDataPart("file", uploadFileName,
RequestBody.create(MediaType.parse("image/*"), file))
.build();
Request request = new Request.Builder()
.url("https://note.242345.xyz/DCIM/index.php") // 替换为你的上传URL
.post(requestBody)
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
return false;
}
// 解析服务器返回的JSON
String responseBody = response.body().string();
ServerResponse serverResponse = gson.fromJson(responseBody, ServerResponse.class);
// 根据服务器返回的success字段判断是否成功
if (serverResponse != null && serverResponse.success){
AppDatabase database = AppDatabase.getDatabase(getApplicationContext());
ImageProcessor processor = new ImageProcessor(getApplicationContext());
processor.processImage(imageUri, database.imageDao());
return true;
}else{
return false;
}
} catch (IOException e) {
Log.e(TAG, "上传图片失败", e);
return false;
}
}
我通过运用之前数据库的知识,简单拼了一个自动上传备份照片的功能,当然毕竟是一个学了几天的新手,代码肯定很烂.
https://github.com/nickfox-taterli/simple-android-leran
主题逻辑就是打开后会自动上传图片,每次上传后会刷新视图,显示剩余还没上传的图片,再次说了,是非常烂的代码.

除了这些HTTP请求外,还有Socket和WebSocket两种,因为HTTP是单向查询的,所以对于某些需要推送的就不太好用,因为安卓已经发展很多年了,太多轮子可以用了,但是服务器端的话也要采用一些兼容SocketIO库的选择才行,不然的话直接实现看了下还是挺复杂的.
除了联网通信外,App中还有很多常见的应用,比如播放,录制音频,播放,录制视频,拍照,显示图片.
启动系统自带录音机进行录音.
private void startSystemRecording() {
// 检查权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, permissions, REQUEST_RECORD_AUDIO_PERMISSION);
return;
}
// 启动系统录音应用
Intent intent = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION);
if (intent.resolveActivity(getPackageManager()) != null) {
startActivityForResult(intent, REQUEST_RECORD_AUDIO_PERMISSION);
} else {
Toast.makeText(this, "No recording app found", Toast.LENGTH_SHORT).show();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_RECORD_AUDIO_PERMISSION && resultCode == RESULT_OK && data != null) {
Uri audioUri = data.getData();
// 处理录音文件
if (audioUri != null) {
Toast.makeText(this, "Recording saved: " + audioUri.toString(), Toast.LENGTH_SHORT).show();
}
}
}
大多数App其实是都是内部支持录音,比如微信就不可能跳转到外面录音啊.
private void checkAndStartRecording() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
!= PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{
Manifest.permission.RECORD_AUDIO,
Manifest.permission.WRITE_EXTERNAL_STORAGE
},
REQUEST_PERMISSION_CODE);
} else {
startRecording();
}
}
private void startRecording() {
try {
// 创建输出文件
outputFile = getExternalCacheDir().getAbsolutePath() + "/recording.3gp";
mediaRecorder = new MediaRecorder();
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
mediaRecorder.setOutputFile(outputFile);
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
mediaRecorder.prepare();
mediaRecorder.start();
isRecording = true;
Toast.makeText(this, "Recording started", Toast.LENGTH_SHORT).show();
} catch (IOException e) {
Log.e("MediaRecorder", "Recording failed", e);
releaseMediaRecorder();
}
}
private void stopRecording() {
if (isRecording) {
mediaRecorder.stop();
isRecording = false;
Toast.makeText(this, "Recording saved to " + outputFile, Toast.LENGTH_LONG).show();
}
releaseMediaRecorder();
}
private void releaseMediaRecorder() {
if (mediaRecorder != null) {
mediaRecorder.release();
mediaRecorder = null;
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_PERMISSION_CODE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startRecording();
} else {
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show();
}
}
}
@Override
protected void onStop() {
super.onStop();
releaseMediaRecorder();
}
播放音乐也是同理的.
private void playAudioWithMediaPlayer(String audioPath) {
try {
if (mediaPlayer == null) {
mediaPlayer = new MediaPlayer();
} else {
mediaPlayer.reset();
}
mediaPlayer.setDataSource(audioPath); // 可以是本地文件路径或网络URL
mediaPlayer.prepareAsync(); // 异步准备
mediaPlayer.setOnPreparedListener(mp -> {
mp.start(); // 准备好后开始播放
Log.d("MediaPlayer", "Duration: " + mp.getDuration() + "ms");
});
mediaPlayer.setOnCompletionListener(mp -> {
releaseMediaPlayer();
Log.d("MediaPlayer", "Playback completed");
});
mediaPlayer.setOnErrorListener((mp, what, extra) -> {
releaseMediaPlayer();
Log.e("MediaPlayer", "Playback error: " + what + ", " + extra);
return true;
});
} catch (IOException e) {
Log.e("MediaPlayer", "Playback failed", e);
releaseMediaPlayer();
}
}
private void releaseMediaPlayer() {
if (mediaPlayer != null) {
mediaPlayer.release();
mediaPlayer = null;
}
}
// 暂停播放
private void pausePlayback() {
if (mediaPlayer != null && mediaPlayer.isPlaying()) {
mediaPlayer.pause();
}
}
// 继续播放
private void resumePlayback() {
if (mediaPlayer != null && !mediaPlayer.isPlaying()) {
mediaPlayer.start();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
releaseMediaPlayer();
}
使用MediaStore.ACTION_VIDEO_CAPTURE意图可以调起系统的拍照和录像,这里也不展开了,但是用CamerX可以在软件内部直接用相机,包括拍照和录像.
private void takePhoto() {
// 创建输出文件
File photoFile = new File(
getOutputDirectory(),
new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS").format(new Date()) + ".jpg"
);
ImageCapture.OutputFileOptions outputFileOptions =
new ImageCapture.OutputFileOptions.Builder(photoFile).build();
imageCapture.takePicture(
outputFileOptions,
ContextCompat.getMainExecutor(this),
new ImageCapture.OnImageSavedCallback() {
@Override
public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
String savedUri = photoFile.getAbsolutePath();
Toast.makeText(MainActivity.this, "Photo saved: " + savedUri, Toast.LENGTH_SHORT).show();
}
@Override
public void onError(@NonNull ImageCaptureException exception) {
Log.e("CameraX", "Photo capture failed: " + exception.getMessage(), exception);
}
}
);
}
之前说的播放都要求资源在本地,还有一个轮子叫ExoPlayer就支持网络流,各种动态加载都很方便.这一次学习很多已经是调包的过程了,虽然看起来技术含量不高,但是需要反复实践.