Android App开发实战项目之电子书架App的实现(附源码和演示视频 简单易懂 可直接使用)

简介: Android App开发实战项目之电子书架App的实现(附源码和演示视频 简单易懂 可直接使用)

需要图片集和源码请点赞关注收藏后评论区留言~~~

一、需求描述

在手机上浏览电子书的浏览体验跟阅读纸质书差不多,翻页过程仍旧呈现纸张翻转的视觉特效,让读者看起来赏心悦目。总结一下,手机阅读无非是要具有两大功能点:其一为书架管理,主要是书籍的增删改查,其二为浏览操作,主要是翻页过程的处理

二、功能分析

电子书有几个问题,一方面是电子书格式多样,另一方面是Android没有现成的控件可以统一显示这些电子书,格式各异的电子书要在手机屏幕的方寸之间展示,十分困难。

对于前一个问题。可将电子书统一成少数几种公共格式以便降低编码难度,对于后一个问题,可将电子书的每个页面都转成图片文件,然后利用图像视图浏览电子书。

接下来的实战项目中,暂且只支持两种格式的电子书,分别是PDF,一种与平台无关的电子文件和DJVU。

接下来分析电子书阅读器中用到的一些技术

资产管理器AssetManager 初始的五本演示电子书

数据库框架Room 每本电子书的图书名称 作者 页数统一保存到数据库中

PDF文件渲染器 把PDF文件解析为一组图片

贝塞尔曲线 在浏览电子书的翻页过程中 利用它实现翻页特效

JNI接口

图片文件处理

输入对话框

下面简单介绍一下主要代码模块之间关系

EbookReaderActivity 电子书阅读器的书籍列表页面

PdfRenderActiviity  PDF电子书的阅读页面

PdfSlideActivity PDF电子书的平滑翻页

PdfCurveActivity 贝塞尔曲线翻页

PdfOpenglActivity 卷曲翻页

DvjuRenderActivity DJVU电子书的阅读页面

ImageFragment 每页电子书图片的展示碎片

三、效果展示

效果展示如下 既有普通的平滑翻页 也有贝塞尔曲线和卷曲翻页 看起来更加逼真和赏心悦目

演示视频如下

Android电子书App

效果图如下

四、代码

Ebook类

package com.example.ebook;
import android.content.Intent;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.View;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import com.example.ebook.adapter.BookListAdapter;
import com.example.ebook.dao.BookDao;
import com.example.ebook.entity.BookInfo;
import com.example.ebook.util.AssetsUtil;
import com.example.ebook.widget.InputDialog;
import java.util.ArrayList;
import java.util.List;
public class EbookReaderActivity extends AppCompatActivity implements
        AdapterView.OnItemClickListener, AdapterView.OnItemLongClickListener {
    private static final String TAG = "EbookReaderActivity";
    private ListView lv_ebook; // 声明一个用于展示书籍列表的列表视图对象
    private String[] mFileNameArray = {"tangshi.pdf", "android.pdf", "zhugeliang.djvu", "dufu.djvu", "luyou.djvu"};
    private List<BookInfo> mBookList = new ArrayList<>(); // 书籍信息列表
    private BookListAdapter mAdapter; // 声明一个书籍列表的适配器对象
    private BookDao bookDao; // 声明一个书籍的持久化对象
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_ebook_reader);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); // 保持屏幕常亮
        initView(); // 初始化视图
        new Thread(() -> copyPdfFile()).start(); // 启动演示文件的复制线程
    }
    // 初始化视图
    private void initView() {
        Toolbar tl_head = findViewById(R.id.tl_head);
        tl_head.setTitle("电子书架");
        setSupportActionBar(tl_head); // 替换系统自带的ActionBar
        // 设置工具栏左侧导航图标的点击监听器
        tl_head.setNavigationOnClickListener(view -> finish());
        lv_ebook = findViewById(R.id.lv_ebook);
    }
    // 把assets目录下的演示文件复制到存储卡
    private void copyPdfFile() {
        // 从App实例中获取唯一的书籍持久化对象
        bookDao = MainApplication.getInstance().getBookDB().bookDao();
        mBookList = bookDao.queryAllBook(); // 获取所有书籍记录
        if (mBookList!=null && mBookList.size()>0) {
            runOnUiThread(() -> initBookList()); // 初始化书籍列表
            return;
        }
        List<BookInfo> bookList = new ArrayList<>();
        for (String file_name : mFileNameArray) {
            String dir = String.format("%s/%s/",
                    getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString(),
                    file_name.substring(file_name.lastIndexOf(".")+1)
            );
            String fileName = file_name.substring(file_name.lastIndexOf("/") + 1);
            // 把资产目录下的电子书复制到存储卡
            AssetsUtil.Assets2Sd(this, fileName, dir + fileName);
            bookList.add(new BookInfo(file_name));
        }
        bookDao.insertBookList(bookList); // 把演示用的电子书信息添加到数据库
        runOnUiThread(() -> initBookList()); // 初始化书籍列表
    }
    @Override
    protected void onRestart() {
        super.onRestart();
        initBookList(); // 初始化书籍列表
    }
    // 初始化书籍列表
    private void initBookList() {
        mBookList = bookDao.queryAllBook(); // 获取所有书籍记录
        // 下面把书籍列表通过ListView展现出来
        mAdapter = new BookListAdapter(this, mBookList);
        lv_ebook.setAdapter(mAdapter);
        lv_ebook.setOnItemClickListener(this);
        lv_ebook.setOnItemLongClickListener(this);
    }
    // 在点击书籍记录时触发
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        String file_name = mBookList.get(position).getFileName();
        String title = mBookList.get(position).getTitle();
        Log.d(TAG, "file_name="+file_name+", title="+title);
        if (file_name.endsWith(".pdf")) { // PDF格式
            // 跳转到PDF阅读界面
            startReader(file_name, title, PdfRenderActivity.class);
        } else if (file_name.endsWith(".djvu")) { // DJVU格式
            // 跳转到第三方Vudroid提供的阅读界面
            startReader(file_name, title, DjvuRenderActivity.class);
        } else {
            Toast.makeText(this, "暂不支持该格式的电子书", Toast.LENGTH_SHORT).show();
        }
    }
    // 在长按书籍记录时触发
    @Override
    public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
        // 以下创建并弹出标题填写对话框
        InputDialog dialog = new InputDialog(this, mBookList.get(position).getFileName(),
                position, "请输入书籍名称", (idt, content, seq) -> {
            BookInfo book = mBookList.get(seq);
            book.setTitle(content);
            bookDao.updateBook(book); // 更新数据库中该书籍记录的标题
        });
        dialog.show();
        return true;
    }
    // 启动指定的电子书阅读界面
    private void startReader(String file_name, String title, Class<?> cls) {
        Intent intent = new Intent(this, cls);
        intent.putExtra("file_name", file_name);
        intent.putExtra("title", title);
        startActivity(intent);
    }
}

