目录
一、测试环境说明
电脑环境
Windows 11
编写语言
JAVA
开发软件
Android Studio (2020)
开发软件只要大于等于测试版本即可(近几年官网直接下载也可以),若是版本低于测试版本请自行测试。项目需要根据你的软件自行适配
二、项目简介
该项目简介来自网络,具体内容需要自行测试
本项目是一个基于Android平台的电子书阅读应用,采用Java语言开发,使用Android Studio作为开发工具。应用实现了完整的电子书阅读功能,包括启动页倒计时、书籍列表展示、在线阅读等功能模块。系统采用MVC架构设计,通过异步网络请求获取书籍数据,使用本地文件缓存机制存储下载的书籍内容,并实现了流畅的翻页效果。
该应用的主要特点包括:精美的启动页倒计时动画、清晰的书籍分类列表、支持在线下载和本地阅读两种模式、自定义翻页效果、阅读进度保存等功能。在技术实现上,项目运用了Handler消息机制、AsyncHttpClient网络请求框架、Gson数据解析、文件读写操作等关键技术。阅读界面实现了基于贝塞尔曲线的仿真翻页效果,提供接近真实书籍的阅读体验。应用还考虑了内存管理问题,使用WeakReference防止内存泄漏,优化了大文件读取性能。
该项目由编程乐学团队介入,优化布局完善功能
三、项目演示
网络资源模板--基于Android studio 小说阅读App
四、部设计详情(部分)
小说页面

