安卓开发(0)前言&目录
主要是右上角的menu里面的按钮的功能实现。
写完一个页面别忘了去AndroidManifest.xml
注册一下。
快捷记录功能实现
功能概述
快捷记录功能帮助用户在就诊过程中快速记录重要信息,自动保存并支持历史记录查看,即使软件卸载了笔记都还在。
核心文件
-
NoteEditActivity.java
-
activity_note_edit.xml
-
top_menu.xml
代码实现
布局文件(activity_note_edit.xml)
很简单的界面,一整个文字编辑界面。
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:background="@android:color/white">
<EditText
android:id="@+id/note"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:hint="在此输入内容..."
android:inputType="textMultiLine"
android:scrollbars="vertical"
android:scrollHorizontally="false"
android:padding="16dp"
android:textSize="16sp"
android:lineSpacingExtra="4dp">
<requestFocus />
</EditText>
</ScrollView>
菜单配置(top_menu.xml)
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!-- 其他菜单项省略 -->
<item
android:id="@+id/note"
android:title="快捷记录"
app:showAsAction="never" />
</menu>
主要功能实现(NoteEditActivity.java)
但是这个功能还有一个问题没有解决,就是键盘弹出的时候对界面的遮挡没有解决。一直以为这是一个任何笔记编辑界面都已经不需要考虑的问题,但是我始终没能解决😭😭。
-
文件读写:使用
openFileInput
和openFileOutput
持久化存储笔记到私有目录。即使软件卸载了这个笔记也还在。 -
时间戳:仅首次打开时在末尾添加
--- yyyy-MM-dd HH:mm:ss ---
分隔标记。 -
自动聚焦:启动后延迟请求焦点并弹出软键盘,确保光标在末尾、内容滚动到底部。
-
自动保存:在
onPause
和onDestroy
时自动保存,防止内容丢失。
public class NoteEditActivity extends AppCompatActivity {
private static final String TAG = "NoteEditActivity";
private static final String FILE_NAME = "hospital_notes.txt";
private EditText noteEditText;
private ScrollView scrollView;
private boolean timestampAdded = false;
private boolean isKeyboardShowing = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_note_edit);
// 初始化控件
noteEditText = findViewById(R.id.note);
scrollView = findViewById(R.id.scroll_view);
// 设置系统边距(状态栏、导航栏)
ViewCompat.setOnApplyWindowInsetsListener(scrollView, (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
// 设置键盘弹出监听
setupKeyboardListener();
// 加载已有内容
loadNoteContent();
// 添加时间戳(仅一次)
addTimestamp();
// 关键:延迟聚焦并弹出输入法
focusOnEditText();
}
@Override
protected void onPause() {
super.onPause();
saveNoteContent();
}
@Override
protected void onDestroy() {
super.onDestroy();
saveNoteContent();
}
/**
* 加载已有笔记内容,若文件不存在则创建
*/
private void loadNoteContent() {
try {
BufferedReader reader = new BufferedReader(
new InputStreamReader(openFileInput(FILE_NAME), StandardCharsets.UTF_8)
);
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append("\n");
}
reader.close();
noteEditText.setText(sb.toString());
noteEditText.setSelection(sb.length()); // 光标在末尾
} catch (IOException e) {
Log.d(TAG, "文件不存在,将创建新文件: " + FILE_NAME);
try {
new File(getFilesDir(), FILE_NAME).createNewFile();
} catch (IOException ex) {
Log.e(TAG, "创建文件失败", ex);
}
}
}
/**
* 保存当前笔记内容
*/
private void saveNoteContent() {
String content = noteEditText.getText().toString().trim();
if (content.isEmpty()) return;
try {
FileOutputStream fos = openFileOutput(FILE_NAME, MODE_PRIVATE);
fos.write(content.getBytes(StandardCharsets.UTF_8));
fos.close();
Log.d(TAG, "笔记内容已保存");
} catch (IOException e) {
Log.e(TAG, "保存失败", e);
}
}
/**
* 添加时间戳(仅首次进入时)
*/
private void addTimestamp() {
if (timestampAdded) return;
String timestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(new Date());
String currentText = noteEditText.getText().toString();
String separator = currentText.isEmpty() ? "" : "\n";
String newText = currentText + separator + "--- " + timestamp + " ---\n";
noteEditText.setText(newText);
noteEditText.setSelection(newText.length()); // 光标移到末尾
timestampAdded = true;
}
/**
* 聚焦到输入框并弹出键盘,确保滚动到底部
*/
private void focusOnEditText() {
noteEditText.post(() -> {
noteEditText.requestFocus();
InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
if (imm != null) {
imm.showSoftInput(noteEditText, 0); // 弹出键盘
}
// 延迟滚动,确保键盘弹出后再滚动
scrollView.postDelayed(() -> {
scrollToCursorPosition();
}, 300);
});
}
}
功能调用流程
-
入口:在MainActivity的菜单中点击"快捷记录"选项
-
启动:通过Intent启动NoteEditActivity
-
加载:自动加载已有笔记内容,并添加当前时间戳
-
编辑:用户在EditText中输入内容
-
保存:在Activity暂停或销毁时自动保存内容到文件系统
保存全部地图功能
功能概述
"下载全部地图"功能将应用assets目录中的地图图片保存到设备相册,方便离线查看。
核心实现代码
1. 功能触发点
在MainActivity的菜单点击事件中触发:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.download_maps) {
// 调用下载地图方法
downloadMaps();
return true;
}
// ...其他菜单项处理
}
2. 下载地图主方法
/**
* 下载地图主方法
* 1. 检查存储权限
* 2. 有权限则执行复制操作
* 3. 无权限则请求权限
*/
private void downloadMaps() {
// 1. 检查权限
if (hasStoragePermission()) {
// 2. 执行下载
copyAssetsToPictures();
} else {
// 3. 显示提示并请求权限
Toast.makeText(this, "需要存储权限才能下载地图", Toast.LENGTH_SHORT).show();
requestStoragePermission();
}
}
3. 权限管理
注意这里不同的安卓版本需要写不同的权限。
/**
* 检查是否有存储权限
* 适配Android不同版本的权限模型
*/
boolean hasStoragePermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// Android 13及以上版本
return checkSelfPermission(Manifest.permission.READ_MEDIA_IMAGES) == PackageManager.PERMISSION_GRANTED;
} else {
// Android 12及以下
return checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
}
}
/** 请求存储权限 */
void requestStoragePermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requestPermissions(new String[]{Manifest.permission.READ_MEDIA_IMAGES}, 100);
} else {
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 100);
}
}
4. 核心复制功能
/**
* 从assets目录复制图片到Pictures目录
*/
void copyAssetsToPictures() {
try {
// 获取assets目录下的所有文件名
String[] files = getAssets().list("");
if (files != null) {
// 创建目标文件夹
File picturesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
File hospitalNaviDir = new File(picturesDir, "HospitalNavi");
if (!hospitalNaviDir.exists()) {
hospitalNaviDir.mkdirs();
}
int copiedCount = 0;
// 遍历assets中的文件
for (String filename : files) {
// 只处理jpg图片文件
if (filename.endsWith(".jpg")) {
// 复制文件
copyFile(filename, new File(hospitalNaviDir, filename));
copiedCount++;
}
}
// 显示复制成功的提示
Toast.makeText(this, "成功保存" + copiedCount + "张地图到相册", Toast.LENGTH_SHORT).show();
}
} catch (IOException e) {
Log.e("MainActivity", "保存文件失败", e);
Toast.makeText(this, "保存地图失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
功能流程
-
用户点击菜单中的"下载全部地图"选项
-
系统检查存储权限
-
有权限:直接执行复制操作
-
无权限:请求存储权限
-
-
权限被授予后,将assets中的所有jpg图片复制到设备Pictures/HospitalNavi目录
-
显示复制结果提示
停车找车功能实现
功能概述
用户在医院停车场停车后,通过拍照和定位记录当前停车位置。当定位成功之后,按钮变成可点击的,照片选中的时候,会记录下照片并显示,同时在地图上做出标记。再次进入这个页面,依然存在标记和照片。当软件进程结束,自动清理掉地图标记和照片。
核心文件
-
FindCarActivity.java
-
activity_findcar.xml
布局文件(activity_findcar.xml)
-
上半部分:地图显示
-
使用
MapView
控件显示百度地图; -
高度占父容器的50%(通过
layout_weight="1"
实现); -
宽度占满,用于可视化停车位置。
-
-
下半部分:功能区域
-
使用嵌套
LinearLayout
垂直布局,高度同样占50%; -
包含三个子控件:
-
ImageView
:显示拍照记录的图片,固定大小,背景灰色; -
Button
:点击拍照并记录位置; -
TextView
:显示当前选中位置的坐标信息,文字居中。
-
-
<?xml version="1.0" encoding="utf-8"?>
<!-- 找车功能页面布局 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- 页面标题 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="停车找车(activity_findcar.xml)"
android:textSize="8sp"
android:gravity="center"
android:padding="16dp"/>
<!-- 上半部分:地图(占50%) -->
<!-- 百度地图显示控件 -->
<com.baidu.mapapi.map.MapView
android:id="@+id/mapView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<!-- 下半部分:图片和按钮(占50%) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical"
android:padding="16dp"
android:gravity="center">
<!-- 图片显示区域 -->
<ImageView
android:id="@+id/photoImageView"
android:layout_width="220dp"
android:layout_height="160dp"
android:background="@color/grey"
android:scaleType="centerCrop"/>
<!-- 拍照按钮 -->
<Button
android:id="@+id/takePhotoButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="拍照记录位置"
android:textSize="16sp" />
<!-- 显示坐标信息 -->
<TextView
android:id="@+id/locationInfoText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="14sp"
android:gravity="center" />
</LinearLayout>
</LinearLayout>
主要功能实现(FindCarActivity.java)
类与成员变量定义
主要是相关的变量,和使用的控件。
/**
* 找车功能Activity类
* 用于实现停车找车功能,包括拍照记录停车位置和在地图上显示位置
*/
public class FindCarActivity extends AppCompatActivity {
private static final String TAG = "FindCarActivity";
private static final String PREFS_NAME = "FindCarPrefs";
private static final String KEY_IS_PARKING_RECORDED = "is_parking_recorded";
private static final String KEY_PARKING_LATITUDE = "parking_latitude";
private static final String KEY_PARKING_LONGITUDE = "parking_longitude";
private static final String KEY_PARKING_ADDRESS = "parking_address";
// 百度地图控件
private MapView mMapView;
private BaiduMap mBaiduMap;
// 定位相关
private LocationClient mLocationClient;
private boolean isLocationComplete = false; // 定位是否完成
// UI组件
private Button takePhotoButton;
private ImageView photoImageView;
private TextView locationInfoText;
// 记录的停车位置
private LatLng parkingLocation;
// 拍照相关
private static final int REQUEST_IMAGE_CAPTURE = 1;
private static final int REQUEST_CAMERA_PERMISSION = 100;
private static final int LOCATION_PERMISSION_REQUEST_CODE = 1001;
private String currentPhotoPath;
// SharedPreferences用于保存停车位置
private SharedPreferences sharedPreferences;
private boolean isFirstTime = true;
// 其他方法省略
}
初始化与核心方法
地图初始化,显示、定位、显示位置与前面首页是一样的,不再赘述。
这里主要是拍照功能的实现,调用顺序,和实现的逻辑。
如果是第一次记录,才会执行下面的操作。如果已经有过记录,直接显示之前的记录。
/**
* Activity创建时调用的方法
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_findcar);
// 初始化SharedPreferences
sharedPreferences = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
// 检查是否已经记录过停车位置
isFirstTime = !sharedPreferences.getBoolean(KEY_IS_PARKING_RECORDED, false);
// 初始化UI组件
initUI();
// 初始化百度地图控件
initBaiduMap();
// 如果不是第一次进入,直接显示之前记录的位置
if (!isFirstTime) {
showSavedParkingLocation();
}
}
/**
* 拍照并记录当前位置
*/
private void takePhotoAndRecordLocation() {
// 检查相机权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
// 请求相机权限
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.CAMERA},
REQUEST_CAMERA_PERMISSION);
} else {
// 启动拍照
dispatchTakePictureIntent();
}
}
定位监听器与停车位置记录
-
监听定位结果
-
防止重复记录:通过
isLocationComplete
标志确保只记录一次停车位置。 -
显示当前定位:将定位结果通过
MyLocationData
更新到地图上,显示蓝点。 -
地图标记与聚焦:使用自定义图标(car_marker)在地图上打点,并将地图中心移至该位置,缩放至18级。
-
持久化存储:将停车位置和地址保存到
SharedPreferences
,实现退出页面不丢失。 -
更新UI提示用户:在界面显示位置信息,并弹出 Toast 提示“已记录停车位置”。
-
停止定位节省资源:记录完成后立即停止定位服务。
/**
* 定位监听器
*/
public class MyLocationListener extends BDAbstractLocationListener {
@Override
public void onReceiveLocation(BDLocation location) {
// 检查定位是否已完成
if (isLocationComplete) {
return;
}
if (location != null && mMapView != null && mBaiduMap != null) {
int locType = location.getLocType();
double latitude = location.getLatitude();
double longitude = location.getLongitude();
String addrStr = location.getAddrStr();
// 检查定位类型是否有效
if (locType == BDLocation.TypeGpsLocation ||
locType == BDLocation.TypeNetWorkLocation ||
locType == BDLocation.TypeOffLineLocation ||
locType == BDLocation.TypeCacheLocation) {
// 设置定位数据到地图上
MyLocationData locData = new MyLocationData.Builder()
.accuracy(location.getRadius())
.direction(location.getDirection()).latitude(latitude)
.longitude(longitude).build();
mBaiduMap.setMyLocationData(locData);
// 保存停车位置
parkingLocation = new LatLng(latitude, longitude);
// 在地图上标记停车位置
markParkingLocation(latitude, longitude);
// 保存停车位置到SharedPreferences
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean(KEY_IS_PARKING_RECORDED, true);
editor.putFloat(KEY_PARKING_LATITUDE, (float) latitude);
editor.putFloat(KEY_PARKING_LONGITUDE, (float) longitude);
if (addrStr != null) {
editor.putString(KEY_PARKING_ADDRESS, addrStr);
}
editor.apply();
// 更新UI显示位置信息
String locationInfo = "停车位置:\n纬度: " + latitude + "\n经度: " + longitude;
if (addrStr != null && !addrStr.isEmpty()) {
locationInfo += "\n地址: " + addrStr;
}
locationInfoText.setText(locationInfo);
// 显示提示信息
Toast.makeText(FindCarActivity.this, "已记录停车位置", Toast.LENGTH_SHORT).show();
// 设置定位完成标志
isLocationComplete = true;
isFirstTime = false;
// 停止定位以节省电量
if (mLocationClient != null && mLocationClient.isStarted()) {
mLocationClient.stop();
}
} else {
Log.e(TAG, "定位失败,错误码: " + locType);
Toast.makeText(FindCarActivity.this, "定位失败,请检查GPS设置,错误码: " + locType, Toast.LENGTH_SHORT).show();
}
}
}
}
/**
* 在地图上标记停车位置
*/
private void markParkingLocation(double latitude, double longitude) {
// 创建位置点
LatLng point = new LatLng(latitude, longitude);
// 构建Marker图标
MarkerOptions markerOptions = new MarkerOptions()
.position(point)
.icon(BitmapDescriptorFactory.fromResource(R.drawable.car_marker)); // 使用自定义的车辆图标
// 在地图上添加标记
mBaiduMap.addOverlay(markerOptions);
// 移动地图中心到标记位置
MapStatus.Builder builder = new MapStatus.Builder();
builder.target(point).zoom(18.0f);
mBaiduMap.animateMapStatus(MapStatusUpdateFactory.newMapStatus(builder.build()));
}
拍照相关方法
-
启动拍照:
dispatchTakePictureIntent()
创建拍照 Intent(Intent
是一个消息传递对象,可用于请求操作),检查相机可用后,调用createImageFile()
创建照片文件,并通过FileProvider
安全传递文件路径,启动相机。 -
创建照片文件:
createImageFile()
在应用专属目录下生成带时间戳的临时 JPG 文件,并保存路径到currentPhotoPath
。 -
显示照片缩略图:
setPic()
计算图片缩放比例,按 ImageView 尺寸加载合适大小的 Bitmap,并设置到photoImageView
显示。
/**
* 启动拍照Intent
*/
private void dispatchTakePictureIntent() {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
// 创建用于保存照片的文件
File photoFile = null;
try {
photoFile = createImageFile();
} catch (IOException ex) {
Toast.makeText(this, "创建照片文件失败", Toast.LENGTH_SHORT).show();
return;
}
if (photoFile != null) {
Uri photoURI = FileProvider.getUriForFile(this,
"com.example.hospitalnavi.fileprovider",
photoFile);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);
}
}
}
/**
* 创建用于保存照片的文件
*/
private File createImageFile() throws IOException {
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String imageFileName = "JPEG_" + timeStamp + "_";
File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File image = File.createTempFile(
imageFileName, /* 前缀 */
".jpg", /* 后缀 */
storageDir /* 目录 */
);
currentPhotoPath = image.getAbsolutePath();
return image;
}
/**
* 显示照片缩略图
*/
private void setPic() {
// 获取ImageView的尺寸
int targetW = photoImageView.getWidth();
int targetH = photoImageView.getHeight();
// 获取照片的尺寸
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
bmOptions.inJustDecodeBounds = true;
BitmapFactory.decodeFile(currentPhotoPath, bmOptions);
int photoW = bmOptions.outWidth;
int photoH = bmOptions.outHeight;
// 计算缩放比例
int scaleFactor = Math.min(photoW / targetW, photoH / targetH);
// 解码照片文件
bmOptions.inJustDecodeBounds = false;
bmOptions.inSampleSize = scaleFactor;
Bitmap bitmap = BitmapFactory.decodeFile(currentPhotoPath, bmOptions);
photoImageView.setImageBitmap(bitmap);
}
生命周期管理
/**
* Activity恢复时调用
*/
@Override
protected void onResume() {
super.onResume();
if (mMapView != null) {
mMapView.onResume();
}
}
/**
* Activity暂停时调用
*/
@Override
protected void onPause() {
super.onPause();
if (mMapView != null) {
mMapView.onPause();
}
}
/**
* Activity销毁时调用
*/
@Override
protected void onDestroy() {
super.onDestroy();
// 停止并销毁定位客户端
if (mLocationClient != null) {
mLocationClient.stop();
mLocationClient = null;
}
if (mMapView != null) {
mBaiduMap.setMyLocationEnabled(false); // 关闭定位图层
mMapView.onDestroy();
}
}