基于 Android 的博物馆文化展示与预约 App —— 设计与实现(有源码和文档)

一、项目背景与目标

随着"让文物活起来"和"数字博物馆"理念的普及,越来越多的博物馆开始借助移动应用向公众提供藏品展示、新闻资讯、参观预约等数字化服务。本文介绍的 museumApp 是一款以"博物馆文化展示与预约"为核心的 Android 原生应用,其目标是:

  • 集中展示:在一个 App 内集成博物馆简介、开放时间、新闻资讯、展览活动、藏品典藏等内容。
  • 在线预约:让观众可以填写预约日期、人数、时段,提交到本地数据库,并在"我的"中查看自己的预约记录。
  • 个人中心:支持账号注册、登录、个人资料维护(含头像更换),并区分普通用户与管理员角色。
  • 教学价值:作为 Android 课程设计的实践项目,对"启动页 → 登录/注册 → 底部导航 + Fragment → SQLite 持久化 → 详情页"这一典型 App 流程进行完整封装。

特别说明:项目虽然引入了 volley.jar 网络库,但实际业务中并未发起任何真实网络请求 ------所有藏品、新闻、活动、用户、预约等数据均通过本地 SQLite 或硬编码资源(R.drawable)提供,属于"离线优先"的教学型 Demo,可以保证课堂演示时不依赖外网。

二、整体架构

App 采用经典的 MVC + 单 Activity 多 Fragment 结构:

复制代码
UI 层   :Activity / Fragment / Adapter (View)
业务层  :DBUser / DBReservation(基于 SQLiteOpenHelper 的 DAO)
数据层  :entity(User / Reservation / News / Blank) + SPUtils
资源层  :drawable / layout / values
  • 一个 MainActivity 承载底部 BottomNavigationView,通过 FragmentTransaction.replace() 切换"首页 / 典藏 / 我的"三个 Fragment。
  • 业务页(藏品、活动、新闻、预约、详情等)各自独立 Activity,跳转通过 Intent 完成。
  • 数据持久化由 DBHelper(单例的 SQLiteOpenHelper)+ 两个 DAO(DBUserDBReservation)承担,登录态、自动登录、记住密码等小数据由 SPUtils(SharedPreferences 封装)存储。

三、技术栈与依赖

build.gradle 中声明的依赖:

技术 用途
AppCompat / Material / ConstraintLayout 基础 UI 框架与主题
RecyclerView 列表控件(项目中以 ListView 为主,RecyclerView 备用)
ViewPager + 自定义 AdPagerAdapter 首页顶部 3 秒自动轮播 + 触摸暂停/继续
Glide + glide-transformations 头像与详情大图的加载与变换
PictureSelector (picture_library v2.6.0) "我的资料"中更换头像(相册/拍照)
volley.jar(libs 目录) 已引入但未使用
SQLite(系统内置) 用户表、预约表
复制代码
// build.gradle 节选
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'com.github.bumptech.glide:glide:4.11.0'
implementation 'com.github.LuckSiege.PictureSelector:picture_library:v2.6.0'
implementation 'jp.wasabeef:glide-transformations:3.3.0'
implementation files('libs\\volley.jar')

四、目录结构与 Java 文件清单

复制代码
graduate/museumApp/
├── build.gradle
├── libs/volley.jar
└── src/main/
    ├── AndroidManifest.xml
    ├── res/        (drawable / layout / values 等)
    └── java/com/example/museum/
        ├── activity/      13 个 Activity
        ├── fragment/       3 个 Fragment
        ├── adapter/        5 个 Adapter
        ├── database/       3 个数据库类
        ├── entity/         4 个实体
        └── utils/          3 个工具类

各包关键类说明:

关键类 职责
activity OpenActivity 启动页:5 秒倒计时 + 数据库初始化 + 跳转登录/主页
LoginActivity 登录:账号、密码、记住密码、自动登录
RegisterActivity 注册:昵称/性别/手机号/职业/兴趣/密码/头像
MainActivity 主页:底部导航 + Fragment 容器
ActivitiesActivity 教育活动列表
NewsActivity 新闻列表
CollectionActivity 典藏列表(按分类)
ReservationActivity 网上预约表单
MyInfoActivity 我的个人资料(编辑 + 头像)
MyRecordActivity 我的预约记录
DetailActivity 通用详情(博物馆简介/开放时间)
CollectionDetailActivity 藏品详情
NewsDetailActivity 新闻详情
fragment BlankFragment_Main 首页:轮播 + 功能入口
BlankFragment_Collection 典藏分类 Fragment
BlankFragment_me "我的" Fragment
adapter AdPagerAdapter 首页轮播 ViewPager 适配器
BlankAdapter 通用卡片 ArrayAdapter(首页功能入口)
NewsAdapter 新闻列表
CollectionAdapter 典藏分类
RecordAdapter 预约记录
database DBHelper SQLiteOpenHelper 单例,建表 + 初始化管理员
DBUser 用户增删改查、登录校验
DBReservation 预约新增 / 查询(按用户名筛选)
entity User / Reservation / News / Blank 4 个 POJO
utils SPUtils SharedPreferences 封装(键值常量化)
GlideEngine 实现 PictureSelector 的 ImageEngine
UserTypeEnum 用户类型枚举(普通用户/管理员/院长)

五、数据库设计

数据库名 museum.db,版本 1,存在应用私有数据目录下。

5.1 用户表 users

字段 类型 约束 说明
name text PRIMARY KEY 用户名(账号)
psw text NOT NULL 密码
occupation text NOT NULL 职业
sex text NOT NULL 性别
phone text NOT NULL 手机号
interest text NOT NULL 兴趣
photo text NOT NULL 头像路径
userType text NOT NULL 用户类型(普通/管理员/院长)

5.2 预约表 reservation

字段 类型 约束 说明
id integer PRIMARY KEY AUTOINCREMENT 预约 ID
name text NOT NULL 预约人姓名
time text NOT NULL 预约日期
number text NOT NULL 预约人数
ampm text NOT NULL 上午/下午

首次启动时,DBHelper.init() 会判断管理员账号 admin / 123 是否存在,若不存在则写入;并把当前登录用户缓存到 DBUser.user 静态字段中,便于全局访问。

核心建表代码:

1:50:src/main/java/com/example/museum/database/DBHelper.java 复制代码
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
    String sql="create table users(name text primary key,psw text not null," +
            "occupation text not null,sex text not null,phone text not null," +
            "interest text not null,photo text not null,userType text not null)";
    sqLiteDatabase.execSQL(sql);
    sql = "CREATE TABLE reservation (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
            "name TEXT NOT NULL, time TEXT NOT NULL, " +
            "number TEXT NOT NULL, ampm TEXT NOT NULL)";
    sqLiteDatabase.execSQL(sql);
}

六、关键模块实现

6.1 启动页与登录态判断

OpenActivity 是 LAUNCHER 入口,使用 CountDownTimer 实现 5 秒倒计时与"跳过"按钮:

22:65:src/main/java/com/example/museum/activity/OpenActivity.java 复制代码
public class OpenActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_open);
        DBHelper.GetInstance(this.getApplication()).init();
        in = findViewById(R.id.in);
        // 倒计时 5 秒
        CountDownTimer timer = new CountDownTimer(5000, 1000) {
            public void onTick(long millisUntilFinished) {
                in.setText(millisUntilFinished / 1000 + "秒");
            }
            public void onFinish() { finishView(); }
        };
        timer.start();
    }
    private void finishView(){
        Intent intent = new Intent();
        // 根据 SPUtils 中的登录态决定去向
        if (!TextUtils.isEmpty(SPUtils.getName(this))
                && (Boolean) SPUtils.get(this, SPUtils.AUTO_LOGIN, false)) {
            intent.setClass(OpenActivity.this, MainActivity.class);
        } else {
            intent.setClass(OpenActivity.this, LoginActivity.class);
        }
        startActivity(intent);
        finish();
    }
}

登录页支持"自动登录"和"记住密码"两个选项,二者状态均由 SPUtils 持久化。校验逻辑直接走 DBUser.check(account, password),从本地 users 表中匹配。

6.2 主页与底部导航

MainActivity 是 App 唯一的"框架页",由 BottomNavigationView + FrameLayout 组合,通过 FragmentTransaction.replace() 切换 3 个 Fragment:

19:61:src/main/java/com/example/museum/activity/MainActivity.java 复制代码
public class MainActivity extends AppCompatActivity {
    FrameLayout main;
    BottomNavigationView navigation;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        main = findViewById(R.id.main);
        navigation = findViewById(R.id.navigation);
        navigation.setOnItemSelectedListener(item -> {
            switch (item.getItemId()) {
                case R.id.home:
                    replace(new BlankFragment_Main().newInstance()); return true;
                case R.id.cart:
                    replace(new BlankFragment_Collection().newInstance()); return true;
                case R.id.me:
                    replace(new BlankFragment_me().newInstance()); return true;
            }
            return false;
        });
        navigation.setSelectedItemId(R.id.home); // 默认首页
    }
}

底部三 Tab 分别为:首页 / 典藏 / 我的

6.3 首页:自动轮播 + 功能入口

BlankFragment_Main 把"广告位"和"功能入口列表"组合在一起:

  • 顶部 ViewPager 加载 4 张本地 drawablehead1~head4),用 Handler.postDelayed 实现 3 秒自动切换;
  • 触摸时通过 OnTouchListener 暂停轮播,抬手 / 取消时延 1 秒后继续;
  • 下方 ListView 加载 5 个 Blank 实体(博物馆简介、开放时间、教育活动、网上预约、新闻咨询),点击不同位置分别跳到 DetailActivity / ActivitiesActivity / ReservationActivity / NewsActivity
31:93:src/main/java/com/example/museum/fragment/BlankFragment_Main.java 复制代码
private ViewPager mViewPager;
private Handler mHandler = new Handler();
private Runnable mRunnable = new Runnable() {
    @Override
    public void run() {
        int currentItem = mViewPager.getCurrentItem();
        int nextItem = (currentItem + 1) % mViewPager.getAdapter().getCount();
        mViewPager.setCurrentItem(nextItem);
        mHandler.postDelayed(this, 3000);
    }
};

int[] adImages = {R.drawable.head4, R.drawable.head2, R.drawable.head3, R.drawable.head1};
AdPagerAdapter adapter = new AdPagerAdapter(getActivity(), adImages);
mViewPager.setOnTouchListener((v, event) -> {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mHandler.removeCallbacks(mRunnable); break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            mHandler.postDelayed(mRunnable, 1000); break;
    }
    return false;
});

6.4 典藏分类与详情

BlankFragment_Collection 进入"典藏"Tab 时展示 5 个分类入口:镇馆之宝 / 精品鉴赏 / 三维藏品 / 藏品公开 / 藏品征集 。每条点击进入 CollectionActivity,再以 CollectionAdapter 渲染同分类下的具体藏品(缩略图 + 名称),点入 CollectionDetailActivity 查看大图与文字详情。

6.5 新闻与活动

NewsActivityActivitiesActivity 使用 ArrayAdapter<News> 渲染图文列表,点击进入 NewsDetailActivity / DetailActivity 渲染正文。这部分数据与首页一样来自代码内硬编码,未做联网。

6.6 网上预约

ReservationActivity 是 App 业务核心:

  • 通过 DatePickerDialog 选择预约日期(自动阻止选择过去日期),并格式化为 yyyy年MM月dd日
  • 使用 RadioGroup 选择上午 / 下午;
  • 表单上展示"当前时段剩余预约量"(DBReservation.allCount - DBReservation.count,其中 allCount = 999);
  • 提交时校验:人数不能为空、本次人数 + 历史累计不能超过 999;校验通过后调用 DBReservation.add(...) 写入数据库。
76:114:src/main/java/com/example/museum/activity/ReservationActivity.java 复制代码
protected void showDatePickerDialog() {
    new DatePickerDialog(this, (view, yearSet, monthSet, daySet) -> {
        if (yearSet < year) {
            Toast.makeText(this, "不能选择过去日期", Toast.LENGTH_SHORT).show();
            return;
        }
        Calendar c = Calendar.getInstance();
        c.set(yearSet, monthSet, daySet);
        time = new SimpleDateFormat("yyyy年MM月dd日", Locale.CHINA).format(c.getTime());
        textView.setText(time);
    }, year, month - 1, day).show();
}