PDF类

package com.example.ebook;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import android.view.ViewGroup;
import com.example.ebook.util.AssetsUtil;
import com.example.ebook.util.Utils;
import com.example.ebook.widget.CurveView;
import java.util.ArrayList;
import java.util.List;
public class PdfCurveActivity extends AppCompatActivity {
    private final static String TAG = "PdfCurveActivity";
    private CurveView cv_book; // 声明一个卷曲视图对象
    private List<String> mPathList = new ArrayList<>(); // 图片路径列表
    private String mFileName = "tangshi.pdf"; // 演示文件的名称
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_pdf_curve);
        initView(); // 初始化视图
        // 加载pdf会花一点点时间,这里先让整个界面出来,再慢慢渲染pdf
        new Handler(Looper.myLooper()).post(() -> renderPDF());
    }
    // 初始化视图
    private void initView() {
        String title = "";
        // 从前个页面传来的数据中获取书籍的标题和文件名称
        if (getIntent().getExtras()!=null && !getIntent().getExtras().isEmpty()) {
            title = getIntent().getStringExtra("title");
            mFileName = getIntent().getStringExtra("file_name");
        }
        Toolbar tl_head = findViewById(R.id.tl_head);
        tl_head.setTitle(!TextUtils.isEmpty(title) ? title : mFileName);
        // 设置工具栏左侧导航图标的点击监听器
        tl_head.setNavigationOnClickListener(view -> finish());
        cv_book = findViewById(R.id.cv_book);
        findViewById(R.id.btn_resume).setOnClickListener(v -> cv_book.reset());
    }
    // 开始渲染PDF文件
    private void renderPDF() {
        // 把资产文件转换为图片路径列表
        mPathList = AssetsUtil.getPathListFromPdf(this, mFileName);
        Log.d(TAG, "mPathList.size="+mPathList.size());
        Bitmap first = BitmapFactory.decodeFile(mPathList.get(0));
        int height = (int)(1.0*first.getHeight()/first.getWidth() * Utils.getScreenWidth(this));
        Log.d(TAG, "height="+height);
        ViewGroup.LayoutParams params = cv_book.getLayoutParams();
        params.height = height; // 根据书页图片的尺寸调整卷曲视图的高度
        cv_book.setLayoutParams(params); // 设置卷曲视图的布局参数
        cv_book.setFilePath(mPathList); // 设置卷曲视图的文件路径
    }
}

卷曲翻页类

