[开源] 用了安卓才发现,我只能自己写个备忘录了

希米  (UID: 5950) [复制链接]
帖子链接已复制到剪贴板
帖子已经有人评论啦,不支持删除!

189 9

前言

最近从 iPhone 换到安卓,发现原生系统里居然没自带。去应用商店翻了几页,不是满屏广告就是非要联网登录,用着根本不放心。考虑到备忘录都是隐私,干脆别找了,自己写一个吧。

过程

说实话,配置android studio 环境确实折腾,Ai提供的方法都是老版本,官网下载最新版本很多功能菜单对不上,主要是英文太烂了。但写代码的过程就比较快了前后就半小时。我、把需求丢给 AI,它直接生成了完整的逻辑和界面代码。从环境装好到最终编译出 APK 安装到手机,体验非常顺畅。AI 的效率确实没得说。

软件功能

没搞什么花哨的功能,风格类似iphone的,简单明了,主打一个省心:

  • 分类管理:文件夹可以随时新建、重命名、删除,分类逻辑很直接。
  • 本地存储:数据全存在手机本地,没有任何联网权限,不用担心隐私上传到服务器。
  • 增删改查:笔记的写、改、删逻辑都做好了,满足日常随手记的需求。
  • 交互简洁:界面就是简单的 Mac 风格,没乱七八糟的干扰。

预览

[开源] 用了安卓才发现,我只能自己写个备忘录了
[开源] 用了安卓才发现,我只能自己写个备忘录了
[开源] 用了安卓才发现,我只能自己写个备忘录了

结语

以前觉得写 App 是程序员的专属领域,离我这种小白挺远的。现在发现有了 AI 帮忙,只要心里有想法,几分钟就能搞定一个。以后再遇到什么工具不好用,别再去外面找来找去了,自己动手改个顺手的,这感觉真的比什么都强。

下载与源码

如果你也想试试这款简洁的备忘录,或者想在此基础新增功能,也可以直接下载源码修改编译:

APK:直接安装 APK (约 9MB,下载后解压安装即可)

源代码:项目源代码(约 182KB,直接用 Android Studio 打开)

 

注:下载链接禁用了迅雷等第三方工具下载,浏览器本地下载不受影响!

温馨提示: 这软件就是个本地笔记,没写联网和定位代码,纯粹为了自用。安装时遇到获取应用列表的权限,直接点“拒绝”就行,没任何影响。

关于

  1. 作者:希米
  2. 原文: https://www.ximi.me/post-6041.html
  3. 最后更新:2026-06-10

核心代码:

package com.example.mynotepad

import android.content.Context
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.json.JSONArray
import org.json.JSONObject
import java.util.UUID

// --- 数据模型 ---
data class Note(val id: String = UUID.randomUUID().toString(), var title: String, var content: String)
// 这里的 val name 改成了 var name,以支持重命名
data class Folder(val id: String = UUID.randomUUID().toString(), var name: String, val notes: MutableList<Note> = mutableListOf())