private void save() {
    String number = edit_number.getText().toString();
    String am = rgSex.getCheckedRadioButtonId() == R.id.rb_am ? "上午" : "下午";
    if ("".equals(number)) {
        Toast.makeText(this, "人数不能为空", Toast.LENGTH_LONG).show();
        return;
    }
    if (DBReservation.count + Integer.valueOf(number) > 999) {
        Toast.makeText(this, "输入人数超出剩余数量", Toast.LENGTH_LONG).show();
        return;
    }
    DBReservation db = new DBReservation(getBaseContext());
    db.add(new Reservation(DBUser.user.getName(), time, number, am));
    Toast.makeText(this, "保存成功", Toast.LENGTH_SHORT).show();
    finish();
}

预约完成后,可在"我的 → 预约记录"中按用户名筛选展示历史预约。

6.7 个人中心

"我的"Fragment 通过 DBUser.user 静态字段拿到当前登录用户,渲染头像 + 昵称 + 三个入口:

  • 个人信息MyInfoActivity:支持修改昵称/性别/手机号/职业/兴趣,并通过 PictureSelector + GlideEngine 拍照/选图更换头像,写入 users.photo
  • 预约记录MyRecordActivity:调用 DBReservation.getAll(userName) 拿到当前用户所有预约。
  • 退出登录 :清空 SPUtils 后回到 LoginActivity

七、关键流程

7.1 启动与登录流程

复制代码
OpenActivity(启动页,5s 倒计时)
   │
   ├── DBHelper.init():建库 + 写入管理员 admin/123
   │
   ├── SPUtils.AUTO_LOGIN == true  → MainActivity
   │
   └── 否则 → LoginActivity
                 ├── 点击"注册" → RegisterActivity → LoginActivity
                 └── 登录成功 → MainActivity

7.2 预约流程

复制代码
MainActivity → 首页"网上预约"卡片 → ReservationActivity
   → 选择日期 + 上午/下午 + 人数
   → 校验通过 → DBReservation.add()
   → 跳回 MainActivity,"我的 → 预约记录" → MyRecordActivity 拉取记录

八、运行效果

下面整理项目 pic/ 目录下的实际运行截图,按典型用户路径排序:

阶段 截图 说明
启动后首页 pic/Screenshot_20240630_085930.png 顶部 ViewPager 轮播 + 5 个功能入口 + 底部三 Tab
进入功能 pic/Screenshot_20240630_090022.png 点击"博物馆简介"进入通用详情页
活动列表 pic/Screenshot_20240630_090157.png 教育活动图文列表
活动详情 pic/Screenshot_20240630_090214.png 大图 + 活动正文
典藏分类 pic/Screenshot_20240630_090606.png 5 个圆角按钮
典藏列表 pic/Screenshot_20240630_090625.png 镇馆之宝
典藏列表 pic/Screenshot_20240630_090642.png 精品鉴赏
典藏列表 pic/Screenshot_20240630_090707.png 三维藏品
典藏列表 pic/Screenshot_20240630_090728.png 藏品公开
典藏列表 pic/Screenshot_20240630_090750.png 藏品征集
典藏详情 pic/Screenshot_20240630_090803.png 大图 + 详情正文
新闻列表 pic/Screenshot_20240630_090254.png 招聘、活动、新闻混合
新闻详情 pic/Screenshot_20240630_090312.png 时间 + 长文
登录 pic/Screenshot_20240630_124422.png 记住密码 / 自动登录
注册 pic/Screenshot_20240630_124439.png 7 个字段 + 头像
我的 pic/Screenshot_20240630_124356.png 头像 + 三个入口
个人资料 pic/Screenshot_20240630_124509.png 头像 + 表单 + 保存
预约记录 pic/Screenshot_20240630_124550.png 按用户筛选后的记录