package com.example.ebook;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.pdf.PdfRenderer;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.text.TextUtils;
import android.util.Log;
import android.view.ViewGroup.LayoutParams;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import com.example.ebook.util.AssetsUtil;
import com.example.ebook.util.BitmapUtil;
import com.example.ebook.util.Utils;
import java.io.File;
import java.util.ArrayList;
import fi.harism.curl.CurlPage;
import fi.harism.curl.CurlView;
public class PdfOpenglActivity extends AppCompatActivity {
    private final static String TAG = "PdfOpenglActivity";
    private CurlView cv_content; // 声明一个卷曲视图对象
    private ArrayList<String> mImgList = new ArrayList<>(); // 图片路径列表
    private String mFileName = "tangshi.pdf"; // 演示文件的名称
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_pdf_opengl);
        initView(); // 初始化视图
        // 加载pdf会花一点点时间,这里先让整个界面出来,再慢慢渲染pdf
        new Handler(Looper.myLooper()).post(() -> renderPDF());
    }
    // 初始化视图
    private void initView() {
        String title = "";
        // 从前个页面传来的数据中获取书籍的标题和文件名称
        if (getIntent().getExtras()!=null && !getIntent().getExtras().isEmpty()) {
            title = getIntent().getStringExtra("title");
            mFileName = getIntent().getStringExtra("file_name");
        }
        Toolbar tl_head = findViewById(R.id.tl_head);
        tl_head.setTitle(!TextUtils.isEmpty(title) ? title : mFileName);
        // 设置工具栏左侧导航图标的点击监听器
        tl_head.setNavigationOnClickListener(view -> finish());
        cv_content = findViewById(R.id.cv_content);
    }
    // 开始渲染PDF文件
    private void renderPDF() {
        String dir = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() + "/pdf/";
        String filePath = dir + mFileName;
        // 无法直接从asset目录读取PDF文件,只能先把PDF文件复制到存储卡,再从存储卡读取PDF
        AssetsUtil.Assets2Sd(this, mFileName, filePath);
        try {
            // 打开存储卡里指定路径的PDF文件
            ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
                    new File(filePath), ParcelFileDescriptor.MODE_READ_ONLY);
            // 创建一个PDF渲染器
            PdfRenderer pdfRenderer = new PdfRenderer(pfd);
            Log.d(TAG, "page count=" + pdfRenderer.getPageCount());
            // 依次处理PDF文件的每个页面
            for (int i = 0; i < pdfRenderer.getPageCount(); i++) {
                // 生成该页图片的保存路径
                String imgPath = String.format("%s/%03d.jpg", dir, i);
                mImgList.add(imgPath);
                // 打开序号为i的页面
                PdfRenderer.Page page = pdfRenderer.openPage(i);
                // 创建该页面的临时位图
                Bitmap bitmap = Bitmap.createBitmap(page.getWidth(), page.getHeight(),
                        Bitmap.Config.ARGB_8888);
                bitmap.eraseColor(Color.WHITE); // 将临时位图洗白
                // 渲染该PDF页面并写入到临时位图
                page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
                BitmapUtil.saveImage(imgPath, bitmap); // 把位图对象保存为图片文件
                page.close(); // 关闭该PDF页面
            }
            pdfRenderer.close(); // 处理完毕,关闭PDF渲染器
        } catch (Exception e1) {
            e1.printStackTrace();
        }
        // 从指定路径的图片文件中获取位图数据
        Bitmap bitmap = BitmapFactory.decodeFile(mImgList.get(0));
        int iv_height = (int)(1.0*bitmap.getHeight()/bitmap.getWidth() * Utils.getScreenWidth(this));
        showImage(iv_height); // 在卷曲视图上显示位图图像
        bitmap.recycle(); // 回收位图对象
    }
    // 在卷曲视图上显示位图图像
    private void showImage(int height) {
        LayoutParams params = cv_content.getLayoutParams();
        params.height = height;
        // 设置卷曲视图的布局参数
        cv_content.setLayoutParams(params);
        // 设置卷曲视图的书页提供器
        cv_content.setPageProvider(new PageProvider(mImgList));
        // 设置卷曲视图的尺寸变更观察器
        cv_content.setSizeChangedObserver(new SizeChangedObserver());
        // 设置卷曲视图默认显示第一页
        cv_content.setCurrentIndex(0);
        // 设置卷曲视图的背景颜色
        cv_content.setBackgroundColor(Color.LTGRAY);
    }
    // 定义一个加载图片页面的提供器
    private class PageProvider implements CurlView.PageProvider {
        private ArrayList<String> mPathArray = new ArrayList<>();
        public PageProvider(ArrayList<String> pathArray) {
            mPathArray = pathArray;
        }
        @Override
        public int getPageCount() {
            return mPathArray.size();
        }
        // 在页面更新时触发
        public void updatePage(CurlPage page, int width, int height, int index) {
            // 加载指定页面的位图
            Bitmap front = BitmapFactory.decodeFile(mPathArray.get(index));
            // 设置书页的纹理
            page.setTexture(front, CurlPage.SIDE_BOTH);
        }
    }
    // 定义一个监听卷曲视图发生尺寸变更的观察器
    private class SizeChangedObserver implements CurlView.SizeChangedObserver {
        @Override
        public void onSizeChanged(int w, int h) {
            // 设置卷曲视图的观看模式
            cv_content.setViewMode(CurlView.SHOW_ONE_PAGE);
            // 设置卷曲视图的四周边缘
            cv_content.setMargins(0f, 0f, 0f, 0f);
        }
    }
}