// --- 导航屏幕状态 ---
sealed class Screen {
    object FolderList : Screen()
    data class NoteList(val folderId: String) : Screen()
    data class NoteEditor(val folderId: String, val noteId: String?) : Screen()
}

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme(
                colorScheme = lightColorScheme(
                    primary = Color(0xFFEAA83A), // 经典 Mac 备忘录琥珀黄
                    background = Color(0xFFF6F5F0), // 仿纸质温暖灰白底色
                    surface = Color(0xFFFFFFFF)
                )
            ) {
                MacNotesApp()
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MacNotesApp() {
    val context = LocalContext.current
    var folders by remember { mutableStateOf(listOf<Folder>()) }
    var currentScreen by remember { mutableStateOf<Screen>(Screen.FolderList) }

    // 弹窗与状态控制
    var showFolderDialog by remember { mutableStateOf(false) }
    var newFolderName by remember { mutableStateOf("") }

    // 目录管理的全新状态
    var expandedMenuFolderId by remember { mutableStateOf<String?>(null) } // 控制哪个文件夹的下拉菜单展开
    var folderToEdit by remember { mutableStateOf<Folder?>(null) } // 当前选中的文件夹
    var showRenameDialog by remember { mutableStateOf(false) }
    var editFolderName by remember { mutableStateOf("") }
    var showDeleteDialog by remember { mutableStateOf(false) }

    // 初始化读取
    LaunchedEffect(Unit) {
        folders = loadDataFromLocal(context)
    }

    // 保存方法
    val saveToLocal = {
        saveDataToLocal(context, folders)
    }

    // 物理返回键接管
    BackHandler(enabled = currentScreen !is Screen.FolderList) {
        currentScreen = when (val screen = currentScreen) {
            is Screen.NoteList -> Screen.FolderList
            is Screen.NoteEditor -> Screen.NoteList(screen.folderId)
            else -> Screen.FolderList
        }
    }

    Scaffold(
        topBar = {
            TopAppBar(
                title = {
                    Text(
                        text = when (val screen = currentScreen) {
                            is Screen.FolderList -> "文件夹"
                            is Screen.NoteList -> folders.find { it.id == screen.folderId }?.name ?: "备忘录"
                            is Screen.NoteEditor -> "编辑备忘录"
                        },
                        fontWeight = FontWeight.Bold,
                        color = Color(0xFF333333)
                    )
                },
                navigationIcon = {
                    if (currentScreen !is Screen.FolderList) {
                        IconButton(onClick = {
                            currentScreen = when (val screen = currentScreen) {
                                is Screen.NoteList -> Screen.FolderList
                                is Screen.NoteEditor -> Screen.NoteList(screen.folderId)
                                else -> Screen.FolderList
                            }
                        }) {
                            Text("◀", color = Color(0xFFEAA83A), fontSize = 16.sp, fontWeight = FontWeight.Bold)
                        }
                    }
                },
                actions = {
                    // 如果处于已有备忘录的编辑页面,右上角显示“垃圾桶”按钮
                    if (currentScreen is Screen.NoteEditor) {
                        val editorScreen = currentScreen as Screen.NoteEditor
                        if (editorScreen.noteId != null) {
                            IconButton(onClick = {
                                val currentFolder = folders.find { it.id == editorScreen.folderId }
                                currentFolder?.notes?.removeIf { it.id == editorScreen.noteId }
                                saveToLocal()
                                Toast.makeText(context, "已删除备忘录", Toast.LENGTH_SHORT).show()
                                currentScreen = Screen.NoteList(editorScreen.folderId)
                            }) {
                                Text("🗑️", fontSize = 18.sp)
                            }
                        }
                    }
                },
                colors = TopAppBarDefaults.topAppBarColors(containerColor = Color(0xFFF6F5F0))
            )
        }
    ) { paddingValues ->
        Box(modifier = Modifier.padding(paddingValues).fillMaxSize().background(Color(0xFFF6F5F0))) {
            when (val screen = currentScreen) {
                // 1. 文件夹列表界面
                is Screen.FolderList -> {
                    Column(modifier = Modifier.fillMaxSize()) {
                        LazyColumn(modifier = Modifier.weight(1f).padding(horizontal = 16.dp)) {
                            items(folders) { folder ->
                                Card(
                                    modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp).clickable {
                                        currentScreen = Screen.NoteList(folder.id)
                                    },
                                    colors = CardDefaults.cardColors(containerColor = Color.White),
                                    shape = RoundedCornerShape(10.dp)
                                ) {
                                    Row(
                                        modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
                                        verticalAlignment = Alignment.CenterVertically
                                    ) {
                                        Text("📁", fontSize = 20.sp)
                                        Spacer(modifier = Modifier.width(12.dp))
                                        Text(text = folder.name, modifier = Modifier.weight(1f), fontSize = 16.sp, fontWeight = FontWeight.Medium)
                                        Text(text = "${folder.notes.size}", color = Color.Gray, fontSize = 14.sp)

                                        // 【新增功能】:目录的更多操作按钮(三个点)
                                        Box {
                                            IconButton(onClick = { expandedMenuFolderId = folder.id }) {
                                                Text("⋮", color = Color.Gray, fontSize = 20.sp, fontWeight = FontWeight.Bold)
                                            }
                                            DropdownMenu(
                                                expanded = expandedMenuFolderId == folder.id,
                                                onDismissRequest = { expandedMenuFolderId = null },
                                                modifier = Modifier.background(Color.White)
                                            ) {
                                                DropdownMenuItem(
                                                    text = { Text("📝 重命名") },
                                                    onClick = {
                                                        expandedMenuFolderId = null
                                                        folderToEdit = folder
                                                        editFolderName = folder.name
                                                        showRenameDialog = true
                                                    }
                                                )
                                                DropdownMenuItem(
                                                    text = { Text("🗑️ 删除", color = Color.Red) },
                                                    onClick = {
                                                        expandedMenuFolderId = null
                                                        folderToEdit = folder
                                                        showDeleteDialog = true
                                                    }
                                                )
                                            }
                                        }
                                    }
                                }
                            }
                        }
                        // 底部新建文件夹按钮
                        Button(
                            onClick = { showFolderDialog = true },
                            modifier = Modifier.fillMaxWidth().padding(16.dp),
                            colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFEAA83A))
                        ) {
                            Text("新建文件夹", color = Color.White)
                        }
                    }
                }

                // 2. 指定文件夹内的记事列表界面
                is Screen.NoteList -> {
                    val currentFolder = folders.find { it.id == screen.folderId }
                    val notes = currentFolder?.notes ?: listOf()

                    Column(modifier = Modifier.fillMaxSize()) {
                        if (notes.isEmpty()) {
                            Box(modifier = Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) {
                                Text("该文件夹下没有备忘录", color = Color.Gray)
                            }
                        } else {
                            LazyColumn(modifier = Modifier.weight(1f).padding(horizontal = 16.dp)) {
                                items(notes) { note ->
                                    Card(
                                        modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp).clickable {
                                            currentScreen = Screen.NoteEditor(screen.folderId, note.id)
                                        },
                                        colors = CardDefaults.cardColors(containerColor = Color.White),
                                        shape = RoundedCornerShape(10.dp)
                                    ) {
                                        Column(modifier = Modifier.padding(16.dp)) {
                                            Text(text = note.title.ifEmpty { "无标题" }, fontWeight = FontWeight.Bold, fontSize = 16.sp)
                                            Spacer(modifier = Modifier.height(4.dp))
                                            Text(text = note.content.ifEmpty { "没有其他文本" }, color = Color.Gray, maxLines = 1, fontSize = 14.sp)
                                        }
                                    }
                                }
                            }
                        }
                        // 新建笔记按钮
                        Button(
                            onClick = { currentScreen = Screen.NoteEditor(screen.folderId, null) },
                            modifier = Modifier.fillMaxWidth().padding(16.dp),
                            colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFEAA83A))
                        ) {
                            Text("✍️ 撰写新备忘录", color = Color.White)
                        }
                    }
                }

                // 3. 记事编辑器界面
                is Screen.NoteEditor -> {
                    val currentFolder = folders.find { it.id == screen.folderId }
                    val existingNote = currentFolder?.notes?.find { it.id == screen.noteId }

                    var title by remember { mutableStateOf(existingNote?.title ?: "") }
                    var content by remember { mutableStateOf(existingNote?.content ?: "") }

                    Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
                        OutlinedTextField(
                            value = title,
                            onValueChange = { title = it },
                            placeholder = { Text("标题", fontSize = 20.sp, fontWeight = FontWeight.Bold) },
                            modifier = Modifier.fillMaxWidth(),
                            colors = OutlinedTextFieldDefaults.colors(
                                focusedBorderColor = Color.Transparent,
                                unfocusedBorderColor = Color.Transparent
                            ),
                            textStyle = LocalTextStyle.current.copy(fontSize = 20.sp, fontWeight = FontWeight.Bold)
                        )
                        HorizontalDivider(color = Color(0xFFE5E5E5), thickness = 1.dp, modifier = Modifier.padding(vertical = 8.dp))
                        OutlinedTextField(
                            value = content,
                            onValueChange = { content = it },
                            placeholder = { Text("开始输入内容...") },
                            modifier = Modifier.fillMaxWidth().weight(1f),
                            colors = OutlinedTextFieldDefaults.colors(
                                focusedBorderColor = Color.Transparent,
                                unfocusedBorderColor = Color.Transparent
                            )
                        )
                        Button(
                            onClick = {
                                if (existingNote != null) {
                                    existingNote.title = title
                                    existingNote.content = content
                                } else {
                                    // 仅当用户输入了内容或标题时才保存,避免产生空白笔记
                                    if (title.isNotBlank() || content.isNotBlank()) {
                                        currentFolder?.notes?.add(0, Note(title = title, content = content))
                                    }
                                }
                                saveToLocal()
                                Toast.makeText(context, "已存储到本地", Toast.LENGTH_SHORT).show()
                                currentScreen = Screen.NoteList(screen.folderId)
                            },
                            modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
                            colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFEAA83A))
                        ) {
                            Text("完成并保存", color = Color.White)
                        }
                    }
                }
            }
        }
    }

    // --- 所有弹窗组件 ---

    // 1. 新建文件夹弹窗
    if (showFolderDialog) {
        AlertDialog(
            onDismissRequest = { showFolderDialog = false },
            title = { Text("新建文件夹") },
            text = {
                OutlinedTextField(
                    value = newFolderName,
                    onValueChange = { newFolderName = it },
                    label = { Text("文件夹名称") }
                )
            },
            confirmButton = {
                TextButton(onClick = {
                    if (newFolderName.isNotBlank()) {
                        folders = folders + Folder(name = newFolderName)
                        saveToLocal()
                        newFolderName = ""
                        showFolderDialog = false
                    }
                }) { Text("创建", color = Color(0xFFEAA83A)) }
            },
            dismissButton = {
                TextButton(onClick = { showFolderDialog = false }) { Text("取消", color = Color.Gray) }
            }
        )
    }

    // 2. 重命名文件夹弹窗
    if (showRenameDialog && folderToEdit != null) {
        AlertDialog(
            onDismissRequest = { showRenameDialog = false },
            title = { Text("重命名") },
            text = {
                OutlinedTextField(
                    value = editFolderName,
                    onValueChange = { editFolderName = it },
                    label = { Text("新名称") }
                )
            },
            confirmButton = {
                TextButton(onClick = {
                    if (editFolderName.isNotBlank()) {
                        // 更新内存数据
                        folderToEdit?.name = editFolderName
                        // 强制触发重组以更新UI
                        folders = folders.toList()
                        saveToLocal()
                        showRenameDialog = false
                    }
                }) { Text("保存", color = Color(0xFFEAA83A)) }
            },
            dismissButton = {
                TextButton(onClick = { showRenameDialog = false }) { Text("取消", color = Color.Gray) }
            }
        )
    }

    // 3. 删除文件夹防误触确认弹窗
    if (showDeleteDialog && folderToEdit != null) {
        AlertDialog(
            onDismissRequest = { showDeleteDialog = false },
            title = { Text("删除文件夹?") },
            text = { Text("确定要删除「${folderToEdit?.name}」吗?\n警告:这将会一并删除该文件夹下的所有备忘录,且无法恢复。") },
            confirmButton = {
                TextButton(onClick = {
                    folders = folders.filter { it.id != folderToEdit?.id }
                    saveToLocal()
                    showDeleteDialog = false
                }) { Text("确认删除", color = Color.Red) }
            },
            dismissButton = {
                TextButton(onClick = { showDeleteDialog = false }) { Text("取消", color = Color.Gray) }
            }
        )
    }
}