九、设计亮点与经验总结

  1. 单 Activity + Fragment 的轻量"主页"模式 :业务页各自独立 Activity,主页仅保留"首页 / 典藏 / 我的"三个 Fragment,避免传统"多 Activity + Activity 间互相回传"的样板代码。
  2. DAO + 单例 DBHelperDBHelper.GetInstance(context) 全程复用 SQLiteDatabase,避免频繁 open/close,同时把 onCreate 写表与 init() 初始化种子数据解耦。
  3. 首页轮播的"触摸可控 + 自动续播" :通过 OnTouchListener 监听 ACTION_DOWN/UP/CANCEL,再配合 Handler.removeCallbacks/postDelayed,既能暂停也能延时恢复,体验比第三方 Banner 控件更可控。
  4. 预约余量的"软上限"allCount = 999count 跟随 getAll() 累加,是一种"教学型防超"------既演示了 DAO 的统计查询,又避免了对真实库存逻辑的过度设计。
  5. 登录态分离 :把"是否记住密码 / 是否自动登录"两个状态拆为两个 CheckBox + 两个 SPUtils 键值,是常见需求;再叠加 CountDownTimer 启动页倒计时,完整覆盖真实 App 的"冷启动 → 拉起主页"的体验链路。
  6. 可扩展的"通用详情页"DetailActivity 接收 Blank 实体作为 Intent 附加数据,由 Blank 携带图标 + 标题 + 描述,使"博物馆简介 / 开放时间"等纯展示型页面共用一个 Activity,降低模板代码。

十、可改进点

虽然本项目足以支撑教学演示和功能跑通,但仍有不少可演进空间:

  • 真实网络层 :把硬编码列表数据抽到接口(/api/collections/api/news),用 Retrofit/OkHttp 替换当前的 volley.jar 占位,并在 ListView 上加 SwipeRefreshLayout
  • Room 替代裸 SQLite :用 Room 把 User / Reservation 注解为 Entity,写编译期校验的 DAO 接口,配合 LiveData/Flow 让预约记录自动刷新。
  • MVVM 化 :引入 ViewModel + LiveData,把"首页 Fragment + 业务 Activity"改造成数据驱动,配合 ViewBinding 替换 findViewById
  • 图片选择统一化 :用 ActivityResultContracts 替代旧版 onActivityResult,并结合 Android 13 的 READ_MEDIA_IMAGES 权限模型重构头像选择。
  • 暗色模式与国际化 :当前主题写死绿色(#1AB394),可结合 values-nightMaterial You 提升一致性。
  • 统一包名遗留AndroidManifest.xmlpackage="com.example.smallshop" 与 Java 包 com.example.museum 不一致,可通过 android.namespace + applicationIdSuffix 整理,避免 R 类的 import 必须写成 com.example.smallshop.R

十一、小结

museumApp 用最经典的"启动页 + 登录 + 主页三 Tab + 详情/表单页"结构,完整跑通了一个 Android 入门到进阶的"博物馆文化展示与预约"业务。它不追求花哨的视觉效果,但把 Activity/Fragment 导航、SQLite 持久化、SharedPreferences 登录态、ViewPager 轮播、PictureSelector 选图、Glide 加载 等基础但重要的知识点全部串了起来,非常适合作为 Android 课程设计或入门实战项目的参考实现。


相关推荐
一笑的小酒馆2 小时前
AndroidAGP9.0升级之路
android
阮胜昌2 小时前
MySQL 中的动态数据脱敏:无需更改应用程序即可保护敏感数据
android·mysql·adb
aaajj4 小时前
关于MODIFY_PHONE_STATE权限
android
古法安卓5 小时前
Android-高版本 LMKD 源码解析:基于 PSI 的内存压力监控与查杀全流程
android·java·android studio
古法安卓5 小时前
Android-PSI 详解:libpsi 源码解析——116 行代码架起 lmkd 与内核之间的桥
android·java·android studio
mmsx8 小时前
Android 新闻资讯客户端实战:从零搭建一个完整的新闻 App
android·app·源码
三84410 小时前
PHP 绕过污点追踪构造 Webshell:反序列化引用复用与 Trait 可变函数 RCE
android
石头猫灯10 小时前
WordPress wp2shell漏洞断点逐过程到注入点
android·学习
Android打工仔11 小时前
Kotlin 协程源码解析(五):BaseContinuationImpl.resumeWith() —— Continuation 链是如何被展开的?
android·kotlin