XML文件

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/book_bg3"
    android:orientation="vertical">
    <androidx.appcompat.widget.Toolbar
        android:id="@+id/tl_head"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="@color/blue_light"
        app:navigationIcon="@drawable/icon_back" />
    <ListView
        android:id="@+id/lv_ebook"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />
</LinearLayout>

创作不易 觉得有帮助请点赞关注收藏~~~

相关文章
|
16天前
|
JavaScript 前端开发 Android开发
【03】仿站技术之python技术,看完学会再也不用去购买收费工具了-修改整体页面做好安卓下载发给客户-并且开始提交网站公安备案-作为APP下载落地页文娱产品一定要备案-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
【03】仿站技术之python技术,看完学会再也不用去购买收费工具了-修改整体页面做好安卓下载发给客户-并且开始提交网站公安备案-作为APP下载落地页文娱产品一定要备案-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
57 13
【03】仿站技术之python技术,看完学会再也不用去购买收费工具了-修改整体页面做好安卓下载发给客户-并且开始提交网站公安备案-作为APP下载落地页文娱产品一定要备案-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
|
18天前
|
JavaScript 搜索推荐 Android开发
【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
41 8
【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
|
18天前
|
数据采集 JavaScript Android开发
【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
48 7
【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
|
8天前
|
小程序 搜索推荐
2025同城线下陪玩APP开发/电竞游戏平台搭建游戏陪玩APP源码/语音APP开发
线下陪玩约玩APP旨在满足现代人的社交、兴趣分享、专业指导及休闲娱乐需求。用户可通过平台结识新朋友、找到志同道合的伙伴,并享受高质量的陪玩服务。平台提供用户注册登录、陪玩师筛选与预约、实时沟通等功能,支持个性化游戏体验和高效匹配。
25 0
2025同城线下陪玩APP开发/电竞游戏平台搭建游戏陪玩APP源码/语音APP开发
|
3月前
|
存储 监控 API
app开发之安卓Android+苹果ios打包所有权限对应解释列表【长期更新】-以及默认打包自动添加权限列表和简化后的基本打包权限列表以uniapp为例-优雅草央千澈
app开发之安卓Android+苹果ios打包所有权限对应解释列表【长期更新】-以及默认打包自动添加权限列表和简化后的基本打包权限列表以uniapp为例-优雅草央千澈
108 11
|
7月前
|
开发工具 uml git
AOSP源码下载方法,解决repo sync错误:android-13.0.0_r82
本文分享了下载AOSP源码的方法,包括如何使用repo工具和处理常见的repo sync错误,以及配置Python环境以确保顺利同步特定版本的AOSP代码。
917 0
AOSP源码下载方法,解决repo sync错误:android-13.0.0_r82
|
7月前
|
Java Android开发 芯片
使用Android Studio导入Android源码:基于全志H713 AOSP,方便解决编译、编码问题
本文介绍了如何将基于全志H713芯片的AOSP Android源码导入Android Studio以解决编译和编码问题,通过操作步骤的详细说明,展示了在Android Studio中利用代码提示和补全功能快速定位并修复编译错误的方法。
365 0
使用Android Studio导入Android源码:基于全志H713 AOSP,方便解决编译、编码问题
|
7月前
|
开发工具 Android开发 git
全志H713 Android 11 :给AOSP源码,新增一个Product
本文介绍了在全志H713 Android 11平台上新增名为myboard的产品的步骤,包括创建新的device目录、编辑配置文件、新增内核配置、记录差异列表以及编译kernel和Android系统的详细过程。
429 0
|
存储 缓存 安全
Android14 适配之——现有 App 安装到 Android14 手机上需要注意些什么?
Android14 适配之——现有 App 安装到 Android14 手机上需要注意些什么?
578 0
|
10月前
|
传感器 物联网 Android开发
【Android App】物联网中查看手机支持的传感器及实现摇一摇功能-加速度传感器(附源码和演示 超详细)
【Android App】物联网中查看手机支持的传感器及实现摇一摇功能-加速度传感器(附源码和演示 超详细)
283 1

热门文章

最新文章