// --- JSON 文件读取与存储逻辑 ---
private fun saveDataToLocal(context: Context, folders: List<Folder>) {
    try {
        val rootArray = JSONArray()
        for (f in folders) {
            val fObj = JSONObject().apply {
                put("id", f.id)
                put("name", f.name)
                val nArray = JSONArray()
                for (n in f.notes) {
                    val nObj = JSONObject().apply {
                        put("id", n.id)
                        put("title", n.title)
                        put("content", n.content)
                    }
                    nArray.put(nObj)
                }
                put("notes", nArray)
            }
            rootArray.put(fObj)
        }
        context.openFileOutput("notes_storage.json", Context.MODE_PRIVATE).use {
            it.write(rootArray.toString().toByteArray())
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

private fun loadDataFromLocal(context: Context): List<Folder> {
    val list = mutableListOf<Folder>()
    try {
        val text = context.openFileInput("notes_storage.json").use { stream ->
            stream.bufferedReader().use { it.readText() }
        }
        val rootArray = JSONArray(text)
        for (i in 0 until rootArray.length()) {
            val fObj = rootArray.getJSONObject(i)
            val folder = Folder(
                id = fObj.getString("id"),
                name = fObj.getString("name"),
                notes = mutableListOf()
            )
            val nArray = fObj.getJSONArray("notes")
            for (j in 0 until nArray.length()) {
                val nObj = nArray.getJSONObject(j)
                folder.notes.add(
                    Note(
                        id = nObj.getString("id"),
                        title = nObj.getString("title"),
                        content = nObj.getString("content")
                    )
                )
            }
            list.add(folder)
        }
    } catch (e: Exception) {
        list.add(Folder(name = "我的备忘"))
    }
    return list
}
博客:www.ximi.me
已有评论 ( 9 )
提示:您必须 登录 才能查看此内容。
域名市场
   域名载入中...
创建新帖
自助推广 (点击空位或 这里 添加)
确认删除
确定要删除这篇帖子吗?删除后将无法恢复。
删除成功
帖子已成功删除,页面将自动刷新。
删除失败
删除帖子时发生错误,请稍后再试。