2014年12月3日14:42:40
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
查看照片
![]()
播放视频
![]()
by tops
一、需求分析
一个记事本,能够输入标题和内容,创建日期、最新修改日期等信息。
如果没有输入标题则使用内容的第一句话作为标题,创建日期和修改日期均由系统自动生成,无需用户干预。
提供笔记列表,列表中笔记展示位标题、创建日期/修改日期
高级的可以给笔记添加照片或视频,这既可以自己拍摄也可以添加手机中已有的视频。
二、可行性分析
技术可行,经济可行,作为练习使用
技术方面主要用到SQLite,listview、Intent等知识点。
技术方面主要用到SQLite,listview、Intent等知识点。
三、编写项目计划书
项目功能模块划分
打开应用的第一个页面用于展示已有的笔记列表,列表中的每一个笔记条目都可以点击,点击之后呈现此笔记的完整内容/编辑页面,列表下方有一个添加笔记的按钮,
开发周期
开发人员安排及工作分配
四、系统设计-功能结构设计,业务流程设计
uml建模工具的使用
新建笔记
点击添加笔记按钮→打开编辑笔记页面→
用户分别在标题栏和内容栏输入内容;
点击添加视频时,打开系统录像拍摄视频并保存,然后在多媒体列表中显示视频图片、文件名称、路径;
点击添加图像时,打开照相机拍摄图片并保存,然后在多媒体列表中显示图像图片、文件名称、路径;
→点击保存按钮,将笔记和多媒体信息保存到数据库;
→点击取消按钮,关闭当前页面,返回主页面/笔记列表页面。
修改笔记
点击笔记列表中的笔记时,打开编辑笔记页面,并传入当前笔记的信息,在编辑页面有用户对笔记操作,跟新建笔记的操作相同
删除笔记
选择已有笔记,进行数据库删除操作。
保存笔记
将编辑页面里的笔记信息存入到笔记数据库表中,多媒体信息存入到多媒体数据库表中
五、数据库设计
笔记表-notes
id- Integer型、主键、自动增加 INTEGER PRIMARY KEY AUTOINCREMENT,
name- text型,不为空,默认为“” TEXT NOT NULL DEFAULT \"\",
content,text型,不为空,默认为“” TEXT NOT NULL DEFAULT \"\",
date,text型,不为空,默认为“” TEXT NOT NULL DEFAULT \"\",
多媒体信息表-media
id-Integer型、主键、自动增加 INTEGER PRIMARY KEY AUTOINCREMENT,
path- text型,不为空,默认为“” TEXT NOT NULL DEFAULT \"\",
note_id- Integer型,不为空,默认为0 INTEGER NOT NULL DEFAULT 0
六、架构设计
模块与模块之间的通信机制,MVC、
视图层,使用LinearLayout,列表使用ListView
项目工程结构:
七、代码开发及工作分配
主页面/笔记列表显示页面的布局代码:
/Notes/res/layout/activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"tools:context=".MainActivity" ><ListViewandroid:id="@android:id/list"android:layout_width="fill_parent"android:layout_height="wrap_content"android:layout_weight="1" ></ListView><Buttonandroid:id="@+id/btnAddNote"android:layout_width="fill_parent"android:layout_height="wrap_content"android:text="添加日志" /></LinearLayout>
编辑笔记页面的布局代码:
/Notes/res/layout/aty_eidt_note.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="match_parent"android:orientation="vertical" ><EditTextandroid:id="@+id/etName"android:layout_width="match_parent"android:layout_height="wrap_content"android:ems="10"android:singleLine="true" ><requestFocus /></EditText><EditTextandroid:id="@+id/etContent"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_weight="1"android:ems="10"android:gravity="top" /><ListViewandroid:id="@android:id/list"android:layout_width="fill_parent"android:layout_height="wrap_content"android:layout_weight="2" ></ListView><LinearLayoutandroid:layout_width="fill_parent"android:layout_height="wrap_content" ><Buttonandroid:id="@+id/btnSave"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_weight="1"android:text="保存" /><Buttonandroid:id="@+id/btnCancel"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_weight="1"android:text="取消" /><Buttonandroid:id="@+id/btnAddPhoto"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_weight="1"android:text="拍照" /><Buttonandroid:id="@+id/btnAddVideo"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="录像"android:layout_weight="1" /></LinearLayout></LinearLayout>
显示多媒体列表的条目布局:
/Notes/res/layout/media_list_cell.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="match_parent"android:orientation="horizontal"android:gravity="center_vertical" ><ImageViewandroid:id="@+id/ivIcon"android:layout_width="80dp"android:layout_height="80dp" /><TextViewandroid:id="@+id/tvPath"android:layout_width="match_parent"android:layout_height="wrap_content"android:textAppearance="?android:attr/textAppearanceLarge" /></LinearLayout>
显示笔记列表的条目布局:
/Notes/res/layout/notes_list_cell.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="match_parent"android:orientation="vertical" ><TextViewandroid:id="@+id/tvName"android:layout_width="match_parent"android:layout_height="wrap_content"android:textAppearance="?android:attr/textAppearanceLarge" /><TextViewandroid:id="@+id/tvDate"android:layout_width="match_parent"android:layout_height="wrap_content" /></LinearLayout>
主页面/笔记列表显示页面的java代码
/Notes/src/com/tops/notes/MainActivity.java
package com.tops.notes;import com.tops.notes.db.NotesDB;import android.app.Activity;import android.app.ListActivity;import android.content.Intent;import android.database.Cursor;import android.database.sqlite.SQLiteDatabase;import android.os.Bundle;import android.view.Menu;import android.view.View;import android.view.View.OnClickListener;import android.widget.ListView;import android.widget.SimpleCursorAdapter;/*** 继承ListActivity的Activity,呈现已经存在的日志和添加日志按钮** @author TOPS**/public class MainActivity extends ListActivity {private SimpleCursorAdapter adapter = null;private NotesDB db;private SQLiteDatabase dbRead;public static final int REQUEST_CODE_ADD_NOTE = 1;public static final int REQUEST_CODE_EDIT_NOTE = 2;/*** 实现OnClickListener接口,添加日志按钮的监听*/private OnClickListener btnAddNote_clickHandler = new OnClickListener() {@Overridepublic void onClick(View v) {// 有返回结果的开启编辑日志的Activity,// requestCode If >= 0, this code will be returned// in onActivityResult() when the activity exits.startActivityForResult(new Intent(MainActivity.this,AtyEditNote.class), REQUEST_CODE_ADD_NOTE);}};@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);// 操作数据库db = new NotesDB(this);dbRead = db.getReadableDatabase();// 查询数据库并将数据显示在ListView上。// 建议使用CursorLoader,这个操作因为在UI线程,容易引起无响应错误adapter = new SimpleCursorAdapter(this, R.layout.notes_list_cell, null,new String[] { NotesDB.COLUMN_NAME_NOTE_NAME,NotesDB.COLUMN_NAME_NOTE_DATE }, new int[] {R.id.tvName, R.id.tvDate });setListAdapter(adapter);refreshNotesListView();findViewById(R.id.btnAddNote).setOnClickListener(btnAddNote_clickHandler);}/*** 复写方法,笔记列表中的笔记条目被点击时被调用,打开编辑笔记页面,同事传入当前笔记的信息*/@Overrideprotected void onListItemClick(ListView l, View v, int position, long id) {// 获取当前笔记条目的Cursor对象Cursor c = adapter.getCursor();c.moveToPosition(position);// 显式Intent开启编辑笔记页面Intent i = new Intent(MainActivity.this, AtyEditNote.class);// 传入笔记id,name,contenti.putExtra(AtyEditNote.EXTRA_NOTE_ID,c.getInt(c.getColumnIndex(NotesDB.COLUMN_NAME_ID)));i.putExtra(AtyEditNote.EXTRA_NOTE_NAME,c.getString(c.getColumnIndex(NotesDB.COLUMN_NAME_NOTE_NAME)));i.putExtra(AtyEditNote.EXTRA_NOTE_CONTENT,c.getString(c.getColumnIndex(NotesDB.COLUMN_NAME_NOTE_CONTENT)));// 有返回的开启ActivitystartActivityForResult(i, REQUEST_CODE_EDIT_NOTE);super.onListItemClick(l, v, position, id);}/*** Called when an activity you launched exits, giving you the requestCode* you started it with 当被开启的Activity存在并返回结果时调用的方法** 当从编辑笔记页面返回时调用,刷新笔记列表*/@Overrideprotected void onActivityResult(int requestCode, int resultCode, Intent data) {switch (requestCode) {case REQUEST_CODE_ADD_NOTE:case REQUEST_CODE_EDIT_NOTE:if (resultCode == Activity.RESULT_OK) {refreshNotesListView();}break;default:break;}super.onActivityResult(requestCode, resultCode, data);}/*** 刷新笔记列表,内容从数据库中查询*/public void refreshNotesListView() {/*** Change the underlying cursor to a new cursor. If there is an existing* cursor it will be closed.** Parameters: cursor The new cursor to be used*/adapter.changeCursor(dbRead.query(NotesDB.TABLE_NAME_NOTES, null, null,null, null, null, null));}}
编辑笔记页面的java代码:
/Notes/src/com/tops/notes/AtyEditNote.java
package com.tops.notes;import java.io.File;import java.io.IOException;import java.text.SimpleDateFormat;import java.util.ArrayList;import java.util.Date;import java.util.List;import android.app.ListActivity;import android.content.ContentValues;import android.content.Context;import android.content.Intent;import android.database.Cursor;import android.database.sqlite.SQLiteDatabase;import android.net.Uri;import android.os.Bundle;import android.os.Environment;import android.provider.MediaStore;import android.view.LayoutInflater;import android.view.View;import android.view.View.OnClickListener;import android.view.ViewGroup;import android.widget.BaseAdapter;import android.widget.EditText;import android.widget.ImageView;import android.widget.ListView;import android.widget.TextView;import com.tops.notes.db.NotesDB;public class AtyEditNote extends ListActivity {private int noteId = -1;private EditText etName, etContent;private MediaAdapter adapter;private NotesDB db;private SQLiteDatabase dbRead, dbWrite;private String currentPath = null;public static final int REQUEST_CODE_GET_PHOTO = 1;public static final int REQUEST_CODE_GET_VIDEO = 2;public static final String EXTRA_NOTE_ID = "noteId";public static final String EXTRA_NOTE_NAME = "noteName";public static final String EXTRA_NOTE_CONTENT = "noteContent";/*** 按钮点击的监听器,实现OnClickListener接口*/private OnClickListener btnClickHandler = new OnClickListener() {Intent i;File f;@Overridepublic void onClick(View v) {switch (v.getId()) {case R.id.btnAddPhoto:// 添加照片按钮// 使用Intent调用系统照相机,传入图像保存路径和名称i = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);f = new File(getMediaDir(), System.currentTimeMillis() + ".jpg");if (!f.exists()) {try {f.createNewFile();} catch (IOException e) {e.printStackTrace();}}currentPath = f.getAbsolutePath();i.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(f));startActivityForResult(i, REQUEST_CODE_GET_PHOTO);break;case R.id.btnAddVideo:// 添加视频按钮// 使用Intent调用系统录像器,传入视频保存路径和名称i = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);f = new File(getMediaDir(), System.currentTimeMillis() + ".mp4");if (!f.exists()) {try {f.createNewFile();} catch (IOException e) {e.printStackTrace();}}currentPath = f.getAbsolutePath();i.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(f));startActivityForResult(i, REQUEST_CODE_GET_VIDEO);break;case R.id.btnSave:// 保存按钮// 保存多媒体信息和笔记信息到数据库,然后关闭当前页面,返回到笔记列表页面/主页面saveMedia(saveNote());setResult(RESULT_OK);finish();break;case R.id.btnCancel:// 取消按钮// 关闭当前页面,返回到笔记列表页面/主页面setResult(RESULT_CANCELED);finish();break;default:break;}}};@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.aty_eidt_note);db = new NotesDB(this);dbRead = db.getReadableDatabase();dbWrite = db.getWritableDatabase();// 显示多媒体列表adapter = new MediaAdapter(this);setListAdapter(adapter);etName = (EditText) findViewById(R.id.etName);etContent = (EditText) findViewById(R.id.etContent);// 获取Activity传递过来的noteIdnoteId = getIntent().getIntExtra(EXTRA_NOTE_ID, -1);if (noteId > -1) {etName.setText(getIntent().getStringExtra(EXTRA_NOTE_NAME));etContent.setText(getIntent().getStringExtra(EXTRA_NOTE_CONTENT));// 查询本笔记的noteId并且检查是否有对应的多媒体,有则遍历显示在MediaList中Cursor c = dbRead.query(NotesDB.TABLE_NAME_MEDIA, null,NotesDB.COLUMN_NAME_MEDIA_OWNER_NOTE_ID + "=?",new String[] { noteId + "" }, null, null, null);while (c.moveToNext()) {adapter.add(new MediaListCellData(c.getString(c.getColumnIndex(NotesDB.COLUMN_NAME_MEDIA_PATH)), c.getInt(c.getColumnIndex(NotesDB.COLUMN_NAME_ID))));}/*** Notifies the attached observers that the underlying data has been* changed and any View reflecting the data set should refresh* itself.*/adapter.notifyDataSetChanged();}findViewById(R.id.btnSave).setOnClickListener(btnClickHandler);findViewById(R.id.btnCancel).setOnClickListener(btnClickHandler);findViewById(R.id.btnAddPhoto).setOnClickListener(btnClickHandler);findViewById(R.id.btnAddVideo).setOnClickListener(btnClickHandler);}@Overrideprotected void onListItemClick(ListView l, View v, int position, long id) {MediaListCellData data = adapter.getItem(position);Intent i;switch (data.type) {case MediaType.PHOTO:i = new Intent(this, AtyPhotoViewer.class);i.putExtra(AtyPhotoViewer.EXTRA_PATH, data.path);startActivity(i);break;case MediaType.VIDEO:i = new Intent(this, AtyVideoViewer.class);i.putExtra(AtyVideoViewer.EXTRA_PATH, data.path);startActivity(i);break;}super.onListItemClick(l, v, position, id);}@Overrideprotected void onActivityResult(int requestCode, int resultCode, Intent data) {System.out.println(data);switch (requestCode) {case REQUEST_CODE_GET_PHOTO:case REQUEST_CODE_GET_VIDEO:if (resultCode == RESULT_OK) {adapter.add(new MediaListCellData(currentPath));adapter.notifyDataSetChanged();}break;default:break;}super.onActivityResult(requestCode, resultCode, data);}/*** 获取存储Media的目录路径** @return File类型的目录路径*/public File getMediaDir() {File dir = new File(Environment.getExternalStorageDirectory(),"NotesMedia");if (!dir.exists()) {dir.mkdirs();}return dir;}/*** 保存Media信息到数据库** @param noteId*/public void saveMedia(int noteId) {MediaListCellData data;ContentValues cv;for (int i = 0; i < adapter.getCount(); i++) {data = adapter.getItem(i);if (data.id <= -1) {cv = new ContentValues();cv.put(NotesDB.COLUMN_NAME_MEDIA_PATH, data.path);cv.put(NotesDB.COLUMN_NAME_MEDIA_OWNER_NOTE_ID, noteId);dbWrite.insert(NotesDB.TABLE_NAME_MEDIA, null, cv);}}}/*** 保存日志到数据库** @return*/public int saveNote() {ContentValues cv = new ContentValues();cv.put(NotesDB.COLUMN_NAME_NOTE_NAME, etName.getText().toString());cv.put(NotesDB.COLUMN_NAME_NOTE_CONTENT, etContent.getText().toString());cv.put(NotesDB.COLUMN_NAME_NOTE_DATE, new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));if (noteId > -1) {dbWrite.update(NotesDB.TABLE_NAME_NOTES, cv, NotesDB.COLUMN_NAME_ID+ "=?", new String[] { noteId + "" });return noteId;} else {return (int) dbWrite.insert(NotesDB.TABLE_NAME_NOTES, null, cv);}}/*** 复写Activity的生命周期方法,用于关闭读写数据库的操作*/@Overrideprotected void onDestroy() {dbRead.close();dbWrite.close();super.onDestroy();}/*** 继承BaseAdapter类的MediaAdapter类,用于显示媒体信息** @author TOPS**/static class MediaAdapter extends BaseAdapter {private Context context;private List<MediaListCellData> list = new ArrayList<AtyEditNote.MediaListCellData>();public MediaAdapter(Context context) {this.context = context;}public void add(MediaListCellData data) {list.add(data);}@Overridepublic int getCount() {return list.size();}@Overridepublic MediaListCellData getItem(int position) {return list.get(position);}@Overridepublic long getItemId(int position) {return position;}@Overridepublic View getView(int position, View convertView, ViewGroup parent) {if (convertView == null) {convertView = LayoutInflater.from(context).inflate(R.layout.media_list_cell, null);}MediaListCellData data = getItem(position);ImageView ivIcon = (ImageView) convertView.findViewById(R.id.ivIcon);TextView tvPath = (TextView) convertView.findViewById(R.id.tvPath);ivIcon.setImageResource(data.iconId);tvPath.setText(data.path);return convertView;}}/*** 显示多媒体的条目类** @author TOPS**/static class MediaListCellData {int type = 0;int id = -1;String path = "";int iconId = R.drawable.ic_launcher;public MediaListCellData(String path, int id) {this(path);this.id = id;}public MediaListCellData(String path) {this.path = path;if (path.endsWith(".jpg")) {iconId = R.drawable.icon_photo;type = MediaType.PHOTO;} else if (path.endsWith(".mp4")) {iconId = R.drawable.icon_video;type = MediaType.VIDEO;}}}/*** 多媒体的种类** @author TOPS**/static class MediaType {static final int PHOTO = 1;static final int VIDEO = 2;}}
显示图片的页面:
/Notes/src/com/tops/notes/AtyPhotoViewer.java
package com.tops.notes;import java.io.File;import android.app.Activity;import android.net.Uri;import android.os.Bundle;import android.widget.ImageView;/*** 显示照片的Activity** @author TOPS**/public class AtyPhotoViewer extends Activity {private ImageView iv;public static final String EXTRA_PATH = "path";@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);iv = new ImageView(this);setContentView(iv);String path = getIntent().getStringExtra(EXTRA_PATH);if (path != null) {iv.setImageURI(Uri.fromFile(new File(path)));} else {finish();}}}
显示视频的页面:
/Notes/src/com/tops/notes/AtyVideoViewer.java
package com.tops.notes;import android.app.Activity;import android.os.Bundle;import android.widget.MediaController;import android.widget.VideoView;/*** 显示视频的Activity** @author TOPS**/public class AtyVideoViewer extends Activity {private VideoView vv;public static final String EXTRA_PATH = "path";@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);vv = new VideoView(this);vv.setMediaController(new MediaController(this));setContentView(vv);String path = getIntent().getStringExtra(EXTRA_PATH);if (path != null) {vv.setVideoPath(path);} else {finish();}}}
操作数据库的类:
/Notes/src/com/tops/notes/db/NotesDB.java
package com.tops.notes.db;import android.content.Context;import android.database.sqlite.SQLiteDatabase;import android.database.sqlite.SQLiteOpenHelper;/*** 实现SQLiteOpenHelper接口的NotesDB类,用于创建数据库表** @author TOPS**/public class NotesDB extends SQLiteOpenHelper {public static final String TABLE_NAME_NOTES = "notes";public static final String TABLE_NAME_MEDIA = "media";public static final String COLUMN_NAME_ID = "_id";public static final String COLUMN_NAME_NOTE_NAME = "name";public static final String COLUMN_NAME_NOTE_CONTENT = "content";public static final String COLUMN_NAME_NOTE_DATE = "date";public static final String COLUMN_NAME_MEDIA_PATH = "path";public static final String COLUMN_NAME_MEDIA_OWNER_NOTE_ID = "note_id";public NotesDB(Context context) {super(context, "notes", null, 1);}/*** 当第一次打开数据库,表不存在时调用,以创建表*/@Overridepublic void onCreate(SQLiteDatabase db) {db.execSQL("CREATE TABLE " + TABLE_NAME_NOTES + "(" + COLUMN_NAME_ID+ " INTEGER PRIMARY KEY AUTOINCREMENT," + COLUMN_NAME_NOTE_NAME+ " TEXT NOT NULL DEFAULT \"\"," + COLUMN_NAME_NOTE_CONTENT+ " TEXT NOT NULL DEFAULT \"\"," + COLUMN_NAME_NOTE_DATE+ " TEXT NOT NULL DEFAULT \"\"" + ")");db.execSQL("CREATE TABLE " + TABLE_NAME_MEDIA + "(" + COLUMN_NAME_ID+ " INTEGER PRIMARY KEY AUTOINCREMENT,"+ COLUMN_NAME_MEDIA_PATH + " TEXT NOT NULL DEFAULT \"\","+ COLUMN_NAME_MEDIA_OWNER_NOTE_ID+ " INTEGER NOT NULL DEFAULT 0" + ")");}/*** 当表存在且是旧版本时调用,删除旧表,创建新表*/@Overridepublic void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {// TODO Auto-generated method stub}}
manifest文件代码:
/Notes/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"package="com.tops.notes"android:versionCode="1"android:versionName="1.0" ><uses-sdkandroid:minSdkVersion="11"android:targetSdkVersion="21" /><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><applicationandroid:allowBackup="true"android:icon="@drawable/ic_launcher"android:label="@string/app_name"android:theme="@style/AppTheme" ><activityandroid:name=".MainActivity"android:configChanges="keyboardHidden|orientation"android:label="@string/app_name" ><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity><activityandroid:name="AtyEditNote"android:configChanges="keyboardHidden|orientation" ></activity><activityandroid:name="AtyPhotoViewer"android:configChanges="keyboardHidden|orientation" ></activity><activityandroid:name="AtyVideoViewer"android:configChanges="keyboardHidden|orientation" ></activity></application></manifest>
八、测试
列出要测试的功能、记录测试时间及操作方式、记录造成BUg出现的操作步骤
发布给提交给客户
测试功能和操作方式:
应用的功能是否达到需求,达到了。
数据库创建是否成功,成功了,两个理由:查看目录知道有数据库文件;关闭应用重新打开时笔记不会消失。
运行截图:
查看照片
播放视频
后记:
这因为只是学习实践,为了练习Android使用SQLite,listview、Intent等知识点所做,但是功能还是太简单、界面太简陋、需要慢慢优化,比较好的学习对象是随笔记/gnote。
源码下载:http://download.youkuaiyun.com/detail/huolangge/8221669
本文详细介绍了一款具备多媒体功能的记事本App的设计与开发过程,包括需求分析、系统设计、数据库架构等内容。
2万+