- 页面的结构
小说阅读页面采用全屏沉浸式设计,顶部预留状态栏空间,中间为主要内容显示区域,底部显示阅读进度百分比。
页面左右两侧各设置20%宽度为触控翻页区域,中央60%为内容展示区。
背景使用单一纯色,与文字形成鲜明对比确保可读性。整个界面去除了所有非必要元素,最大化内容展示空间,提供专注的阅读环境。
- 使用到的技术
该页面采用贝塞尔曲线算法实现仿真翻页效果,通过计算触摸点位置动态生成翻页动画。
使用内存映射技术高效读取大文本文件,避免内存溢出。采用双缓冲绘图技术消除页面刷新时的闪烁现象。
通过精确的触摸事件处理识别用户翻页手势。后台线程预加载机制确保翻页流畅不卡顿,提升用户体验。
- 页面详细介绍
阅读页提供完整的电子书浏览功能,支持左右滑动翻页操作。智能分页系统根据屏幕尺寸自动计算每页显示内容量。
阅读进度自动保存功能精确记录用户最后阅读位置。页面背景采用单一配色方案,文字与背景对比度经过优化确保阅读舒适性。
翻页动画模拟真实书籍的物理特性,包含自然的弯曲效果和阴影变化,提供沉浸式阅读体验。
java
package com.example.book.view;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Vector;
public class BookPageBezierHelper {
public static final String GBK = "GBK";
public static final String UTF_8 = "UTF-8";
public static final String UNICODE = "Unicode";
public static final String UTF_16_BE = "UTF-16BE";
public static final String UTF_16_LE = "UTF-16LE";
private int mWidth;
private int mHeight;
private File mBookFile;
private int mBookBufferLength;
private MappedByteBuffer mBookBuffer;
private String mBookCharsetName = GBK;
private int mPageLineCount; // 每页可以显示的行数
private int mBufferBegin;
private int mBufferEnd;
private float mVisibleHeight; // 绘制内容的高
private float mVisibleWidth; // 绘制内容的宽
private Paint mPaint;
private boolean mIsFirstPage, mIsLastPage;
private Vector<String> mLinesVector = new Vector<>();
private boolean mIsUserBg = false;
private OnProgressChangedListener onProgressChangedListener;
private Bitmap mBookBgBitmap;
private int mBackGroundColor = 0xffff9e85; // 背景颜色
private int mMarginWidth = 35; // 左右与边缘的距离
private int mMarginHeight = 80; // 上下与边缘的距离
private int mFontSize = 60;
private int mLineMargin = 5;
private int mTextColor = Color.BLACK;
public BookPageBezierHelper(int width, int height, int progress) {
this(width, height, Color.WHITE, Color.BLACK, 50, 60, progress);
}
public BookPageBezierHelper(int width, int height, int backGroundColor, int textColor, int lineMargin, int fontSize, int progress) {
mWidth = width;
mHeight = height;
mTextColor = textColor;
mBackGroundColor = backGroundColor;
mLineMargin = lineMargin;
mFontSize = fontSize;
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setTextAlign(Paint.Align.LEFT);
mPaint.setTextSize(mFontSize);
mPaint.setColor(mTextColor);
mPaint.setTypeface(null);
mVisibleWidth = mWidth - mMarginWidth * 2;
mVisibleHeight = mHeight - mMarginHeight * 2 + 108;
mPageLineCount = (int) (mVisibleHeight / (mFontSize + mLineMargin)); // 可显示的行数
mBufferBegin = mBufferEnd = progress;
}
/**
* 打开文件
*
* @param filePath file路径
* @throws IOException
*/
public void openBook(String filePath) throws IOException {
mBookFile = new File(filePath);
long length = mBookFile.length();
mBookBufferLength = (int) length;
mBookBuffer = new RandomAccessFile(mBookFile, "r").getChannel().map(FileChannel.MapMode.READ_ONLY, 0, length);
mBookCharsetName = codeString(filePath);
}
/**
* 判断文件的编码格式
*
* @param fileName :file
* @return 文件编码格式
*/
public static String codeString(String fileName) {
BufferedInputStream bufferedInputStream;
int fileCode = 0;
String code;
try {
bufferedInputStream = new BufferedInputStream(new FileInputStream(fileName));
} catch (FileNotFoundException e) {
e.printStackTrace();
bufferedInputStream = null;
}
if (bufferedInputStream != null) {
try {
fileCode = (bufferedInputStream.read() << 8) + bufferedInputStream.read();
} catch (IOException e) {
e.printStackTrace();
}
}
switch (fileCode) {
case 0xefbb:
code = UTF_8;
break;
case 0xfffe:
code = UNICODE;
break;
case 0xfeff:
code = UTF_16_BE;
break;
default:
code = GBK;
}
return code;
}
/**
* 读取后一段落
*
* @return
*/
protected byte[] readParagraphBack(int nextPosition) {
int end = nextPosition;
int i;
byte b0, b1;
if (mBookCharsetName.equals(UTF_16_LE)) {
i = end - 2;
while (i > 0) {
b0 = mBookBuffer.get(i);
b1 = mBookBuffer.get(i + 1);
if (b0 == 0x0a && b1 == 0x00 && i != end - 2) {
i += 2;
break;
}
i--;
}
} else if (mBookCharsetName.equals(UTF_16_BE)) {
i = end - 2;
while (i > 0) {
b0 = mBookBuffer.get(i);
b1 = mBookBuffer.get(i + 1);
if (b0 == 0x00 && b1 == 0x0a && i != end - 2) {
i += 2;
break;
}
i--;
}
} else {
i = end - 1;
while (i > 0) {
b0 = mBookBuffer.get(i);
if (b0 == 0x0a && i != end - 1) {
i++;
break;
}
i--;
}
}
if (i < 0)
i = 0;
int nParaSize = end - i;
int j;
byte[] buf = new byte[nParaSize];
for (j = 0; j < nParaSize; j++) {
buf[j] = mBookBuffer.get(i + j);
}
return buf;
}
/**
* 读取前一段落
*
* @param previousPosition
* @return
*/
protected byte[] readParagraphForward(int previousPosition) {
int start = previousPosition;
int i = start;
byte b0, b1;
// 根据编码格式判断换行
if (mBookCharsetName.equals(UTF_16_LE)) {
while (i < mBookBufferLength - 1) {
b0 = mBookBuffer.get(i++);
b1 = mBookBuffer.get(i++);
if (b0 == 0x0a && b1 == 0x00) {
break;
}
}
} else if (mBookCharsetName.equals(UTF_16_BE)) {
while (i < mBookBufferLength - 1) {
b0 = mBookBuffer.get(i++);
b1 = mBookBuffer.get(i++);
if (b0 == 0x00 && b1 == 0x0a) {
break;
}
}
} else {
while (i < mBookBufferLength) {
b0 = mBookBuffer.get(i++);
if (b0 == 0x0a) {
break;
}
}
}
int nParaSize = i - start;
byte[] buf = new byte[nParaSize];
for (i = 0; i < nParaSize; i++) {
buf[i] = mBookBuffer.get(previousPosition + i);
}
return buf;
}
protected Vector<String> pageDown() {
String strParagraph = "";
Vector<String> lines = new Vector<>();
// 一段一段的读取,一行一行的添加,一直到读取超过一页的行数。
while (lines.size() < mPageLineCount && mBufferEnd < mBookBufferLength) {
byte[] paragraphForward = readParagraphForward(mBufferEnd); // 读取一个段落
mBufferEnd += paragraphForward.length;
try {
strParagraph = new String(paragraphForward, mBookCharsetName);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String strReturn = "";
if (strParagraph.indexOf("\r\n") != -1) {
strReturn = "\r\n";
strParagraph = strParagraph.replaceAll("\r\n", "");
} else if (strParagraph.indexOf("\n") != -1) {
strReturn = "\n";
strParagraph = strParagraph.replaceAll("\n", "");
}
if (strParagraph.length() == 0) {
lines.add(strParagraph);
}
while (strParagraph.length() > 0) {
// 计算一行的字数。
int nSize = mPaint.breakText(strParagraph, true, mVisibleWidth, null);
// 添加一行
lines.add(strParagraph.substring(0, nSize));
// 将段落减去这一行,变成剩下的一部分,除非超过一页,才会跳出
strParagraph = strParagraph.substring(nSize);
if (lines.size() >= mPageLineCount) {
break;
}
}
if (strParagraph.length() != 0) {
try {
//位置向后移
mBufferEnd -= (strParagraph + strReturn).getBytes(mBookCharsetName).length;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
}
return lines;
}
protected void pageUp() {
if (mBufferBegin < 0)
mBufferBegin = 0;
Vector<String> lines = new Vector<>();
String strParagraph = "";
while (lines.size() < mPageLineCount && mBufferBegin > 0) {
Vector<String> paraLines = new Vector<>();
byte[] paraBuf = readParagraphBack(mBufferBegin);
mBufferBegin -= paraBuf.length;
try {
strParagraph = new String(paraBuf, mBookCharsetName);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
strParagraph = strParagraph.replaceAll("\r\n", "");
strParagraph = strParagraph.replaceAll("\n", "");
if (strParagraph.length() == 0) {
paraLines.add(strParagraph);
}
while (strParagraph.length() > 0) {
int nSize = mPaint.breakText(strParagraph, true, mVisibleWidth,
null);
paraLines.add(strParagraph.substring(0, nSize));
strParagraph = strParagraph.substring(nSize);
}
lines.addAll(0, paraLines);
}
while (lines.size() > mPageLineCount) {
try {
mBufferBegin += lines.get(0).getBytes(mBookCharsetName).length;
lines.remove(0);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
mBufferEnd = mBufferBegin;
return;
}
protected void prePage() throws IOException {
if (mBufferBegin <= 0) {
mBufferBegin = 0;
mIsFirstPage = true;
return;
} else
mIsFirstPage = false;
mIsLastPage = false;
mLinesVector.clear();
pageUp();
mLinesVector = pageDown();
}
public void nextPage() throws IOException {
if (mBufferEnd >= mBookBufferLength) {
mIsLastPage = true;
return;
} else
mIsLastPage = false;
mIsFirstPage = false;
mLinesVector.clear();
mBufferBegin = mBufferEnd;
mLinesVector = pageDown();
}
public void draw(Canvas canvas) {
if (mLinesVector.size() == 0)
mLinesVector = pageDown();
if (mLinesVector.size() > 0) {
if (mIsUserBg && mBookBgBitmap != null)
canvas.drawBitmap(mBookBgBitmap, 0, 0, null);
else {
canvas.drawColor(mBackGroundColor);
}
int yHeight = mMarginHeight + mFontSize;
// 一行行绘制文字
for (String strLine : mLinesVector) {
canvas.drawText(strLine, mMarginWidth, yHeight, mPaint);
yHeight += mFontSize + mLineMargin;
}
}
// 设置进度
if (onProgressChangedListener!= null) {
onProgressChangedListener.setProgress(mBufferBegin, mBookBufferLength);
}
}
public void setBackground(Context context, int resourceID){
if(context != null && resourceID != 0){
mIsUserBg = true;
setBgBitmap(BitmapFactory.decodeResource(context.getResources(),resourceID));
}
}
public void setBgBitmap(Bitmap bitmap) {
mBookBgBitmap = bitmap;
Matrix matrix = new Matrix();
int width = mBookBgBitmap.getWidth();// 获取资源位图的宽
int height = mBookBgBitmap.getHeight();// 获取资源位图的高
float w = (float) mWidth / (float) mBookBgBitmap.getWidth();
float h = (float) mHeight / (float) mBookBgBitmap.getHeight();
matrix.postScale(w, h);// 获取缩放比例
mBookBgBitmap = Bitmap.createBitmap(mBookBgBitmap, 0, 0, width, height, matrix,true);// 根据缩放比例获取新的位图
}
public void setUseBg(boolean useBg) {
mIsUserBg = useBg;
}
public boolean isFirstPage() {
return mIsFirstPage;
}
public boolean isLastPage() {
return mIsLastPage;
}
public int getEnd() {
return mBufferEnd;
}
public float getProgress() {
return (float) (mBufferBegin * 1.0 / mBookBufferLength);
}
public void setBookProgress(int progress) {
if (progress < 0) {
mBufferBegin = mBufferEnd = 0;
} else if (progress > mBookBufferLength) {
mBufferBegin = mBufferEnd = mBookBufferLength;
} else {
mBufferBegin = mBufferEnd = progress;
}
pageUp();
if (mBufferBegin != 0) {
mLinesVector.clear();
mLinesVector = pageDown();
mBufferBegin = mBufferEnd;
}
mLinesVector.clear();
mLinesVector = pageDown();
}
public void setProgress(int progress) {
if (progress < 0) {
mBufferBegin = mBufferEnd = 0;
} else if (progress > 100) {
mBufferBegin = mBufferEnd = mBookBufferLength;
} else {
mBufferBegin = mBufferEnd = (int) (((float) progress / 100) * mBookBufferLength);
}
pageUp();
if (mBufferBegin != 0) {
mLinesVector.clear();
mLinesVector = pageDown();
mBufferBegin = mBufferEnd;
}
mLinesVector.clear();
mLinesVector = pageDown();
}
public void setTextColor(int color) {
mPaint.setColor(color);
}
/**
* 设置接口
*
* @param onProgressChangedListener
*/
public void setOnProgressChangedListener(OnProgressChangedListener onProgressChangedListener) {
this.onProgressChangedListener = onProgressChangedListener;
}
/**
* 设置阅读进度的接口
*/
public interface OnProgressChangedListener {
/**
* 设置进度方法
*/
void setProgress(int currentLength, int totalLength);
}
public String getCurrentPageContent(){
return mLinesVector.toString();
}
}
五、项目源码
👇👇👇👇👇快捷方式👇👇👇👇👇