}
- * @return if file not exist, return null, else return content of file
- * @throws RuntimeException if an error occurs while operator BufferedReader
- */
- public static ArrayList readFileToList(String filePath, String charsetName) {
- File file = new File(filePath);
- ArrayList fileContent = new ArrayList();
- if (file == null || !file.isFile()) {
- return null;
- }
-
- BufferedReader reader = null;
- try {
- InputStreamReader is = new InputStreamReader(new FileInputStream(file), charsetName);
- reader = new BufferedReader(is);
- String line = null;
- while ((line = reader.readLine()) != null) {
- WePoint wePoint = GsonHelper.fromJson(line, WePoint.class);
- if (wePoint == null) continue;
- fileContent.add(wePoint);
- }
- reader.close();
- return fileContent;
- } catch (IOException e) {
- throw new RuntimeException("Flash test : +++++++++ IOException occurred. ", e);
- } finally {
- if (reader != null) {
- try {
- reader.close();
- } catch (IOException e) {
- throw new RuntimeException("Flash test : +++++++++ IOException occurred. ", e);
- }
- }
- }
- }
-
- public static ArrayList readAssetsFileToList(Context cxt, String filePath) {
- ArrayList fileContent = new ArrayList();
- BufferedReader reader = null;
- try {
- InputStreamReader is = new InputStreamReader(cxt.getAssets().open(filePath), "utf-8");
- reader = new BufferedReader(is);
- String line = null;
- while ((line = reader.readLine()) != null) {
- WePoint wePoint = GsonHelper.fromJson(line, WePoint.class);
- if (wePoint == null) continue;
- fileContent.add(wePoint);
- }
- reader.close();
- return fileContent;
- } catch (IOException e) {
- throw new RuntimeException("Flash test : +++++++++ IOException occurred. ", e);
- } finally {
- if (reader != null) {
- try {
- reader.close();
- } catch (IOException e) {
- throw new RuntimeException("Flash test : +++++++++ IOException occurred. ", e);
- }
- }
- }
-
- }
-
- /**
- * write file【写文件:字符串】
- *
- * @param filePath
- * @param content
- * @param append is append, if true, write to the end of file, else clear content of file and write into it
- * @return return false if content is empty, true otherwise
- * @throws RuntimeException if an error occurs while operator FileWriter
- */
- public static boolean writeFile(String filePath, String content, boolean append) {
- //字符串判空
- if (StringUtils.isEmpty(content)) {
- return false;
- }
-
- FileWriter fileWriter = null;
- try {
- makeDirs(filePath);
- fileWriter = new FileWriter(filePath, append);
- fileWriter.write(content);
- fileWriter.close();
- //updateGallery(filePath);//媒体库数据更新
- return true;
- } catch (IOException e) {
- throw new RuntimeException("IOException occurred. ", e);
- } finally {
- if (fileWriter != null) {
- try {
- fileWriter.close();
- } catch (IOException e) {
- throw new RuntimeException("IOException occurred. ", e);
- }
- }
- }
- }
-
-
- /**
- * write file
- *
- * @param file the file to be opened for writing.
- * @param stream the input stream
- * @param append if true, then bytes will be written to the end of the file rather than the beginning
- * @return return true
- * @throws RuntimeException if an error occurs while operator FileOutputStream
- */
- public static boolean writeFile(File file, InputStream stream, boolean append) {
- OutputStream o = null;
- try {
- makeDirs(file.getAbsolutePath());
- if (!file.exists()) file.createNewFile();
- o = new FileOutputStream(file, append);
- byte data[] = new byte[1024];
- int length = -1;
- while ((length = stream.read(data)) != -1) {
- o.write(data, 0, length);
- }
- o.flush();
- //updateGallery(file.getAbsolutePath());//媒体库数据更新
- return true;
- } catch (FileNotFoundException e) {
- throw new RuntimeException("FileNotFoundException occurred. ", e);
- } catch (IOException e) {
- throw new RuntimeException("IOException occurred. ", e);
- } finally {
- if (o != null) {
- try {
- o.close();
- stream.close();
- } catch (IOException e) {
- throw new RuntimeException("IOException occurred. ", e);
- }
- }
- }
- }
-
- public static boolean makeDirs(String filePath) {
- String folderName = getFolderName(filePath);
- if (StringUtils.isEmpty(folderName)) {
- return false;
- }
-
- File folder = new File(folderName);
- return (folder.exists() && folder.isDirectory()) ? true : folder.mkdirs();
- }
-
- public static String getFolderName(String filePath) {
-
- if (StringUtils.isEmpty(filePath)) {
- return filePath;
- }
-
- int filePosi = filePath.lastIndexOf(File.separator);
- return (filePosi == -1) ? "" : filePath.substring(0, filePosi);
- }
+ //采用自己的格式去设置文件,防止文件被系统文件查询到
+ public static final String SUFFIX_NB = ".nb";
+ public static final String SUFFIX_TXT = ".txt";
+ public static final String SUFFIX_EPUB = ".epub";
+ public static final String SUFFIX_PDF = ".pdf";
+
+ //获取文件夹
+ public static File getFolder(String filePath){
+ File file = new File(filePath);
+ //如果文件夹不存在,就创建它
+ if (!file.exists()){
+ file.mkdirs();
+ }
+ return file;
+ }
+
+ //获取文件
+ public static synchronized File getFile(String filePath){
+ File file = new File(filePath);
+ try {
+ if (!file.exists()){
+ //创建父类文件夹
+ getFolder(file.getParent());
+ //创建文件
+ file.createNewFile();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return file;
+ }
+
+ //获取Cache文件夹
+ public static String getCachePath(){
+ if (isSdCardExist()){
+ return App.Companion.getContext()
+ .getExternalCacheDir()
+ .getAbsolutePath();
+ }
+ else{
+ return App.Companion.getContext()
+ .getCacheDir()
+ .getAbsolutePath();
+ }
+ }
+
+ public static long getDirSize(File file){
+ //判断文件是否存在
+ if (file.exists()) {
+ //如果是目录则递归计算其内容的总大小
+ if (file.isDirectory()) {
+ File[] children = file.listFiles();
+ long size = 0;
+ for (File f : children)
+ size += getDirSize(f);
+ return size;
+ } else {
+ return file.length();
+ }
+ } else {
+ return 0;
+ }
+ }
+
+ public static String getFileSize(long size) {
+ if (size <= 0) return "0";
+ final String[] units = new String[]{"b", "kb", "M", "G", "T"};
+ //计算单位的,原理是利用lg,公式是 lg(1024^n) = nlg(1024),最后 nlg(1024)/lg(1024) = n。
+ int digitGroups = (int) (Math.log10(size) / Math.log10(1024));
+ //计算原理是,size/单位值。单位值指的是:比如说b = 1024,KB = 1024^2
+ return new DecimalFormat("#,##0.##").format(size / Math.pow(1024, digitGroups)) + " " + units[digitGroups];
+ }
+
+ /**
+ * 本来是获取File的内容的。但是为了解决文本缩进、换行的问题
+ * 这个方法就是专门用来获取书籍的...
+ *
+ * 应该放在BookRepository中。。。
+ * @param file
+ * @return
+ */
+ public static String getFileContent(File file){
+ Reader reader = null;
+ String str = null;
+ StringBuilder sb = new StringBuilder();
+ try {
+ reader = new FileReader(file);
+ BufferedReader br = new BufferedReader(reader);
+ while ((str = br.readLine()) != null){
+ //过滤空语句
+ if (!str.equals("")){
+ //由于sb会自动过滤\n,所以需要加上去
+ sb.append(" "+str+"\n");
+ }
+ }
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }finally {
+ IOUtils.close(reader);
+ }
+ return sb.toString();
+ }
+
+ //判断是否挂载了SD卡
+ public static boolean isSdCardExist(){
+ if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())){
+ return true;
+ }
+ return false;
+ }
+
+ //递归删除文件夹下的数据
+ public static synchronized void deleteFile(String filePath){
+ File file = new File(filePath);
+ if (!file.exists()) return;
+
+ if (file.isDirectory()){
+ File[] files = file.listFiles();
+ for (File subFile : files){
+ String path = subFile.getPath();
+ deleteFile(path);
+ }
+ }
+ //删除文件
+ file.delete();
+ }
+
+ //由于递归的耗时问题,取巧只遍历内部三层
+
+ //获取txt文件
+ public static List getTxtFiles(String filePath,int layer){
+ List txtFiles = new ArrayList();
+ File file = new File(filePath);
+
+ //如果层级为 3,则直接返回
+ if (layer == 3){
+ return txtFiles;
+ }
+
+ //获取文件夹
+ File[] dirs = file.listFiles(
+ pathname -> {
+ if (pathname.isDirectory() && !pathname.getName().startsWith(".")) {
+ return true;
+ }
+ //获取txt文件
+ else if(pathname.getName().endsWith(".txt")){
+ txtFiles.add(pathname);
+ return false;
+ }
+ else{
+ return false;
+ }
+ }
+ );
+ //遍历文件夹
+ for (File dir : dirs){
+ //递归遍历txt文件
+ txtFiles.addAll(getTxtFiles(dir.getPath(),layer + 1));
+ }
+ return txtFiles;
+ }
+
+ //由于遍历比较耗时
+ public static Single> getSDTxtFile(){
+ //外部存储卡路径
+ String rootPath = Environment.getExternalStorageDirectory().getPath();
+ return Single.create(new SingleOnSubscribe>() {
+ @Override
+ public void subscribe(SingleEmitter> e) throws Exception {
+ List files = getTxtFiles(rootPath,0);
+ e.onSuccess(files);
+ }
+ });
+ }
+
+ //获取文件的编码格式
+ public static Charset getCharset(String fileName) {
+ BufferedInputStream bis = null;
+ Charset charset = Charset.GBK;
+ byte[] first3Bytes = new byte[3];
+ try {
+ boolean checked = false;
+ bis = new BufferedInputStream(new FileInputStream(fileName));
+ bis.mark(0);
+ int read = bis.read(first3Bytes, 0, 3);
+ if (read == -1)
+ return charset;
+ if (first3Bytes[0] == (byte) 0xEF
+ && first3Bytes[1] == (byte) 0xBB
+ && first3Bytes[2] == (byte) 0xBF) {
+ charset = Charset.UTF8;
+ checked = true;
+ }
+ /*
+ * 不支持 UTF16LE 和 UTF16BE
+ else if (first3Bytes[0] == (byte) 0xFF && first3Bytes[1] == (byte) 0xFE) {
+ charset = Charset.UTF16LE;
+ checked = true;
+ } else if (first3Bytes[0] == (byte) 0xFE
+ && first3Bytes[1] == (byte) 0xFF) {
+ charset = Charset.UTF16BE;
+ checked = true;
+ } else */
+ bis.mark(0);
+ if (!checked) {
+ while ((read = bis.read()) != -1) {
+ if (read >= 0xF0)
+ break;
+ if (0x80 <= read && read <= 0xBF) // 单独出现BF以下的,也算是GBK
+ break;
+ if (0xC0 <= read && read <= 0xDF) {
+ read = bis.read();
+ if (0x80 <= read && read <= 0xBF) // 双字节 (0xC0 - 0xDF)
+ // (0x80 - 0xBF),也可能在GB编码内
+ continue;
+ else
+ break;
+ } else if (0xE0 <= read && read <= 0xEF) {// 也有可能出错,但是几率较小
+ read = bis.read();
+ if (0x80 <= read && read <= 0xBF) {
+ read = bis.read();
+ if (0x80 <= read && read <= 0xBF) {
+ charset = Charset.UTF8;
+ break;
+ } else
+ break;
+ } else
+ break;
+ }
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ IOUtils.close(bis);
+ }
+ return charset;
+ }
}
diff --git a/app/src/main/java/com/yzx/webebook/utils/IOUtils.java b/app/src/main/java/com/yzx/webebook/utils/IOUtils.java
new file mode 100644
index 0000000..adbb5dc
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/utils/IOUtils.java
@@ -0,0 +1,21 @@
+package com.yzx.webebook.utils;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+/**
+ * Created by newbiechen on 17-5-11.
+ */
+
+public class IOUtils {
+
+ public static void close(Closeable closeable){
+ if (closeable == null) return;
+ try {
+ closeable.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ //close error
+ }
+ }
+}
diff --git a/app/src/main/java/com/yzx/webebook/utils/LogUtils.java b/app/src/main/java/com/yzx/webebook/utils/LogUtils.java
new file mode 100644
index 0000000..3209f40
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/utils/LogUtils.java
@@ -0,0 +1,222 @@
+package com.yzx.webebook.utils;
+
+import android.content.Context;
+import android.os.Environment;
+import android.util.Log;
+
+import com.yzx.webebook.App;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+
+/**
+ * Created by newbiechen on 17-4-27.
+ */
+
+public class LogUtils {
+ private static Boolean LOG_SWITCH = true; // 日志文件总开关
+ private static Boolean LOG_TO_FILE = false; // 日志写入文件开关
+ private static String LOG_TAG = "IReader"; // 默认的tag
+ private static char LOG_TYPE = 'v';// 输入日志类型,v代表输出所有信息,w则只输出警告...
+ private static int LOG_SAVE_DAYS = 7;// sd卡中日志文件的最多保存天数
+
+ private final static SimpleDateFormat LOG_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// 日志的输出格式
+ private final static SimpleDateFormat FILE_SUFFIX = new SimpleDateFormat("yyyy-MM-dd");// 日志文件格式
+ private static String LOG_FILE_PATH; // 日志文件保存路径
+ private static String LOG_FILE_NAME;// 日志文件保存名称
+
+ public static void init(Context context) { // 在Application中初始化
+ LOG_FILE_PATH = Environment.getExternalStorageDirectory().getPath() + File.separator + App.Companion.getContext().getPackageName();
+ LOG_FILE_NAME = "Log";
+ }
+
+ /****************************
+ * Warn
+ *********************************/
+ public static void w(Object msg) {
+ w(LOG_TAG, msg);
+ }
+
+ public static void w(String tag, Object msg) {
+ w(tag, msg, null);
+ }
+
+ public static void w(String tag, Object msg, Throwable tr) {
+ if (msg == null) return;
+ log(tag, msg.toString(), tr, 'w');
+ }
+
+ /***************************
+ * Error
+ ********************************/
+ public static void e(Object msg) {
+ e(LOG_TAG, msg);
+ }
+
+ public static void e(String tag, Object msg) {
+ e(tag, msg, null);
+ }
+
+ public static void e(String tag, Object msg, Throwable tr) {
+ if (msg == null) return;
+ log(tag, msg.toString(), tr, 'e');
+ }
+
+ /***************************
+ * Debug
+ ********************************/
+ public static void d(Object msg) {
+ d(LOG_TAG, msg);
+ }
+
+ public static void d(String tag, Object msg) {// 调试信息
+ d(tag, msg, null);
+ }
+
+ public static void d(String tag, Object msg, Throwable tr) {
+ if (msg == null) return;
+ log(tag, msg.toString(), tr, 'd');
+ }
+
+ /****************************
+ * Info
+ *********************************/
+ public static void i(Object msg) {
+ i(LOG_TAG, msg);
+ }
+
+ public static void i(String tag, Object msg) {
+ i(tag, msg, null);
+ }
+
+ public static void i(String tag, Object msg, Throwable tr) {
+ if (msg == null) return;
+ log(tag, msg.toString(), tr, 'i');
+ }
+
+ /**************************
+ * Verbose
+ ********************************/
+ public static void v(Object msg) {
+ v(LOG_TAG, msg);
+ }
+
+ public static void v(String tag, Object msg) {
+ v(tag, msg, null);
+ }
+
+ public static void v(String tag, Object msg, Throwable tr) {
+ if (msg == null) return;
+ log(tag, msg.toString(), tr, 'v');
+ }
+
+ /**
+ * 根据tag, msg和等级,输出日志
+ *
+ * @param tag
+ * @param msg
+ * @param level
+ */
+ private static void log(String tag, String msg, Throwable tr, char level) {
+ if (tag == null || msg == null || tr == null) return;
+ if (LOG_SWITCH) {
+ if ('e' == level && ('e' == LOG_TYPE || 'v' == LOG_TYPE)) { // 输出错误信息
+ Log.e(tag, createMessage(msg), tr);
+ } else if ('w' == level && ('w' == LOG_TYPE || 'v' == LOG_TYPE)) {
+ Log.w(tag, createMessage(msg), tr);
+ } else if ('d' == level && ('d' == LOG_TYPE || 'v' == LOG_TYPE)) {
+ Log.d(tag, createMessage(msg), tr);
+ } else if ('i' == level && ('d' == LOG_TYPE || 'v' == LOG_TYPE)) {
+ Log.i(tag, createMessage(msg), tr);
+ } else {
+ Log.v(tag, createMessage(msg), tr);
+ }
+ if (LOG_TO_FILE)
+ log2File(String.valueOf(level), tag, msg + tr == null ? "" : "\n" + Log.getStackTraceString(tr));
+ }
+ }
+
+ private static String getFunctionName() {
+ StackTraceElement[] sts = Thread.currentThread().getStackTrace();
+ if (sts == null) {
+ return null;
+ }
+ for (StackTraceElement st : sts) {
+ if (st.isNativeMethod()) {
+ continue;
+ }
+ if (st.getClassName().equals(Thread.class.getName())) {
+ continue;
+ }
+ if (st.getFileName().equals("LogUtils.java")) {
+ continue;
+ }
+ return "[" + Thread.currentThread().getName() + "("
+ + Thread.currentThread().getId() + "): " + st.getFileName()
+ + ":" + st.getLineNumber() + "]";
+ }
+ return null;
+ }
+
+ private static String createMessage(String msg) {
+ String functionName = getFunctionName();
+ String message = (functionName == null ? msg
+ : (functionName + " - " + msg));
+ return message;
+ }
+
+ /**
+ * 打开日志文件并写入日志
+ *
+ * @return
+ **/
+ private synchronized static void log2File(String mylogtype, String tag, String text) {
+ Date nowtime = new Date();
+ String date = FILE_SUFFIX.format(nowtime);
+ String dateLogContent = LOG_FORMAT.format(nowtime) + ":" + mylogtype + ":" + tag + ":" + text; // 日志输出格式
+ File destDir = new File(LOG_FILE_PATH);
+ if (!destDir.exists()) {
+ destDir.mkdirs();
+ }
+ File file = new File(LOG_FILE_PATH, LOG_FILE_NAME + date);
+ try {
+ FileWriter filerWriter = new FileWriter(file, true);
+ BufferedWriter bufWriter = new BufferedWriter(filerWriter);
+ bufWriter.write(dateLogContent);
+ bufWriter.newLine();
+ bufWriter.close();
+ filerWriter.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * 删除指定的日志文件
+ */
+ public static void delFile() {// 删除日志文件
+ String needDelFiel = FILE_SUFFIX.format(getDateBefore());
+ File file = new File(LOG_FILE_PATH, needDelFiel + LOG_FILE_NAME);
+ if (file.exists()) {
+ file.delete();
+ }
+ }
+
+ /**
+ * 得到LOG_SAVE_DAYS天前的日期
+ *
+ * @return
+ */
+ private static Date getDateBefore() {
+ Date nowtime = new Date();
+ Calendar now = Calendar.getInstance();
+ now.setTime(nowtime);
+ now.set(Calendar.DATE, now.get(Calendar.DATE) - LOG_SAVE_DAYS);
+ return now.getTime();
+ }
+}
diff --git a/app/src/main/java/com/yzx/webebook/utils/MD5Utils.java b/app/src/main/java/com/yzx/webebook/utils/MD5Utils.java
new file mode 100644
index 0000000..e85825d
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/utils/MD5Utils.java
@@ -0,0 +1,43 @@
+package com.yzx.webebook.utils;
+
+/**
+ * Created by newbiechen on 2018/1/1.
+ */
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ *@Description: 将字符串转化为MD5
+ */
+
+public class MD5Utils {
+
+ public static String strToMd5By32(String str){
+ String reStr = null;
+ try {
+ MessageDigest md5 = MessageDigest.getInstance("MD5");
+ byte[] bytes = md5.digest(str.getBytes());
+ StringBuffer stringBuffer = new StringBuffer();
+ for (byte b : bytes){
+ int bt = b&0xff;
+ if (bt < 16){
+ stringBuffer.append(0);
+ }
+ stringBuffer.append(Integer.toHexString(bt));
+ }
+ reStr = stringBuffer.toString();
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ }
+ return reStr;
+ }
+
+ public static String strToMd5By16(String str){
+ String reStr = strToMd5By32(str);
+ if (reStr != null){
+ reStr = reStr.substring(8, 24);
+ }
+ return reStr;
+ }
+}
diff --git a/app/src/main/java/com/yzx/webebook/utils/NetworkUtils.java b/app/src/main/java/com/yzx/webebook/utils/NetworkUtils.java
new file mode 100644
index 0000000..e755c2b
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/utils/NetworkUtils.java
@@ -0,0 +1,63 @@
+package com.yzx.webebook.utils;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+
+import com.yzx.webebook.App;
+
+/**
+ * Created by newbiechen on 17-5-11.
+ */
+
+public class NetworkUtils {
+
+
+ /**
+ * 获取活动网络信息
+ * @return NetworkInfo
+ */
+ public static NetworkInfo getNetworkInfo(){
+ ConnectivityManager cm = (ConnectivityManager) App.Companion
+ .getContext()
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
+
+ return cm.getActiveNetworkInfo();
+ }
+
+ /**
+ * 网络是否可用
+ * @return
+ */
+ public static boolean isAvailable(){
+ NetworkInfo info = getNetworkInfo();
+ return info != null && info.isAvailable();
+ }
+
+ /**
+ * 网络是否连接
+ * @return
+ */
+ public static boolean isConnected(){
+ NetworkInfo info = getNetworkInfo();
+ return info != null && info.isConnected();
+ }
+
+ /**
+ * 判断wifi是否连接状态
+ * 需添加权限 {@code }
+ *
+ * @param context 上下文
+ * @return {@code true}: 连接
{@code false}: 未连接
+ */
+ public static boolean isWifiConnected(Context context) {
+ ConnectivityManager cm = (ConnectivityManager) context
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
+ return cm != null && cm.getActiveNetworkInfo() != null
+ && cm.getActiveNetworkInfo().getType() == ConnectivityManager.TYPE_WIFI;
+ }
+
+
+
+}
diff --git a/app/src/main/java/com/yzx/webebook/utils/PermissionsChecker.java b/app/src/main/java/com/yzx/webebook/utils/PermissionsChecker.java
new file mode 100644
index 0000000..0b276f2
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/utils/PermissionsChecker.java
@@ -0,0 +1,34 @@
+package com.yzx.webebook.utils;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+
+import androidx.core.content.ContextCompat;
+
+/**
+ * Created by newbiechen on 2017/10/8.
+ */
+
+public class PermissionsChecker {
+ private final Context mContext;
+
+ public PermissionsChecker(Context context) {
+ mContext = context.getApplicationContext();
+ }
+
+ // 判断权限集合
+ public boolean lacksPermissions(String... permissions) {
+ for (String permission : permissions) {
+ if (lacksPermission(permission)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // 判断是否缺少权限
+ private boolean lacksPermission(String permission) {
+ return ContextCompat.checkSelfPermission(mContext, permission) ==
+ PackageManager.PERMISSION_DENIED;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/yzx/webebook/utils/RxUtils.java b/app/src/main/java/com/yzx/webebook/utils/RxUtils.java
new file mode 100644
index 0000000..dc4e16f
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/utils/RxUtils.java
@@ -0,0 +1,60 @@
+package com.yzx.webebook.utils;
+
+
+
+import com.yzx.webebook.model.bean.CommentBean;
+import com.yzx.webebook.model.bean.DetailBean;
+
+import java.util.List;
+
+import io.reactivex.Observable;
+import io.reactivex.ObservableSource;
+import io.reactivex.Single;
+import io.reactivex.SingleSource;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.functions.Function3;
+import io.reactivex.schedulers.Schedulers;
+
+/**
+ * Created by newbiechen on 17-4-29.
+ */
+
+public class RxUtils {
+
+ public static SingleSource toSimpleSingle(Single upstream){
+ return upstream.subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread());
+ }
+
+ public static ObservableSource toSimpleSingle(Observable upstream){
+ return upstream.subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread());
+ }
+
+ public static TwoTuple twoTuple(T first,R second){
+ return new TwoTuple(first, second);
+ }
+
+ public static Single> toCommentDetail(Single detailSingle,
+ Single> bestCommentsSingle,
+ Single> commentsSingle){
+ return Single.zip(detailSingle, bestCommentsSingle, commentsSingle,
+ new Function3, List, DetailBean>() {
+ @Override
+ public DetailBean apply(T t, List commentBeen,
+ List commentBeen2) throws Exception {
+ return new DetailBean(t,commentBeen,commentBeen2);
+ }
+ });
+ }
+
+ public static class TwoTuple {
+ public final A first;
+ public final B second;
+
+ public TwoTuple(A a, B b) {
+ this.first = a;
+ this.second = b;
+ }
+ }
+}
diff --git a/app/src/main/java/com/yzx/webebook/utils/ScreenUtils.java b/app/src/main/java/com/yzx/webebook/utils/ScreenUtils.java
new file mode 100644
index 0000000..67f1b73
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/utils/ScreenUtils.java
@@ -0,0 +1,127 @@
+package com.yzx.webebook.utils;
+
+import android.content.res.Resources;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.view.View;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.yzx.webebook.App;
+
+import java.lang.reflect.Method;
+
+/**
+ * Created by newbiechen on 17-5-1.
+ */
+
+public class ScreenUtils {
+
+ public static int dpToPx(int dp) {
+ DisplayMetrics metrics = getDisplayMetrics();
+ return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, metrics);
+ }
+
+ public static int pxToDp(int px) {
+ DisplayMetrics metrics = getDisplayMetrics();
+ return (int) (px / metrics.density);
+ }
+
+ public static int spToPx(int sp) {
+ DisplayMetrics metrics = getDisplayMetrics();
+ return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, metrics);
+ }
+
+ public static int pxToSp(int px) {
+ DisplayMetrics metrics = getDisplayMetrics();
+ return (int) (px / metrics.scaledDensity);
+ }
+
+ /**
+ * 获取手机显示App区域的大小(头部导航栏+ActionBar+根布局),不包括虚拟按钮
+ *
+ * @return
+ */
+ public static int[] getAppSize() {
+ int[] size = new int[2];
+ DisplayMetrics metrics = getDisplayMetrics();
+ size[0] = metrics.widthPixels;
+ size[1] = metrics.heightPixels;
+ return size;
+ }
+
+ /**
+ * 获取整个手机屏幕的大小(包括虚拟按钮)
+ * 必须在onWindowFocus方法之后使用
+ *
+ * @param activity
+ * @return
+ */
+ public static int[] getScreenSize(AppCompatActivity activity) {
+ int[] size = new int[2];
+ View decorView = activity.getWindow().getDecorView();
+ size[0] = decorView.getWidth();
+ size[1] = decorView.getHeight();
+ return size;
+ }
+
+ /**
+ * 获取导航栏的高度
+ *
+ * @return
+ */
+ public static int getStatusBarHeight() {
+ Resources resources = App.Companion.getContext().getResources();
+ int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android");
+ return resources.getDimensionPixelSize(resourceId);
+ }
+
+ /**
+ * 获取虚拟按键的高度
+ *
+ * @return
+ */
+ public static int getNavigationBarHeight() {
+ int navigationBarHeight = 0;
+ Resources rs = App.Companion.getContext().getResources();
+ int id = rs.getIdentifier("navigation_bar_height", "dimen", "android");
+ if (id > 0 && hasNavigationBar()) {
+ navigationBarHeight = rs.getDimensionPixelSize(id);
+ }
+ return navigationBarHeight;
+ }
+
+ /**
+ * 是否存在虚拟按键
+ *
+ * @return
+ */
+ private static boolean hasNavigationBar() {
+ boolean hasNavigationBar = false;
+ Resources rs = App.Companion.getContext().getResources();
+ int id = rs.getIdentifier("config_showNavigationBar", "bool", "android");
+ if (id > 0) {
+ hasNavigationBar = rs.getBoolean(id);
+ }
+ try {
+ Class systemPropertiesClass = Class.forName("android.os.SystemProperties");
+ Method m = systemPropertiesClass.getMethod("get", String.class);
+ String navBarOverride = (String) m.invoke(systemPropertiesClass, "qemu.hw.mainkeys");
+ if ("1".equals(navBarOverride)) {
+ hasNavigationBar = false;
+ } else if ("0".equals(navBarOverride)) {
+ hasNavigationBar = true;
+ }
+ } catch (Exception e) {
+ }
+ return hasNavigationBar;
+ }
+
+ public static DisplayMetrics getDisplayMetrics() {
+ DisplayMetrics metrics = App.Companion
+ .getContext()
+ .getResources()
+ .getDisplayMetrics();
+ return metrics;
+ }
+}
diff --git a/app/src/main/java/com/yzx/webebook/utils/SharedPreUtils.java b/app/src/main/java/com/yzx/webebook/utils/SharedPreUtils.java
new file mode 100644
index 0000000..c46e24d
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/utils/SharedPreUtils.java
@@ -0,0 +1,61 @@
+package com.yzx.webebook.utils;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import com.yzx.webebook.App;
+
+/**
+ * Created by newbiechen on 17-4-16.
+ */
+
+public class SharedPreUtils {
+ private static final String SHARED_NAME = "IReader_pref";
+ private static SharedPreUtils sInstance;
+ private SharedPreferences sharedReadable;
+ private SharedPreferences.Editor sharedWritable;
+
+ private SharedPreUtils(){
+ sharedReadable = App.Companion.getContext()
+ .getSharedPreferences(SHARED_NAME, Context.MODE_MULTI_PROCESS);
+ sharedWritable = sharedReadable.edit();
+ }
+
+ public static SharedPreUtils getInstance(){
+ if(sInstance == null){
+ synchronized (SharedPreUtils.class){
+ if (sInstance == null){
+ sInstance = new SharedPreUtils();
+ }
+ }
+ }
+ return sInstance;
+ }
+
+ public String getString(String key){
+ return sharedReadable.getString(key,"");
+ }
+
+ public void putString(String key,String value){
+ sharedWritable.putString(key,value);
+ sharedWritable.commit();
+ }
+
+ public void putInt(String key,int value){
+ sharedWritable.putInt(key, value);
+ sharedWritable.commit();
+ }
+
+ public void putBoolean(String key,boolean value){
+ sharedWritable.putBoolean(key, value);
+ sharedWritable.commit();
+ }
+
+ public int getInt(String key,int def){
+ return sharedReadable.getInt(key, def);
+ }
+
+ public boolean getBoolean(String key,boolean def){
+ return sharedReadable.getBoolean(key, def);
+ }
+}
diff --git a/app/src/main/java/com/yzx/webebook/utils/StringUtils.java b/app/src/main/java/com/yzx/webebook/utils/StringUtils.java
index e9793c5..691b8f9 100644
--- a/app/src/main/java/com/yzx/webebook/utils/StringUtils.java
+++ b/app/src/main/java/com/yzx/webebook/utils/StringUtils.java
@@ -1,332 +1,190 @@
package com.yzx.webebook.utils;
-import android.text.TextUtils;
+import android.content.Context;
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+import androidx.annotation.StringRes;
-/**
- * String Utils
- *
- * @author andrew 2015-9-2
- */
-public class StringUtils {
-
- private StringUtils() {
- throw new AssertionError();
- }
-
- /**
- * is null or its length is 0 or it is made by space【字符串为空、长度为0、一个空格】
- *
- *
- * isBlank(null) = true;
- * isBlank("") = true;
- * isBlank(" ") = true;
- * isBlank("a") = false;
- * isBlank("a ") = false;
- * isBlank(" a") = false;
- * isBlank("a b") = false;
- *
- *
- * @param str
- * @return if string is null or its size is 0 or it is made by space, return true, else return false.
- */
- public static boolean isBlank(String str) {
- return (str == null || str.trim().length() == 0);
- }
-
- /**
- * is null or its length is 0【字符串为空、长度为0】
- *
- *
- * isEmpty(null) = true;
- * isEmpty("") = true;
- * isEmpty(" ") = false;
- *
- *
- * @param str
- * @return if string is null or its size is 0, return true, else return false.
- */
- public static boolean isEmpty(CharSequence str) {
- return (str == null || str.length() == 0);
- }
-
- public static boolean isEqual(String paramString1, String paramString2) {
- return paramString1.equals(paramString2);
- }
-
- /**
- * get length of CharSequence【字符串的长度】
- *
- *
- * length(null) = 0;
- * length(\"\") = 0;
- * length(\"abc\") = 3;
- *
- *
- * @param str
- * @return if str is null or empty, return 0, else return {@link CharSequence#length()}.
- */
- public static int length(CharSequence str) {
- return str == null ? 0 : str.length();
- }
-
- /**
- * null Object to empty string
- *
- *
- * nullStrToEmpty(null) = "";
- * nullStrToEmpty("") = "";
- * nullStrToEmpty("aa") = "aa";
- *
- *
- * @param str
- * @return
- */
- public static String nullStrToEmpty(Object str) {
- return (str == null ? "" : (str instanceof String ? (String)str : str.toString()));
- }
-
- /**
- * capitalize first letter
- *
- *
- * capitalizeFirstLetter(null) = null;
- * capitalizeFirstLetter("") = "";
- * capitalizeFirstLetter("2ab") = "2ab"
- * capitalizeFirstLetter("a") = "A"
- * capitalizeFirstLetter("ab") = "Ab"
- * capitalizeFirstLetter("Abc") = "Abc"
- *
- *
- * @param str
- * @return
- */
- public static String capitalizeFirstLetter(String str) {
- if (isEmpty(str)) {
- return str;
- }
-
- char c = str.charAt(0);
- return (!Character.isLetter(c) || Character.isUpperCase(c)) ? str : new StringBuilder(str.length())
- .append(Character.toUpperCase(c)).append(str.substring(1)).toString();
- }
-
- /**
- * encoded in utf-8
- *
- *
- * utf8Encode(null) = null
- * utf8Encode("") = "";
- * utf8Encode("aa") = "aa";
- * utf8Encode("啊啊啊啊") = "%E5%95%8A%E5%95%8A%E5%95%8A%E5%95%8A";
- *
- *
- * @param str
- * @return
- * @throws UnsupportedEncodingException if an error occurs
- */
- public static String utf8Encode(String str) {
- if (!isEmpty(str) && str.getBytes().length != str.length()) {
- try {
- return URLEncoder.encode(str, "UTF-8");
- } catch (UnsupportedEncodingException e) {
- throw new RuntimeException("UnsupportedEncodingException occurred. ", e);
- }
- }
- return str;
- }
+import com.yzx.webebook.App;
- /**
- * encoded in utf-8, if exception, return defultReturn
- *
- * @param str
- * @param defultReturn
- * @return
- */
- public static String utf8Encode(String str, String defultReturn) {
- if (!isEmpty(str) && str.getBytes().length != str.length()) {
- try {
- return URLEncoder.encode(str, "UTF-8");
- } catch (UnsupportedEncodingException e) {
- return defultReturn;
- }
- }
- return str;
- }
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
- /**
- * get innerHtml from href
- *
- *
- * getHrefInnerHtml(null) = ""
- * getHrefInnerHtml("") = ""
- * getHrefInnerHtml("mp3") = "mp3";
- * getHrefInnerHtml("<a innerHtml</a>") = "<a innerHtml</a>";
- * getHrefInnerHtml("<a>innerHtml</a>") = "innerHtml";
- * getHrefInnerHtml("<a<a>innerHtml</a>") = "innerHtml";
- * getHrefInnerHtml("<a href="baidu.com">innerHtml</a>") = "innerHtml";
- * getHrefInnerHtml("<a href="baidu.com" title="baidu">innerHtml</a>") = "innerHtml";
- * getHrefInnerHtml(" <a>innerHtml</a> ") = "innerHtml";
- * getHrefInnerHtml("<a>innerHtml</a></a>") = "innerHtml";
- * getHrefInnerHtml("jack<a>innerHtml</a></a>") = "innerHtml";
- * getHrefInnerHtml("<a>innerHtml1</a><a>innerHtml2</a>") = "innerHtml2";
- *
- *
- * @param href
- * @return
- * - if href is null, return ""
- * - if not match regx, return source
- * - return the last string that match regx
- *
- */
- public static String getHrefInnerHtml(String href) {
- if (isEmpty(href)) {
- return "";
- }
+//import com.zqc.opencc.android.lib.ChineseConverter;
+//import com.zqc.opencc.android.lib.ConversionType;
- String hrefReg = ".*<[\\s]*a[\\s]*.*>(.+?)<[\\s]*/a[\\s]*>.*";
- Pattern hrefPattern = Pattern.compile(hrefReg, Pattern.CASE_INSENSITIVE);
- Matcher hrefMatcher = hrefPattern.matcher(href);
- if (hrefMatcher.matches()) {
- return hrefMatcher.group(1);
- }
- return href;
- }
-
- /**
- * process special char in html
- *
- *
- * htmlEscapeCharsToString(null) = null;
- * htmlEscapeCharsToString("") = "";
- * htmlEscapeCharsToString("mp3") = "mp3";
- * htmlEscapeCharsToString("mp3<") = "mp3<";
- * htmlEscapeCharsToString("mp3>") = "mp3\>";
- * htmlEscapeCharsToString("mp3&mp4") = "mp3&mp4";
- * htmlEscapeCharsToString("mp3"mp4") = "mp3\"mp4";
- * htmlEscapeCharsToString("mp3<>&"mp4") = "mp3\<\>&\"mp4";
- *
- *
- * @param source
- * @return
- */
- public static String htmlEscapeCharsToString(String source) {
- return StringUtils.isEmpty(source) ? source : source.replaceAll("<", "<").replaceAll(">", ">")
- .replaceAll("&", "&").replaceAll(""", "\"");
- }
-
- /**
- * transform half width char to full width char
- *
- *
- * fullWidthToHalfWidth(null) = null;
- * fullWidthToHalfWidth("") = "";
- * fullWidthToHalfWidth(new String(new char[] {12288})) = " ";
- * fullWidthToHalfWidth("!"#$%&) = "!\"#$%&";
- *
- *
- * @param s
- * @return
- */
- public static String fullWidthToHalfWidth(String s) {
- if (isEmpty(s)) {
- return s;
- }
-
- char[] source = s.toCharArray();
- for (int i = 0; i < source.length; i++) {
- if (source[i] == 12288) {
- source[i] = ' ';
- // } else if (source[i] == 12290) {
- // source[i] = '.';
- } else if (source[i] >= 65281 && source[i] <= 65374) {
- source[i] = (char)(source[i] - 65248);
- } else {
- source[i] = source[i];
- }
- }
- return new String(source);
- }
-
- /**
- * transform full width char to half width char
- *
- *
- * halfWidthToFullWidth(null) = null;
- * halfWidthToFullWidth("") = "";
- * halfWidthToFullWidth(" ") = new String(new char[] {12288});
- * halfWidthToFullWidth("!\"#$%&) = "!"#$%&";
- *
- *
- * @param s
- * @return
- */
- public static String halfWidthToFullWidth(String s) {
- if (isEmpty(s)) {
- return s;
- }
-
- char[] source = s.toCharArray();
- for (int i = 0; i < source.length; i++) {
- if (source[i] == ' ') {
- source[i] = (char)12288;
- // } else if (source[i] == '.') {
- // source[i] = (char)12290;
- } else if (source[i] >= 33 && source[i] <= 126) {
- source[i] = (char)(source[i] + 65248);
- } else {
- source[i] = source[i];
- }
- }
- return new String(source);
- }
-
- private static String bytesToHexString(byte[] paramArrayOfByte) {
-
- StringBuilder localStringBuilder = new StringBuilder();
- for (int i = 0; i < paramArrayOfByte.length; i++)
- {
- String str = Integer.toHexString(0xFF & paramArrayOfByte[i]);
- if (str.length() == 1)
- localStringBuilder.append('0');
- localStringBuilder.append(str);
- }
- return localStringBuilder.toString();
- }
-
- public static String hashKey(String paramString) {
- String localObject = "";
- if (!TextUtils.isEmpty(paramString));
- try
- {
- MessageDigest localMessageDigest = MessageDigest.getInstance("MD5");
- localMessageDigest.update(paramString.getBytes());
- String str = bytesToHexString(localMessageDigest.digest());
- localObject = str;
- return localObject;
- }
- catch (NoSuchAlgorithmException localNoSuchAlgorithmException)
- {
- }
- return String.valueOf(paramString.hashCode());
- }
-
- public static String stripLeadingSlash(String paramString) {
+/**
+ * Created by newbiechen on 17-4-22.
+ * 对文字操作的工具类
+ */
- if (TextUtils.isEmpty(paramString))
- {
- return paramString;
- }
- int i = 0;
- while (paramString.charAt(i) == '/'){
- i ++;
- }
- return paramString.substring(i);
- }
+public class StringUtils {
+ private static final String TAG = "StringUtils";
+ private static final int HOUR_OF_DAY = 24;
+ private static final int DAY_OF_YESTERDAY = 2;
+ private static final int TIME_UNIT = 60;
+
+ //将时间转换成日期
+ public static String dateConvert(long time,String pattern){
+ Date date = new Date(time);
+ SimpleDateFormat format = new SimpleDateFormat(pattern);
+ return format.format(date);
+ }
+
+ //将日期转换成昨天、今天、明天
+ public static String dateConvert(String source,String pattern){
+
+ DateFormat format = new SimpleDateFormat(pattern);
+ Calendar calendar = Calendar.getInstance();
+ try {
+ Date date = format.parse(source);
+ long curTime = calendar.getTimeInMillis();
+ calendar.setTime(date);
+ //将MISC 转换成 sec
+ long difSec = Math.abs((curTime - date.getTime())/1000);
+ long difMin = difSec/60;
+ long difHour = difMin/60;
+ long difDate = difHour/60;
+ int oldHour = calendar.get(Calendar.HOUR);
+ //如果没有时间
+ if (oldHour == 0){
+ //比日期:昨天今天和明天
+ if (difDate == 0){
+ return "今天";
+ }
+ else if (difDate < DAY_OF_YESTERDAY){
+ return "昨天";
+ }
+ else {
+ DateFormat convertFormat = new SimpleDateFormat("yyyy-MM-dd");
+ String value = convertFormat.format(date);
+ return value;
+ }
+ }
+
+ if (difSec < TIME_UNIT){
+ return difSec+"秒前";
+ }
+ else if (difMin < TIME_UNIT){
+ return difMin+"分钟前";
+ }
+ else if (difHour < HOUR_OF_DAY){
+ return difHour+"小时前";
+ }
+ else if (difDate < DAY_OF_YESTERDAY){
+ return "昨天";
+ }
+ else {
+ DateFormat convertFormat = new SimpleDateFormat("yyyy-MM-dd");
+ String value = convertFormat.format(date);
+ return value;
+ }
+ } catch (ParseException e) {
+ e.printStackTrace();
+ }
+ return "";
+ }
+
+ public static String toFirstCapital(String str){
+ return str.substring(0,1).toUpperCase()+str.substring(1);
+ }
+
+ public static String getString(@StringRes int id){
+ return App.Companion.getContext().getResources().getString(id);
+ }
+
+ public static String getString(@StringRes int id, Object... formatArgs){
+ return App.Companion.getContext().getResources().getString(id,formatArgs);
+ }
+
+ /**
+ * 将文本中的半角字符,转换成全角字符
+ * @param input
+ * @return
+ */
+ public static String halfToFull(String input)
+ {
+ char[] c = input.toCharArray();
+ for (int i = 0; i< c.length; i++)
+ {
+ if (c[i] == 32) //半角空格
+ {
+ c[i] = (char) 12288;
+ continue;
+ }
+ //根据实际情况,过滤不需要转换的符号
+ //if (c[i] == 46) //半角点号,不转换
+ // continue;
+
+ if (c[i]> 32 && c[i]< 127) //其他符号都转换为全角
+ c[i] = (char) (c[i] + 65248);
+ }
+ return new String(c);
+ }
+
+ //功能:字符串全角转换为半角
+ public static String fullToHalf(String input)
+ {
+ char[] c = input.toCharArray();
+ for (int i = 0; i< c.length; i++)
+ {
+ if (c[i] == 12288) //全角空格
+ {
+ c[i] = (char) 32;
+ continue;
+ }
+
+ if (c[i]> 65280&& c[i]< 65375)
+ c[i] = (char) (c[i] - 65248);
+ }
+ return new String(c);
+ }
+
+ //繁簡轉換
+ public static String convertCC(String input, Context context)
+ {
+// ConversionType currentConversionType = ConversionType.S2TWP;
+// int convertType = SharedPreUtils.getInstance().getInt(SHARED_READ_CONVERT_TYPE, 0);
+//
+// if (input.length() == 0)
+// return "";
+//
+// switch (convertType) {
+// case 1:
+// currentConversionType = ConversionType.TW2SP;
+// break;
+// case 2:
+// currentConversionType = ConversionType.S2HK;
+// break;
+// case 3:
+// currentConversionType = ConversionType.S2T;
+// break;
+// case 4:
+// currentConversionType = ConversionType.S2TW;
+// break;
+// case 5:
+// currentConversionType = ConversionType.S2TWP;
+// break;
+// case 6:
+// currentConversionType = ConversionType.T2HK;
+// break;
+// case 7:
+// currentConversionType = ConversionType.T2S;
+// break;
+// case 8:
+// currentConversionType = ConversionType.T2TW;
+// break;
+// case 9:
+// currentConversionType = ConversionType.TW2S;
+// break;
+// case 10:
+// currentConversionType = ConversionType.HK2S;
+// break;
+// }
+
+// return (convertType != 0)?ChineseConverter.convert(input, currentConversionType, context):input;
+ return input;
+ }
}
diff --git a/app/src/main/java/com/yzx/webebook/utils/SystemBarUtils.java b/app/src/main/java/com/yzx/webebook/utils/SystemBarUtils.java
new file mode 100644
index 0000000..db1baf8
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/utils/SystemBarUtils.java
@@ -0,0 +1,162 @@
+package com.yzx.webebook.utils;
+
+import android.app.Activity;
+import android.os.Build;
+import android.view.View;
+import android.view.WindowManager;
+
+/**
+ * Created by newbiechen on 17-5-16.
+ * 基于 Android 4.4
+ *
+ * 主要参数说明:
+ *
+ * SYSTEM_UI_FLAG_FULLSCREEN : 隐藏StatusBar
+ * SYSTEM_UI_FLAG_HIDE_NAVIGATION : 隐藏NavigationBar
+ * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN: 视图扩展到StatusBar的位置,并且StatusBar不消失。
+ * 这里需要一些处理,一般是将StatusBar设置为全透明或者半透明。之后还需要使用fitSystemWindows=防止视图扩展到Status
+ * Bar上面(会在StatusBar上加一层View,该View可被移动)
+ * SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION: 视图扩展到NavigationBar的位置
+ * SYSTEM_UI_FLAG_LAYOUT_STABLE:稳定效果
+ * SYSTEM_UI_FLAG_IMMERSIVE_STICKY:保证点击任意位置不会退出
+ *
+ * 可设置特效说明:
+ * 1. 全屏特效
+ * 2. 全屏点击不退出特效
+ * 3. 注意在19 <=sdk <=21 时候,必须通过Window设置透明栏
+ */
+
+public class SystemBarUtils {
+
+ private static final int UNSTABLE_STATUS = View.SYSTEM_UI_FLAG_FULLSCREEN;
+ private static final int UNSTABLE_NAV = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
+ private static final int STABLE_STATUS = View.SYSTEM_UI_FLAG_FULLSCREEN |
+ View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
+ private static final int STABLE_NAV = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
+ private static final int EXPAND_STATUS = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
+ private static final int EXPAND_NAV = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
+
+
+ //设置隐藏StatusBar(点击任意地方会恢复)
+ public static void hideUnStableStatusBar(Activity activity){
+ //App全屏,隐藏StatusBar
+ setFlag(activity,UNSTABLE_STATUS);
+ }
+
+ public static void showUnStableStatusBar(Activity activity){
+ clearFlag(activity,UNSTABLE_STATUS);
+ }
+
+ //隐藏NavigationBar(点击任意地方会恢复)
+ public static void hideUnStableNavBar(Activity activity){
+ setFlag(activity,UNSTABLE_NAV);
+ }
+
+ public static void showUnStableNavBar(Activity activity){
+ clearFlag(activity,UNSTABLE_NAV);
+ }
+
+ public static void hideStableStatusBar(Activity activity){
+ //App全屏,隐藏StatusBar
+ setFlag(activity,STABLE_STATUS);
+ }
+
+ public static void showStableStatusBar(Activity activity){
+ clearFlag(activity,STABLE_STATUS);
+ }
+
+ public static void hideStableNavBar(Activity activity){
+ //App全屏,隐藏StatusBar
+ setFlag(activity,STABLE_NAV);
+ }
+
+ public static void showStableNavBar(Activity activity){
+ clearFlag(activity,STABLE_NAV);
+ }
+
+ /**
+ * 视图扩充到StatusBar
+ */
+ public static void expandStatusBar(Activity activity){
+ setFlag(activity, EXPAND_STATUS);
+ }
+
+ /**
+ * 视图扩充到NavBar
+ * @param activity
+ */
+ public static void expandNavBar(Activity activity){
+ setFlag(activity, EXPAND_NAV);
+ }
+
+ public static void transparentStatusBar(Activity activity){
+ if (Build.VERSION.SDK_INT >= 21){
+ expandStatusBar(activity);
+ activity.getWindow()
+ .setStatusBarColor(activity.getResources().getColor(android.R.color.transparent));
+ }
+ else if (Build.VERSION.SDK_INT >= 19){
+ WindowManager.LayoutParams attrs = activity.getWindow().getAttributes();
+ attrs.flags = (WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS | attrs.flags);
+ activity.getWindow().setAttributes(attrs);
+ }
+ }
+
+ public static void transparentNavBar(Activity activity){
+ if (Build.VERSION.SDK_INT >= 21){
+ expandNavBar(activity);
+ //下面这个方法在sdk:21以上才有
+ activity.getWindow()
+ .setNavigationBarColor(activity.getResources().getColor(android.R.color.transparent));
+ }
+ }
+
+ public static void setFlag(Activity activity, int flag){
+ if (Build.VERSION.SDK_INT >= 19){
+ View decorView = activity.getWindow().getDecorView();
+ int option = decorView.getSystemUiVisibility() | flag;
+ decorView.setSystemUiVisibility(option);
+ }
+ }
+
+ //取消flag
+ public static void clearFlag(Activity activity, int flag){
+ if (Build.VERSION.SDK_INT >= 19){
+ View decorView = activity.getWindow().getDecorView();
+ int option = decorView.getSystemUiVisibility() & (~flag);
+ decorView.setSystemUiVisibility(option);
+ }
+ }
+
+ public static void setToggleFlag(Activity activity, int option){
+ if (Build.VERSION.SDK_INT >= 19){
+ if (isFlagUsed(activity,option)){
+ clearFlag(activity,option);
+ }
+ else {
+ setFlag(activity,option);
+ }
+ }
+ }
+
+ /**
+ * @param activity
+ * @return flag是否已被使用
+ */
+ public static boolean isFlagUsed(Activity activity, int flag) {
+ int currentFlag = activity.getWindow().getDecorView().getSystemUiVisibility();
+ if((currentFlag & flag)
+ == flag) {
+ return true;
+ }else {
+ return false;
+ }
+ }
+}
diff --git a/app/src/main/java/com/yzx/webebook/utils/ToastUtils.java b/app/src/main/java/com/yzx/webebook/utils/ToastUtils.java
new file mode 100644
index 0000000..34dda93
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/utils/ToastUtils.java
@@ -0,0 +1,17 @@
+package com.yzx.webebook.utils;
+
+import android.widget.Toast;
+
+import com.yzx.webebook.App;
+
+
+/**
+ * Created by newbiechen on 17-5-11.
+ */
+
+public class ToastUtils {
+
+ public static void show(String msg){
+ Toast.makeText(App.Companion.getContext(), msg, Toast.LENGTH_SHORT).show();
+ }
+}
diff --git a/app/src/main/java/com/yzx/webebook/utils/media/LoaderCreator.java b/app/src/main/java/com/yzx/webebook/utils/media/LoaderCreator.java
new file mode 100644
index 0000000..6fd53c2
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/utils/media/LoaderCreator.java
@@ -0,0 +1,31 @@
+package com.yzx.webebook.utils.media;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import androidx.loader.content.CursorLoader;
+
+/**
+ * Created by newbiechen on 2018/1/14.
+ */
+
+public class LoaderCreator {
+ public static final int ALL_BOOK_FILE = 1;
+
+ public static CursorLoader create(Context context, int id, Bundle bundle) {
+ LocalFileLoader loader = null;
+ switch (id){
+ case ALL_BOOK_FILE:
+ loader = new LocalFileLoader(context);
+ break;
+ default:
+ loader = null;
+ break;
+ }
+ if (loader != null) {
+ return loader;
+ }
+
+ throw new IllegalArgumentException("The id of Loader is invalid!");
+ }
+}
diff --git a/app/src/main/java/com/yzx/webebook/utils/media/LocalFileLoader.java b/app/src/main/java/com/yzx/webebook/utils/media/LocalFileLoader.java
new file mode 100644
index 0000000..f7cde54
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/utils/media/LocalFileLoader.java
@@ -0,0 +1,137 @@
+package com.yzx.webebook.utils.media;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.loader.content.CursorLoader;
+
+import java.io.File;
+import java.sql.Blob;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by newbiechen on 2018/1/14.
+ */
+
+public class LocalFileLoader extends CursorLoader {
+ private static final String TAG = "LocalFileLoader";
+
+ private static final Uri FILE_URI = Uri.parse("content://media/external/file");
+ private static final String SELECTION = MediaStore.Files.FileColumns.DATA + " like ?";
+ private static final String SEARCH_TYPE = "%.txt";
+ private static final String SORT_ORDER = MediaStore.Files.FileColumns.DISPLAY_NAME + " DESC";
+ private static final String[] FILE_PROJECTION = {
+ MediaStore.Files.FileColumns.DATA,
+ MediaStore.Files.FileColumns.DISPLAY_NAME
+ };
+
+ public LocalFileLoader(Context context) {
+ super(context);
+ initLoader();
+ }
+
+ /**
+ * 为 Cursor 设置默认参数
+ */
+ private void initLoader() {
+ setUri(FILE_URI);
+ setProjection(FILE_PROJECTION);
+ setSelection(SELECTION);
+ setSelectionArgs(new String[]{SEARCH_TYPE});
+ setSortOrder(SORT_ORDER);
+ }
+
+ public void parseData(Cursor cursor, final MediaStoreHelper.MediaResultCallback resultCallback) {
+ List files = new ArrayList<>();
+ // 判断是否存在数据
+ if (cursor == null) {
+ // TODO:当媒体库没有数据的时候,需要做相应的处理
+ // 暂时直接返回空数据
+ resultCallback.onResultCallback(files);
+ return;
+ }
+ // 重复使用Loader时,需要重置cursor的position;
+ cursor.moveToPosition(-1);
+ while (cursor.moveToNext()) {
+ String path;
+
+ path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA));
+ // 路径无效
+ if (TextUtils.isEmpty(path)) {
+ continue;
+ } else {
+ File file = new File(path);
+ if (file.isDirectory() || !file.exists()){
+ continue;
+ }
+ else {
+ files.add(file);
+ }
+ }
+ }
+ if (resultCallback != null) {
+ resultCallback.onResultCallback(files);
+ }
+ }
+
+ /**
+ * 从Cursor中读取对应columnName的值
+ *
+ * @param cursor
+ * @param columnName
+ * @param defaultValue
+ * @return 当columnName无效时返回默认值;
+ */
+ protected Object getValueFromCursor(@NonNull Cursor cursor, String columnName, Object defaultValue) {
+ try {
+ int index = cursor.getColumnIndexOrThrow(columnName);
+ int type = cursor.getType(index);
+ switch (type) {
+ case Cursor.FIELD_TYPE_STRING:
+ // TO SOLVE:某些手机的数据库将数值类型存为String类型
+ String value = cursor.getString(index);
+ try {
+ if (defaultValue instanceof String) {
+ return value;
+ } else if (defaultValue instanceof Long) {
+ return Long.valueOf(value);
+ } else if (defaultValue instanceof Integer) {
+ return Integer.valueOf(value);
+ } else if (defaultValue instanceof Double) {
+ return Double.valueOf(value);
+ } else if (defaultValue instanceof Float) {
+ return Float.valueOf(value);
+ }
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ case Cursor.FIELD_TYPE_INTEGER:
+ if (defaultValue instanceof Long) {
+ return cursor.getLong(index);
+ } else if (defaultValue instanceof Integer) {
+ return cursor.getInt(index);
+ }
+ case Cursor.FIELD_TYPE_FLOAT:
+ if (defaultValue instanceof Float) {
+ return cursor.getFloat(index);
+ } else if (defaultValue instanceof Double) {
+ return cursor.getDouble(index);
+ }
+ case Cursor.FIELD_TYPE_BLOB:
+ if (defaultValue instanceof Blob) {
+ return cursor.getBlob(index);
+ }
+ case Cursor.FIELD_TYPE_NULL:
+ default:
+ return defaultValue;
+ }
+ } catch (IllegalArgumentException e) {
+ return defaultValue;
+ }
+ }
+}
diff --git a/app/src/main/java/com/yzx/webebook/utils/media/MediaStoreHelper.java b/app/src/main/java/com/yzx/webebook/utils/media/MediaStoreHelper.java
new file mode 100644
index 0000000..359b963
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/utils/media/MediaStoreHelper.java
@@ -0,0 +1,67 @@
+package com.yzx.webebook.utils.media;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.os.Bundle;
+
+import androidx.fragment.app.FragmentActivity;
+import androidx.loader.app.LoaderManager;
+import androidx.loader.content.Loader;
+
+import java.io.File;
+import java.lang.ref.WeakReference;
+import java.util.List;
+
+/**
+ * Created by newbiechen on 2018/1/14.
+ * 获取媒体库的数据。
+ */
+
+public class MediaStoreHelper {
+
+ /**
+ * 获取媒体库中所有的书籍文件
+ *
+ * 暂时只支持 TXT
+ *
+ * @param activity
+ * @param resultCallback
+ */
+ public static void getAllBookFile(FragmentActivity activity, MediaResultCallback resultCallback) {
+ // 将文件的获取处理交给 LoaderManager。
+ activity.getSupportLoaderManager()
+ .initLoader(LoaderCreator.ALL_BOOK_FILE, null, new MediaLoaderCallbacks(activity, resultCallback));
+ }
+
+ public interface MediaResultCallback {
+ void onResultCallback(List files);
+ }
+
+ /**
+ * Loader 回调处理
+ */
+ static class MediaLoaderCallbacks implements LoaderManager.LoaderCallbacks {
+ protected WeakReference mContext;
+ protected MediaResultCallback mResultCallback;
+
+ public MediaLoaderCallbacks(Context context, MediaResultCallback resultCallback) {
+ mContext = new WeakReference<>(context);
+ mResultCallback = resultCallback;
+ }
+
+ @Override
+ public Loader onCreateLoader(int id, Bundle args) {
+ return LoaderCreator.create(mContext.get(), id, args);
+ }
+
+ @Override
+ public void onLoadFinished(Loader loader, Cursor data) {
+ LocalFileLoader localFileLoader = (LocalFileLoader) loader;
+ localFileLoader.parseData(data, mResultCallback);
+ }
+
+ @Override
+ public void onLoaderReset(Loader loader) {
+ }
+ }
+}
diff --git a/app/src/main/java/com/yzx/webebook/widget/ReadSettingDialog.java b/app/src/main/java/com/yzx/webebook/widget/ReadSettingDialog.java
new file mode 100644
index 0000000..d0ea93a
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/widget/ReadSettingDialog.java
@@ -0,0 +1,333 @@
+package com.yzx.webebook.widget;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.view.Gravity;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.CheckBox;
+import android.widget.ImageView;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+
+import androidx.annotation.NonNull;
+import androidx.core.content.ContextCompat;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.yzx.webebook.R;
+import com.yzx.webebook.adapter.PageStyleAdapter;
+import com.yzx.webebook.model.local.ReadSettingManager;
+import com.yzx.webebook.utils.BrightnessUtils;
+import com.yzx.webebook.utils.ScreenUtils;
+import com.yzx.webebook.widget.page.PageLoader;
+import com.yzx.webebook.widget.page.PageMode;
+import com.yzx.webebook.widget.page.PageStyle;
+
+import java.util.Arrays;
+
+
+/**
+ * Created by newbiechen on 17-5-18.
+ */
+
+public class ReadSettingDialog extends Dialog {
+ private static final String TAG = "ReadSettingDialog";
+ private static final int DEFAULT_TEXT_SIZE = 16;
+
+ ImageView mIvBrightnessMinus;
+ SeekBar mSbBrightness;
+ ImageView mIvBrightnessPlus;
+ CheckBox mCbBrightnessAuto;
+ TextView mTvFontMinus;
+ TextView mTvFont;
+ TextView mTvFontPlus;
+ CheckBox mCbFontDefault;
+ RadioGroup mRgPageMode;
+
+ RadioButton mRbSimulation;
+ RadioButton mRbCover;
+ RadioButton mRbSlide;
+ RadioButton mRbScroll;
+ RadioButton mRbNone;
+ RecyclerView mRvBg;
+ TextView mTvMore;
+ /************************************/
+ private PageStyleAdapter mPageStyleAdapter;
+ private ReadSettingManager mSettingManager;
+ private PageLoader mPageLoader;
+ private Activity mActivity;
+
+ private PageMode mPageMode;
+ private PageStyle mPageStyle;
+
+ private int mBrightness;
+ private int mTextSize;
+
+ private boolean isBrightnessAuto;
+ private boolean isTextDefault;
+
+
+ public ReadSettingDialog(@NonNull Activity activity, PageLoader mPageLoader) {
+ super(activity, R.style.ReadSettingDialog);
+ mActivity = activity;
+ this.mPageLoader = mPageLoader;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.dialog_read_setting);
+ initView();
+ setUpWindow();
+ initData();
+ initWidget();
+ initClick();
+ }
+
+ private void initView() {
+ mIvBrightnessMinus = findViewById(R.id.read_setting_iv_brightness_minus);
+ mSbBrightness = findViewById(R.id.read_setting_sb_brightness);
+ mIvBrightnessPlus = findViewById(R.id.read_setting_iv_brightness_plus);
+ mCbBrightnessAuto = findViewById(R.id.read_setting_cb_brightness_auto);
+ mTvFontMinus = findViewById(R.id.read_setting_tv_font_minus);
+ mTvFont = findViewById(R.id.read_setting_tv_font);
+ mTvFontPlus = findViewById(R.id.read_setting_tv_font_plus);
+ mCbFontDefault = findViewById(R.id.read_setting_cb_font_default);
+ mRgPageMode = findViewById(R.id.read_setting_rg_page_mode);
+ mRbSimulation = findViewById(R.id.read_setting_rb_simulation);
+ mRbCover = findViewById(R.id.read_setting_rb_cover);
+ mRbSlide = findViewById(R.id.read_setting_rb_slide);
+ mRbScroll = findViewById(R.id.read_setting_rb_scroll);
+ mRbNone = findViewById(R.id.read_setting_rb_none);
+ mRvBg = findViewById(R.id.read_setting_rv_bg);
+ mTvMore = findViewById(R.id.read_setting_tv_more);
+ }
+
+ //设置Dialog显示的位置
+ private void setUpWindow() {
+ Window window = getWindow();
+ WindowManager.LayoutParams lp = window.getAttributes();
+ lp.width = WindowManager.LayoutParams.MATCH_PARENT;
+ lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
+ lp.gravity = Gravity.BOTTOM;
+ window.setAttributes(lp);
+ }
+
+ private void initData() {
+ mSettingManager = ReadSettingManager.getInstance();
+
+ isBrightnessAuto = mSettingManager.isBrightnessAuto();
+ mBrightness = mSettingManager.getBrightness();
+ mTextSize = mSettingManager.getTextSize();
+ isTextDefault = mSettingManager.isDefaultTextSize();
+ mPageMode = mSettingManager.getPageMode();
+ mPageStyle = mSettingManager.getPageStyle();
+ }
+
+ private void initWidget() {
+ mSbBrightness.setProgress(mBrightness);
+ mTvFont.setText(mTextSize + "");
+ mCbBrightnessAuto.setChecked(isBrightnessAuto);
+ mCbFontDefault.setChecked(isTextDefault);
+ initPageMode();
+ //RecyclerView
+ setUpAdapter();
+ }
+
+ private void setUpAdapter() {
+ Drawable[] drawables = {
+ getDrawable(R.color.nb_read_bg_1)
+ , getDrawable(R.color.nb_read_bg_2)
+ , getDrawable(R.color.nb_read_bg_3)
+ , getDrawable(R.color.nb_read_bg_4)
+ , getDrawable(R.color.nb_read_bg_5)};
+
+ mPageStyleAdapter = new PageStyleAdapter();
+ mRvBg.setLayoutManager(new GridLayoutManager(getContext(), 5));
+ mRvBg.setAdapter(mPageStyleAdapter);
+ mPageStyleAdapter.refreshItems(Arrays.asList(drawables));
+
+ mPageStyleAdapter.setPageStyleChecked(mPageStyle);
+
+ }
+
+ private void initPageMode() {
+ switch (mPageMode) {
+ case SIMULATION:
+ mRbSimulation.setChecked(true);
+ break;
+ case COVER:
+ mRbCover.setChecked(true);
+ break;
+ case SLIDE:
+ mRbSlide.setChecked(true);
+ break;
+ case NONE:
+ mRbNone.setChecked(true);
+ break;
+ case SCROLL:
+ mRbScroll.setChecked(true);
+ break;
+ }
+ }
+
+ private Drawable getDrawable(int drawRes) {
+ return ContextCompat.getDrawable(getContext(), drawRes);
+ }
+
+ private void initClick() {
+ //亮度调节
+ mIvBrightnessMinus.setOnClickListener(
+ (v) -> {
+ if (mCbBrightnessAuto.isChecked()) {
+ mCbBrightnessAuto.setChecked(false);
+ }
+ int progress = mSbBrightness.getProgress() - 1;
+ if (progress < 0) return;
+ mSbBrightness.setProgress(progress);
+ BrightnessUtils.setBrightness(mActivity, progress);
+ }
+ );
+ mIvBrightnessPlus.setOnClickListener(
+ (v) -> {
+ if (mCbBrightnessAuto.isChecked()) {
+ mCbBrightnessAuto.setChecked(false);
+ }
+ int progress = mSbBrightness.getProgress() + 1;
+ if (progress > mSbBrightness.getMax()) return;
+ mSbBrightness.setProgress(progress);
+ BrightnessUtils.setBrightness(mActivity, progress);
+ //设置进度
+ ReadSettingManager.getInstance().setBrightness(progress);
+ }
+ );
+
+ mSbBrightness.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ int progress = seekBar.getProgress();
+ if (mCbBrightnessAuto.isChecked()) {
+ mCbBrightnessAuto.setChecked(false);
+ }
+ //设置当前 Activity 的亮度
+ BrightnessUtils.setBrightness(mActivity, progress);
+ //存储亮度的进度条
+ ReadSettingManager.getInstance().setBrightness(progress);
+ }
+ });
+
+ mCbBrightnessAuto.setOnCheckedChangeListener(
+ (buttonView, isChecked) -> {
+ if (isChecked) {
+ //获取屏幕的亮度
+ BrightnessUtils.setBrightness(mActivity, BrightnessUtils.getScreenBrightness(mActivity));
+ } else {
+ //获取进度条的亮度
+ BrightnessUtils.setBrightness(mActivity, mSbBrightness.getProgress());
+ }
+ ReadSettingManager.getInstance().setAutoBrightness(isChecked);
+ }
+ );
+
+ //字体大小调节
+ mTvFontMinus.setOnClickListener(
+ (v) -> {
+ if (mCbFontDefault.isChecked()) {
+ mCbFontDefault.setChecked(false);
+ }
+ int fontSize = Integer.valueOf(mTvFont.getText().toString()) - 1;
+ if (fontSize < 0) return;
+ mTvFont.setText(fontSize + "");
+ mPageLoader.setTextSize(fontSize);
+ }
+ );
+
+ mTvFontPlus.setOnClickListener(
+ (v) -> {
+ if (mCbFontDefault.isChecked()) {
+ mCbFontDefault.setChecked(false);
+ }
+ int fontSize = Integer.valueOf(mTvFont.getText().toString()) + 1;
+ mTvFont.setText(fontSize + "");
+ mPageLoader.setTextSize(fontSize);
+ }
+ );
+
+ mCbFontDefault.setOnCheckedChangeListener(
+ (buttonView, isChecked) -> {
+ if (isChecked) {
+ int fontSize = ScreenUtils.dpToPx(DEFAULT_TEXT_SIZE);
+ mTvFont.setText(fontSize + "");
+ mPageLoader.setTextSize(fontSize);
+ }
+ }
+ );
+
+ //Page Mode 切换
+ mRgPageMode.setOnCheckedChangeListener(
+ (group, checkedId) -> {
+ PageMode pageMode;
+ switch (checkedId) {
+ case R.id.read_setting_rb_simulation:
+ pageMode = PageMode.SIMULATION;
+ break;
+ case R.id.read_setting_rb_cover:
+ pageMode = PageMode.COVER;
+ break;
+ case R.id.read_setting_rb_slide:
+ pageMode = PageMode.SLIDE;
+ break;
+ case R.id.read_setting_rb_scroll:
+ pageMode = PageMode.SCROLL;
+ break;
+ case R.id.read_setting_rb_none:
+ pageMode = PageMode.NONE;
+ break;
+ default:
+ pageMode = PageMode.SIMULATION;
+ break;
+ }
+ mPageLoader.setPageMode(pageMode);
+ }
+ );
+
+ //背景的点击事件
+ mPageStyleAdapter.setOnItemClickListener(
+ (view, pos) -> mPageLoader.setPageStyle(PageStyle.values()[pos])
+ );
+
+ //更多设置
+ mTvMore.setOnClickListener(
+ (v) -> {
+// Intent intent = new Intent(getContext(), MoreSettingActivity.class);
+// mActivity.startActivityForResult(intent, ReadActivity.REQUEST_MORE_SETTING);
+ //关闭当前设置
+ dismiss();
+ }
+ );
+ }
+
+ public boolean isBrightFollowSystem() {
+ if (mCbBrightnessAuto == null) {
+ return false;
+ }
+ return mCbBrightnessAuto.isChecked();
+ }
+}
diff --git a/app/src/main/java/com/yzx/webebook/widget/animation/AnimationProvider.java b/app/src/main/java/com/yzx/webebook/widget/animation/AnimationProvider.java
new file mode 100644
index 0000000..1e15d4a
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/widget/animation/AnimationProvider.java
@@ -0,0 +1,98 @@
+package com.yzx.webebook.widget.animation;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.PointF;
+import android.widget.Scroller;
+
+/**
+ * Created by Administrator on 2016/8/1 0001.
+ */
+public abstract class AnimationProvider {
+
+ public enum Direction {
+ NONE(true),NEXT(true), PRE(true), UP(false), DOWN(false);
+
+ public final boolean isHorizontal;
+
+ Direction(boolean isHorizontal) {
+ this.isHorizontal = isHorizontal;
+ }
+ }
+
+ protected Bitmap mCurPageBitmap,mNextPageBitmap;
+ protected float myStartX;
+ protected float myStartY;
+ protected int myEndX;
+ protected int myEndY;
+ protected Direction myDirection;
+
+ protected int mScreenWidth;
+ protected int mScreenHeight;
+
+ protected PointF mTouch = new PointF(); // 拖拽点
+ private Direction direction = Direction.NONE;
+ private boolean isCancel = false;
+
+ public AnimationProvider(int width,int height) {
+ mCurPageBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
+ mNextPageBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
+
+ this.mScreenWidth = width;
+ this.mScreenHeight = height;
+ }
+
+ //绘制滑动页面
+ public abstract void drawMove(Canvas canvas);
+
+ //绘制不滑动页面
+ public abstract void drawStatic(Canvas canvas);
+
+ //设置开始拖拽点
+ public void setStartPoint(float x,float y){
+ myStartX = x;
+ myStartY = y;
+ }
+
+ //设置拖拽点
+ public void setTouchPoint(float x,float y){
+ mTouch.x = x;
+ mTouch.y = y;
+ }
+
+ //设置方向
+ public void setDirection(Direction direction){
+ this.direction = direction;
+ }
+
+ public Direction getDirection(){
+ return direction;
+ }
+
+ public void setCancel(boolean isCancel){
+ this.isCancel = isCancel;
+ }
+
+ public abstract void startAnimation(Scroller scroller);
+
+ /**
+ * 转换页面,在显示下一章的时候,必须首先调用此方法
+ */
+ public void changePage(){
+ Bitmap bitmap = mCurPageBitmap;
+ mCurPageBitmap = mNextPageBitmap;
+ mNextPageBitmap = bitmap;
+ }
+
+ public Bitmap getNextBitmap(){
+ return mNextPageBitmap;
+ }
+
+ public Bitmap getBgBitmap(){
+ return mNextPageBitmap;
+ }
+
+ public boolean getCancel(){
+ return isCancel;
+ }
+}
diff --git a/app/src/main/java/com/yzx/webebook/widget/animation/CoverPageAnim.java b/app/src/main/java/com/yzx/webebook/widget/animation/CoverPageAnim.java
new file mode 100644
index 0000000..eb534dd
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/widget/animation/CoverPageAnim.java
@@ -0,0 +1,100 @@
+package com.yzx.webebook.widget.animation;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.GradientDrawable;
+import android.view.View;
+
+/**
+ * Created by newbiechen on 17-7-24.
+ */
+
+public class CoverPageAnim extends HorizonPageAnim {
+
+ private Rect mSrcRect, mDestRect;
+ private GradientDrawable mBackShadowDrawableLR;
+
+ public CoverPageAnim(int w, int h, View view, OnPageChangeListener listener) {
+ super(w, h, view, listener);
+ mSrcRect = new Rect(0, 0, mViewWidth, mViewHeight);
+ mDestRect = new Rect(0, 0, mViewWidth, mViewHeight);
+ int[] mBackShadowColors = new int[] { 0x66000000,0x00000000};
+ mBackShadowDrawableLR = new GradientDrawable(
+ GradientDrawable.Orientation.LEFT_RIGHT, mBackShadowColors);
+ mBackShadowDrawableLR.setGradientType(GradientDrawable.LINEAR_GRADIENT);
+ }
+
+ @Override
+ public void drawStatic(Canvas canvas) {
+ if (isCancel){
+ mNextBitmap = mCurBitmap.copy(Bitmap.Config.RGB_565, true);
+ canvas.drawBitmap(mCurBitmap, 0, 0, null);
+ }else {
+ canvas.drawBitmap(mNextBitmap, 0, 0, null);
+ }
+ }
+
+ @Override
+ public void drawMove(Canvas canvas) {
+
+ switch (mDirection){
+ case NEXT:
+ int dis = (int) (mViewWidth - mStartX + mTouchX);
+ if (dis > mViewWidth){
+ dis = mViewWidth;
+ }
+ //计算bitmap截取的区域
+ mSrcRect.left = mViewWidth - dis;
+ //计算bitmap在canvas显示的区域
+ mDestRect.right = dis;
+ canvas.drawBitmap(mNextBitmap,0,0,null);
+ canvas.drawBitmap(mCurBitmap,mSrcRect,mDestRect,null);
+ addShadow(dis,canvas);
+ break;
+ default:
+ mSrcRect.left = (int) (mViewWidth - mTouchX);
+ mDestRect.right = (int) mTouchX;
+ canvas.drawBitmap(mCurBitmap,0,0,null);
+ canvas.drawBitmap(mNextBitmap,mSrcRect,mDestRect,null);
+ addShadow((int) mTouchX,canvas);
+ break;
+ }
+ }
+
+ //添加阴影
+ public void addShadow(int left,Canvas canvas) {
+ mBackShadowDrawableLR.setBounds(left, 0, left + 30 , mScreenHeight);
+ mBackShadowDrawableLR.draw(canvas);
+ }
+
+ @Override
+ public void startAnim() {
+ super.startAnim();
+ int dx = 0;
+ switch (mDirection){
+ case NEXT:
+ if (isCancel){
+ int dis = (int) ((mViewWidth - mStartX) + mTouchX);
+ if (dis > mViewWidth){
+ dis = mViewWidth;
+ }
+ dx = mViewWidth - dis;
+ }else{
+ dx = (int) -(mTouchX + (mViewWidth - mStartX));
+ }
+ break;
+ default:
+ if (isCancel){
+ dx = (int) -mTouchX;
+ }else{
+ dx = (int) (mViewWidth - mTouchX);
+ }
+ break;
+ }
+
+ //滑动速度保持一致
+ int duration = (400 * Math.abs(dx)) / mViewWidth;
+ mScroller.startScroll((int) mTouchX, 0, dx, 0, duration);
+ }
+}
diff --git a/app/src/main/java/com/yzx/webebook/widget/animation/HorizonPageAnim.java b/app/src/main/java/com/yzx/webebook/widget/animation/HorizonPageAnim.java
new file mode 100644
index 0000000..a8c7c62
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/widget/animation/HorizonPageAnim.java
@@ -0,0 +1,230 @@
+package com.yzx.webebook.widget.animation;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+/**
+ * Created by newbiechen on 17-7-24.
+ * 横向动画的模板
+ */
+
+public abstract class HorizonPageAnim extends PageAnimation{
+ private static final String TAG = "HorizonPageAnim";
+
+ protected Bitmap mCurBitmap;
+ protected Bitmap mNextBitmap;
+ //是否取消翻页
+ protected boolean isCancel = false;
+
+ //可以使用 mLast代替
+ private int mMoveX = 0;
+ private int mMoveY = 0;
+ //是否移动了
+ private boolean isMove = false;
+ //是否翻阅下一页。true表示翻到下一页,false表示上一页。
+ private boolean isNext = false;
+
+ //是否没下一页或者上一页
+ private boolean noNext = false;
+
+ public HorizonPageAnim(int w, int h, View view, OnPageChangeListener listener) {
+ this(w, h, 0, 0, view, listener);
+ }
+
+ public HorizonPageAnim(int w, int h, int marginWidth, int marginHeight,
+ View view, OnPageChangeListener listener) {
+ super(w, h, marginWidth, marginHeight, view,listener);
+ //创建图片
+ mCurBitmap = Bitmap.createBitmap(mViewWidth, mViewHeight, Bitmap.Config.RGB_565);
+ mNextBitmap = Bitmap.createBitmap(mViewWidth, mViewHeight, Bitmap.Config.RGB_565);
+ }
+
+ /**
+ * 转换页面,在显示下一章的时候,必须首先调用此方法
+ */
+ public void changePage(){
+ Bitmap bitmap = mCurBitmap;
+ mCurBitmap = mNextBitmap;
+ mNextBitmap = bitmap;
+ }
+
+ public abstract void drawStatic(Canvas canvas);
+
+ public abstract void drawMove(Canvas canvas);
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ //获取点击位置
+ int x = (int)event.getX();
+ int y = (int)event.getY();
+ //设置触摸点
+ setTouchPoint(x,y);
+
+ switch (event.getAction()){
+ case MotionEvent.ACTION_DOWN:
+ //移动的点击位置
+ mMoveX = 0;
+ mMoveY = 0;
+ //是否移动
+ isMove = false;
+ //是否存在下一章
+ noNext = false;
+ //是下一章还是前一章
+ isNext = false;
+ //是否正在执行动画
+ isRunning = false;
+ //取消
+ isCancel = false;
+ //设置起始位置的触摸点
+ setStartPoint(x,y);
+ //如果存在动画则取消动画
+ abortAnim();
+ break;
+ case MotionEvent.ACTION_MOVE:
+ final int slop = ViewConfiguration.get(mView.getContext()).getScaledTouchSlop();
+ //判断是否移动了
+ if (!isMove) {
+ isMove = Math.abs(mStartX - x) > slop || Math.abs(mStartY - y) > slop;
+ }
+
+ if (isMove){
+ //判断是否是准备移动的状态(将要移动但是还没有移动)
+ if (mMoveX == 0 && mMoveY ==0) {
+ //判断翻得是上一页还是下一页
+ if (x - mStartX > 0){
+ //上一页的参数配置
+ isNext = false;
+ boolean hasPrev = mListener.hasPrev();
+ setDirection(Direction.PRE);
+ //如果上一页不存在
+ if (!hasPrev) {
+ noNext = true;
+ return true;
+ }
+ }else{
+ //进行下一页的配置
+ isNext = true;
+ //判断是否下一页存在
+ boolean hasNext = mListener.hasNext();
+ //如果存在设置动画方向
+ setDirection(Direction.NEXT);
+
+ //如果不存在表示没有下一页了
+ if (!hasNext) {
+ noNext = true;
+ return true;
+ }
+ }
+ }else{
+ //判断是否取消翻页
+ if (isNext){
+ if (x - mMoveX > 0){
+ isCancel = true;
+ }else {
+ isCancel = false;
+ }
+ }else{
+ if (x - mMoveX < 0){
+ isCancel = true;
+ }else {
+ isCancel = false;
+ }
+ }
+ }
+
+ mMoveX = x;
+ mMoveY = y;
+ isRunning = true;
+ mView.invalidate();
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ if (!isMove){
+ if (x < mScreenWidth / 2){
+ isNext = false;
+ }else{
+ isNext = true;
+ }
+
+ if (isNext) {
+ //判断是否下一页存在
+ boolean hasNext = mListener.hasNext();
+ //设置动画方向
+ setDirection(Direction.NEXT);
+ if (!hasNext) {
+ return true;
+ }
+ } else {
+ boolean hasPrev = mListener.hasPrev();
+ setDirection(Direction.PRE);
+ if (!hasPrev) {
+ return true;
+ }
+ }
+ }
+
+ // 是否取消翻页
+ if (isCancel){
+ mListener.pageCancel();
+ }
+
+ // 开启翻页效果
+ if (!noNext) {
+ startAnim();
+ mView.invalidate();
+ }
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ if (isRunning) {
+ drawMove(canvas);
+ } else {
+ if (isCancel){
+ mNextBitmap = mCurBitmap.copy(Bitmap.Config.RGB_565, true);
+ }
+ drawStatic(canvas);
+ }
+ }
+
+ @Override
+ public void scrollAnim() {
+ if (mScroller.computeScrollOffset()) {
+ int x = mScroller.getCurrX();
+ int y = mScroller.getCurrY();
+
+ setTouchPoint(x, y);
+
+ if (mScroller.getFinalX() == x && mScroller.getFinalY() == y){
+ isRunning = false;
+ }
+ mView.postInvalidate();
+ }
+ }
+
+ @Override
+ public void abortAnim() {
+ if (!mScroller.isFinished()){
+ mScroller.abortAnimation();
+ isRunning = false;
+ setTouchPoint(mScroller.getFinalX(),mScroller.getFinalY());
+ mView.postInvalidate();
+ }
+ }
+
+ @Override
+ public Bitmap getBgBitmap() {
+ return mNextBitmap;
+ }
+
+ @Override
+ public Bitmap getNextBitmap() {
+ return mNextBitmap;
+ }
+}
diff --git a/app/src/main/java/com/yzx/webebook/widget/animation/NonePageAnim.java b/app/src/main/java/com/yzx/webebook/widget/animation/NonePageAnim.java
new file mode 100644
index 0000000..b0fbb5f
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/widget/animation/NonePageAnim.java
@@ -0,0 +1,37 @@
+package com.yzx.webebook.widget.animation;
+
+import android.graphics.Canvas;
+import android.view.View;
+
+/**
+ * Created by newbiechen on 17-7-24.
+ */
+
+public class NonePageAnim extends HorizonPageAnim{
+
+ public NonePageAnim(int w, int h, View view, OnPageChangeListener listener) {
+ super(w, h, view, listener);
+ }
+
+ @Override
+ public void drawStatic(Canvas canvas) {
+ if (isCancel){
+ canvas.drawBitmap(mCurBitmap, 0, 0, null);
+ }else {
+ canvas.drawBitmap(mNextBitmap, 0, 0, null);
+ }
+ }
+
+ @Override
+ public void drawMove(Canvas canvas) {
+ if (isCancel){
+ canvas.drawBitmap(mCurBitmap, 0, 0, null);
+ }else {
+ canvas.drawBitmap(mNextBitmap, 0, 0, null);
+ }
+ }
+
+ @Override
+ public void startAnim() {
+ }
+}
diff --git a/app/src/main/java/com/yzx/webebook/widget/animation/PageAnimation.java b/app/src/main/java/com/yzx/webebook/widget/animation/PageAnimation.java
new file mode 100644
index 0000000..f1c7910
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/widget/animation/PageAnimation.java
@@ -0,0 +1,157 @@
+package com.yzx.webebook.widget.animation;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.animation.LinearInterpolator;
+import android.widget.Scroller;
+
+/**
+ * Created by newbiechen on 17-7-24.
+ * 翻页动画抽象类
+ */
+
+public abstract class PageAnimation {
+ //正在使用的View
+ protected View mView;
+ //滑动装置
+ protected Scroller mScroller;
+ //监听器
+ protected OnPageChangeListener mListener;
+ //移动方向
+ protected Direction mDirection = Direction.NONE;
+
+ protected boolean isRunning = false;
+
+ //屏幕的尺寸
+ protected int mScreenWidth;
+ protected int mScreenHeight;
+ //屏幕的间距
+ protected int mMarginWidth;
+ protected int mMarginHeight;
+ //视图的尺寸
+ protected int mViewWidth;
+ protected int mViewHeight;
+ //起始点
+ protected float mStartX;
+ protected float mStartY;
+ //触碰点
+ protected float mTouchX;
+ protected float mTouchY;
+ //上一个触碰点
+ protected float mLastX;
+ protected float mLastY;
+
+ public PageAnimation(int w, int h,View view,OnPageChangeListener listener){
+ this(w, h, 0, 0, view,listener);
+ }
+
+ public PageAnimation(int w, int h, int marginWidth, int marginHeight, View view,OnPageChangeListener listener){
+ mScreenWidth = w;
+ mScreenHeight = h;
+
+ mMarginWidth = marginWidth;
+ mMarginHeight = marginHeight;
+
+ mViewWidth = mScreenWidth - mMarginWidth * 2;
+ mViewHeight = mScreenHeight - mMarginHeight * 2;
+
+ mView = view;
+ mListener = listener;
+
+ mScroller = new Scroller(mView.getContext(), new LinearInterpolator());
+ }
+
+ public void setStartPoint(float x,float y){
+ mStartX = x;
+ mStartY = y;
+
+ mLastX = mStartX;
+ mLastY = mStartY;
+ }
+
+ public void setTouchPoint(float x,float y){
+ mLastX = mTouchX;
+ mLastY = mTouchY;
+
+ mTouchX = x;
+ mTouchY = y;
+ }
+
+ public boolean isRunning(){
+ return isRunning;
+ }
+
+ /**
+ * 开启翻页动画
+ */
+ public void startAnim(){
+ if (isRunning){
+ return;
+ }
+ isRunning = true;
+ }
+
+ public void setDirection(Direction direction){
+ mDirection = direction;
+ }
+
+ public Direction getDirection(){
+ return mDirection;
+ }
+
+ public void clear(){
+ mView = null;
+ }
+ /**
+ * 点击事件的处理
+ * @param event
+ */
+ public abstract boolean onTouchEvent(MotionEvent event);
+
+ /**
+ * 绘制图形
+ * @param canvas
+ */
+ public abstract void draw(Canvas canvas);
+
+ /**
+ * 滚动动画
+ * 必须放在computeScroll()方法中执行
+ */
+ public abstract void scrollAnim();
+
+ /**
+ * 取消动画
+ */
+ public abstract void abortAnim();
+
+ /**
+ * 获取背景板
+ * @return
+ */
+ public abstract Bitmap getBgBitmap();
+
+ /**
+ * 获取内容显示版面
+ */
+ public abstract Bitmap getNextBitmap();
+
+ public enum Direction {
+ NONE(true),NEXT(true), PRE(true), UP(false), DOWN(false);
+
+ public final boolean isHorizontal;
+
+ Direction(boolean isHorizontal) {
+ this.isHorizontal = isHorizontal;
+ }
+ }
+
+ public interface OnPageChangeListener {
+ boolean hasPrev();
+ boolean hasNext();
+ void pageCancel();
+ }
+
+}
diff --git a/app/src/main/java/com/yzx/webebook/widget/animation/ScrollPageAnim.java b/app/src/main/java/com/yzx/webebook/widget/animation/ScrollPageAnim.java
new file mode 100644
index 0000000..d441091
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/widget/animation/ScrollPageAnim.java
@@ -0,0 +1,408 @@
+package com.yzx.webebook.widget.animation;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Iterator;
+
+/**
+ * Created by newbiechen on 17-7-23.
+ * 原理:仿照ListView源码实现的上下滑动效果
+ * Alter by: zeroAngus
+ *
+ * 问题:
+ * 1. 向上翻页,重复的问题 (完成)
+ * 2. 滑动卡顿的问题。原因:由于绘制的数据过多造成的卡顿问题。 (主要是文字绘制需要的时长比较多) 解决办法:做文字缓冲
+ * 3. 弱网环境下,显示的问题
+ */
+public class ScrollPageAnim extends PageAnimation {
+ private static final String TAG = "ScrollAnimation";
+ // 滑动追踪的时间
+ private static final int VELOCITY_DURATION = 1000;
+ private VelocityTracker mVelocity;
+
+ // 整个Bitmap的背景显示
+ private Bitmap mBgBitmap;
+
+ // 下一个展示的图片
+ private Bitmap mNextBitmap;
+
+ // 被废弃的图片列表
+ private ArrayDeque mScrapViews;
+ // 正在被利用的图片列表
+ private ArrayList mActiveViews = new ArrayList<>(2);
+
+ // 是否处于刷新阶段
+ private boolean isRefresh = true;
+
+ public ScrollPageAnim(int w, int h, int marginWidth, int marginHeight,
+ View view, OnPageChangeListener listener) {
+ super(w, h, marginWidth, marginHeight, view, listener);
+ // 创建两个BitmapView
+ initWidget();
+ }
+
+ private void initWidget() {
+ mBgBitmap = Bitmap.createBitmap(mScreenWidth, mScreenHeight, Bitmap.Config.RGB_565);
+
+ mScrapViews = new ArrayDeque<>(2);
+ for (int i = 0; i < 2; ++i) {
+ BitmapView view = new BitmapView();
+ view.bitmap = Bitmap.createBitmap(mViewWidth, mViewHeight, Bitmap.Config.RGB_565);
+ view.srcRect = new Rect(0, 0, mViewWidth, mViewHeight);
+ view.destRect = new Rect(0, 0, mViewWidth, mViewHeight);
+ view.top = 0;
+ view.bottom = view.bitmap.getHeight();
+
+ mScrapViews.push(view);
+ }
+ onLayout();
+ isRefresh = false;
+ }
+
+ // 修改布局,填充内容
+ private void onLayout() {
+ // 如果还没有开始加载,则从上到下进行绘制
+ if (mActiveViews.size() == 0) {
+ fillDown(0, 0);
+ mDirection = Direction.NONE;
+ } else {
+ int offset = (int) (mTouchY - mLastY);
+ // 判断是下滑还是上拉 (下滑)
+ if (offset > 0) {
+ int topEdge = mActiveViews.get(0).top;
+ fillUp(topEdge, offset);
+ }
+ // 上拉
+ else {
+ // 底部的距离 = 当前底部的距离 + 滑动的距离 (因为上滑,得到的值肯定是负的)
+ int bottomEdge = mActiveViews.get(mActiveViews.size() - 1).bottom;
+ fillDown(bottomEdge, offset);
+ }
+ }
+ }
+
+ // 底部填充
+ private Iterator downIt;
+
+ /**
+ * 创建View填充底部空白部分
+ *
+ * @param bottomEdge :当前最后一个View的底部,在整个屏幕上的位置,即相对于屏幕顶部的距离
+ * @param offset :滑动的偏移量
+ */
+ private void fillDown(int bottomEdge, int offset) {
+
+ downIt = mActiveViews.iterator();
+ BitmapView view;
+
+ // 进行删除
+ while (downIt.hasNext()) {
+ view = downIt.next();
+ view.top = view.top + offset;
+ view.bottom = view.bottom + offset;
+ // 设置允许显示的范围
+ view.destRect.top = view.top;
+ view.destRect.bottom = view.bottom;
+
+ // 判断是否越界了
+ if (view.bottom <= 0) {
+ // 添加到废弃的View中
+ mScrapViews.add(view);
+ // 从Active中移除
+ downIt.remove();
+ // 如果原先是从上加载,现在变成从下加载,则表示取消
+ if (mDirection == Direction.UP) {
+ mListener.pageCancel();
+ mDirection = Direction.NONE;
+ }
+ }
+ }
+
+ // 滑动之后的最后一个 View 的距离屏幕顶部上的实际位置
+ int realEdge = bottomEdge + offset;
+
+ // 进行填充
+ while (realEdge < mViewHeight && mActiveViews.size() < 2) {
+ // 从废弃的Views中获取一个
+ view = mScrapViews.getFirst();
+/* //擦除其Bitmap(重新创建会不会更好一点)
+ eraseBitmap(view.bitmap,view.bitmap.getWidth(),view.bitmap.getHeight(),0,0);*/
+ if (view == null) return;
+
+ Bitmap cancelBitmap = mNextBitmap;
+ mNextBitmap = view.bitmap;
+
+ if (!isRefresh) {
+ boolean hasNext = mListener.hasNext(); //如果不成功则无法滑动
+
+ // 如果不存在next,则进行还原
+ if (!hasNext) {
+ mNextBitmap = cancelBitmap;
+ for (BitmapView activeView : mActiveViews) {
+ activeView.top = 0;
+ activeView.bottom = mViewHeight;
+ // 设置允许显示的范围
+ activeView.destRect.top = activeView.top;
+ activeView.destRect.bottom = activeView.bottom;
+ }
+ abortAnim();
+ return;
+ }
+ }
+
+ // 如果加载成功,那么就将View从ScrapViews中移除
+ mScrapViews.removeFirst();
+ // 添加到存活的Bitmap中
+ mActiveViews.add(view);
+ mDirection = Direction.DOWN;
+
+ // 设置Bitmap的范围
+ view.top = realEdge;
+ view.bottom = realEdge + view.bitmap.getHeight();
+ // 设置允许显示的范围
+ view.destRect.top = view.top;
+ view.destRect.bottom = view.bottom;
+
+ realEdge += view.bitmap.getHeight();
+ }
+ }
+
+ private Iterator upIt;
+
+ /**
+ * 创建View填充顶部空白部分
+ *
+ * @param topEdge : 当前第一个View的顶部,到屏幕顶部的距离
+ * @param offset : 滑动的偏移量
+ */
+ private void fillUp(int topEdge, int offset) {
+ // 首先进行布局的调整
+ upIt = mActiveViews.iterator();
+ BitmapView view;
+ while (upIt.hasNext()) {
+ view = upIt.next();
+ view.top = view.top + offset;
+ view.bottom = view.bottom + offset;
+ //设置允许显示的范围
+ view.destRect.top = view.top;
+ view.destRect.bottom = view.bottom;
+
+ // 判断是否越界了
+ if (view.top >= mViewHeight) {
+ // 添加到废弃的View中
+ mScrapViews.add(view);
+ // 从Active中移除
+ upIt.remove();
+
+ // 如果原先是下,现在变成从上加载了,则表示取消加载
+
+ if (mDirection == Direction.DOWN) {
+ mListener.pageCancel();
+ mDirection = Direction.NONE;
+ }
+ }
+ }
+
+ // 滑动之后,第一个 View 的顶部距离屏幕顶部的实际位置。
+ int realEdge = topEdge + offset;
+
+ // 对布局进行View填充
+ while (realEdge > 0 && mActiveViews.size() < 2) {
+ // 从废弃的Views中获取一个
+ view = mScrapViews.getFirst();
+ if (view == null) return;
+
+ // 判断是否存在上一章节
+ Bitmap cancelBitmap = mNextBitmap;
+ mNextBitmap = view.bitmap;
+ if (!isRefresh) {
+ boolean hasPrev = mListener.hasPrev(); // 如果不成功则无法滑动
+ // 如果不存在next,则进行还原
+ if (!hasPrev) {
+ mNextBitmap = cancelBitmap;
+ for (BitmapView activeView : mActiveViews) {
+ activeView.top = 0;
+ activeView.bottom = mViewHeight;
+ // 设置允许显示的范围
+ activeView.destRect.top = activeView.top;
+ activeView.destRect.bottom = activeView.bottom;
+ }
+ abortAnim();
+ return;
+ }
+ }
+ // 如果加载成功,那么就将View从ScrapViews中移除
+ mScrapViews.removeFirst();
+ // 加入到存活的对象中
+ mActiveViews.add(0, view);
+ mDirection = Direction.UP;
+ // 设置Bitmap的范围
+ view.top = realEdge - view.bitmap.getHeight();
+ view.bottom = realEdge;
+
+ // 设置允许显示的范围
+ view.destRect.top = view.top;
+ view.destRect.bottom = view.bottom;
+ realEdge -= view.bitmap.getHeight();
+ }
+ }
+
+ /**
+ * 对Bitmap进行擦除
+ *
+ * @param b
+ * @param width
+ * @param height
+ * @param paddingLeft
+ * @param paddingTop
+ */
+ private void eraseBitmap(Bitmap b, int width, int height,
+ int paddingLeft, int paddingTop) {
+ /* if (mInitBitmapPix == null) return;
+ b.setPixels(mInitBitmapPix, 0, width, paddingLeft, paddingTop, width, height);*/
+ }
+
+ /**
+ * 重置位移
+ */
+ public void resetBitmap() {
+ isRefresh = true;
+ // 将所有的Active加入到Scrap中
+ for (BitmapView view : mActiveViews) {
+ mScrapViews.add(view);
+ }
+ // 清除所有的Active
+ mActiveViews.clear();
+ // 重新进行布局
+ onLayout();
+ isRefresh = false;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+
+ // 初始化速度追踪器
+ if (mVelocity == null) {
+ mVelocity = VelocityTracker.obtain();
+ }
+
+ mVelocity.addMovement(event);
+ // 设置触碰点
+ setTouchPoint(x, y);
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ isRunning = false;
+ // 设置起始点
+ setStartPoint(x, y);
+ // 停止动画
+ abortAnim();
+ break;
+ case MotionEvent.ACTION_MOVE:
+ mVelocity.computeCurrentVelocity(VELOCITY_DURATION);
+ isRunning = true;
+ // 进行刷新
+ mView.postInvalidate();
+ break;
+ case MotionEvent.ACTION_UP:
+ isRunning = false;
+ // 开启动画
+ startAnim();
+ // 删除检测器
+ mVelocity.recycle();
+ mVelocity = null;
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ try {
+ mVelocity.recycle(); // if velocityTracker won't be used should be recycled
+ mVelocity = null;
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ break;
+ }
+ return true;
+ }
+
+
+ BitmapView tmpView;
+
+ @Override
+ public void draw(Canvas canvas) {
+ //进行布局
+ onLayout();
+
+ //绘制背景
+ canvas.drawBitmap(mBgBitmap, 0, 0, null);
+ //绘制内容
+ canvas.save();
+ //移动位置
+ canvas.translate(0, mMarginHeight);
+ //裁剪显示区域
+ canvas.clipRect(0, 0, mViewWidth, mViewHeight);
+/* //设置背景透明
+ canvas.drawColor(0x40);*/
+ //绘制Bitmap
+ for (int i = 0; i < mActiveViews.size(); ++i) {
+ tmpView = mActiveViews.get(i);
+ canvas.drawBitmap(tmpView.bitmap, tmpView.srcRect, tmpView.destRect, null);
+ }
+ canvas.restore();
+ }
+
+ @Override
+ public synchronized void startAnim() {
+ isRunning = true;
+ mScroller.fling(0, (int) mTouchY, 0, (int) mVelocity.getYVelocity()
+ , 0, 0, Integer.MAX_VALUE * -1, Integer.MAX_VALUE);
+ }
+
+ @Override
+ public void scrollAnim() {
+ if (mScroller.computeScrollOffset()) {
+ int x = mScroller.getCurrX();
+ int y = mScroller.getCurrY();
+ setTouchPoint(x, y);
+ if (mScroller.getFinalX() == x && mScroller.getFinalY() == y) {
+ isRunning = false;
+ }
+ mView.postInvalidate();
+ }
+ }
+
+ @Override
+ public void abortAnim() {
+ if (!mScroller.isFinished()) {
+ mScroller.abortAnimation();
+ isRunning = false;
+ }
+ }
+
+ @Override
+ public Bitmap getBgBitmap() {
+ return mBgBitmap;
+ }
+
+ @Override
+ public Bitmap getNextBitmap() {
+ return mNextBitmap;
+ }
+
+ private static class BitmapView {
+ Bitmap bitmap;
+ Rect srcRect;
+ Rect destRect;
+ int top;
+ int bottom;
+ }
+}
diff --git a/app/src/main/java/com/yzx/webebook/widget/animation/SimulationPageAnim.java b/app/src/main/java/com/yzx/webebook/widget/animation/SimulationPageAnim.java
new file mode 100644
index 0000000..b6f929c
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/widget/animation/SimulationPageAnim.java
@@ -0,0 +1,668 @@
+package com.yzx.webebook.widget.animation;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PointF;
+import android.graphics.Region;
+import android.graphics.drawable.GradientDrawable;
+import android.os.Build;
+import android.view.View;
+
+/**
+ * Created by newbiechen on 17-7-24.
+ */
+
+public class SimulationPageAnim extends HorizonPageAnim {
+ private static final String TAG = "SimulationPageAnim";
+
+ private int mCornerX = 1; // 拖拽点对应的页脚
+ private int mCornerY = 1;
+ private Path mPath0;
+ private Path mPath1;
+
+ private PointF mBezierStart1 = new PointF(); // 贝塞尔曲线起始点
+ private PointF mBezierControl1 = new PointF(); // 贝塞尔曲线控制点
+ private PointF mBeziervertex1 = new PointF(); // 贝塞尔曲线顶点
+ private PointF mBezierEnd1 = new PointF(); // 贝塞尔曲线结束点
+
+ private PointF mBezierStart2 = new PointF(); // 另一条贝塞尔曲线
+ private PointF mBezierControl2 = new PointF();
+ private PointF mBeziervertex2 = new PointF();
+ private PointF mBezierEnd2 = new PointF();
+
+ private float mMiddleX;
+ private float mMiddleY;
+ private float mDegrees;
+ private float mTouchToCornerDis;
+ private ColorMatrixColorFilter mColorMatrixFilter;
+ private Matrix mMatrix;
+ private float[] mMatrixArray = {0, 0, 0, 0, 0, 0, 0, 0, 1.0f};
+
+ private boolean mIsRTandLB; // 是否属于右上左下
+ private float mMaxLength;
+ private GradientDrawable mBackShadowDrawableLR; // 有阴影的GradientDrawable
+ private GradientDrawable mBackShadowDrawableRL;
+ private GradientDrawable mFolderShadowDrawableLR;
+ private GradientDrawable mFolderShadowDrawableRL;
+
+ private GradientDrawable mFrontShadowDrawableHBT;
+ private GradientDrawable mFrontShadowDrawableHTB;
+ private GradientDrawable mFrontShadowDrawableVLR;
+ private GradientDrawable mFrontShadowDrawableVRL;
+
+ private Paint mPaint;
+
+ // 适配 android 高版本无法使用 XOR 的问题
+ private Path mXORPath;
+
+ public SimulationPageAnim(int w, int h, View view, OnPageChangeListener listener) {
+ super(w, h, view, listener);
+ mPath0 = new Path();
+ mPath1 = new Path();
+ mXORPath = new Path();
+ mMaxLength = (float) Math.hypot(mScreenWidth, mScreenHeight);
+ mPaint = new Paint();
+
+ mPaint.setStyle(Paint.Style.FILL);
+
+ createDrawable();
+
+ ColorMatrix cm = new ColorMatrix();//设置颜色数组
+ float array[] = {1, 0, 0, 0, 0,
+ 0, 1, 0, 0, 0,
+ 0, 0, 1, 0, 0,
+ 0, 0, 0, 1, 0};
+ cm.set(array);
+ mColorMatrixFilter = new ColorMatrixColorFilter(cm);
+ mMatrix = new Matrix();
+
+ mTouchX = 0.01f; // 不让x,y为0,否则在点计算时会有问题
+ mTouchY = 0.01f;
+ }
+
+ @Override
+ public void drawMove(Canvas canvas) {
+ switch (mDirection) {
+ case NEXT:
+ calcPoints();
+ drawCurrentPageArea(canvas, mCurBitmap, mPath0);
+ drawNextPageAreaAndShadow(canvas, mNextBitmap);
+ drawCurrentPageShadow(canvas);
+ drawCurrentBackArea(canvas, mCurBitmap);
+ break;
+ default:
+ calcPoints();
+ drawCurrentPageArea(canvas, mNextBitmap, mPath0);
+ drawNextPageAreaAndShadow(canvas, mCurBitmap);
+ drawCurrentPageShadow(canvas);
+ drawCurrentBackArea(canvas, mNextBitmap);
+ break;
+ }
+ }
+
+ @Override
+ public void drawStatic(Canvas canvas) {
+ if (isCancel) {
+ mNextBitmap = mCurBitmap.copy(Bitmap.Config.RGB_565, true);
+ canvas.drawBitmap(mCurBitmap, 0, 0, null);
+ } else {
+ canvas.drawBitmap(mNextBitmap, 0, 0, null);
+ }
+ }
+
+ @Override
+ public void startAnim() {
+ super.startAnim();
+ int dx, dy;
+ // dx 水平方向滑动的距离,负值会使滚动向左滚动
+ // dy 垂直方向滑动的距离,负值会使滚动向上滚动
+ if (isCancel) {
+
+ if (mCornerX > 0 && mDirection.equals(Direction.NEXT)) {
+ dx = (int) (mScreenWidth - mTouchX);
+ } else {
+ dx = -(int) mTouchX;
+ }
+
+ if (!mDirection.equals(Direction.NEXT)) {
+ dx = (int) -(mScreenWidth + mTouchX);
+ }
+
+ if (mCornerY > 0) {
+ dy = (int) (mScreenHeight - mTouchY);
+ } else {
+ dy = -(int) mTouchY; // 防止mTouchY最终变为0
+ }
+ } else {
+ if (mCornerX > 0 && mDirection.equals(Direction.NEXT)) {
+ dx = -(int) (mScreenWidth + mTouchX);
+ } else {
+ dx = (int) (mScreenWidth - mTouchX + mScreenWidth);
+ }
+ if (mCornerY > 0) {
+ dy = (int) (mScreenHeight - mTouchY);
+ } else {
+ dy = (int) (1 - mTouchY); // 防止mTouchY最终变为0
+ }
+ }
+ mScroller.startScroll((int) mTouchX, (int) mTouchY, dx, dy, 400);
+ }
+
+ @Override
+ public void setDirection(Direction direction) {
+ super.setDirection(direction);
+
+ switch (direction) {
+ case PRE:
+ //上一页滑动不出现对角
+ if (mStartX > mScreenWidth / 2) {
+ calcCornerXY(mStartX, mScreenHeight);
+ } else {
+ calcCornerXY(mScreenWidth - mStartX, mScreenHeight);
+ }
+ break;
+ case NEXT:
+ if (mScreenWidth / 2 > mStartX) {
+ calcCornerXY(mScreenWidth - mStartX, mStartY);
+ }
+ break;
+ }
+ }
+
+ @Override
+ public void setStartPoint(float x, float y) {
+ super.setStartPoint(x, y);
+ calcCornerXY(x, y);
+ }
+
+ @Override
+ public void setTouchPoint(float x, float y) {
+ super.setTouchPoint(x, y);
+ //触摸y中间位置吧y变成屏幕高度
+ if ((mStartY > mScreenHeight / 3 && mStartY < mScreenHeight * 2 / 3) || mDirection.equals(Direction.PRE)) {
+ mTouchY = mScreenHeight;
+ }
+
+ if (mStartY > mScreenHeight / 3 && mStartY < mScreenHeight / 2 && mDirection.equals(Direction.NEXT)) {
+ mTouchY = 1;
+ }
+ }
+
+ /**
+ * 创建阴影的GradientDrawable
+ */
+ private void createDrawable() {
+ int[] color = {0x333333, 0xb0333333};
+ mFolderShadowDrawableRL = new GradientDrawable(
+ GradientDrawable.Orientation.RIGHT_LEFT, color);
+ mFolderShadowDrawableRL
+ .setGradientType(GradientDrawable.LINEAR_GRADIENT);
+
+ mFolderShadowDrawableLR = new GradientDrawable(
+ GradientDrawable.Orientation.LEFT_RIGHT, color);
+ mFolderShadowDrawableLR
+ .setGradientType(GradientDrawable.LINEAR_GRADIENT);
+
+ // 背面颜色组
+ int[] mBackShadowColors = new int[]{0xff111111, 0x111111};
+ mBackShadowDrawableRL = new GradientDrawable(
+ GradientDrawable.Orientation.RIGHT_LEFT, mBackShadowColors);
+ mBackShadowDrawableRL.setGradientType(GradientDrawable.LINEAR_GRADIENT);
+
+ mBackShadowDrawableLR = new GradientDrawable(
+ GradientDrawable.Orientation.LEFT_RIGHT, mBackShadowColors);
+ mBackShadowDrawableLR.setGradientType(GradientDrawable.LINEAR_GRADIENT);
+
+ // 前面颜色组
+ int[] mFrontShadowColors = new int[]{0x80111111, 0x111111};
+ mFrontShadowDrawableVLR = new GradientDrawable(
+ GradientDrawable.Orientation.LEFT_RIGHT, mFrontShadowColors);
+ mFrontShadowDrawableVLR
+ .setGradientType(GradientDrawable.LINEAR_GRADIENT);
+ mFrontShadowDrawableVRL = new GradientDrawable(
+ GradientDrawable.Orientation.RIGHT_LEFT, mFrontShadowColors);
+ mFrontShadowDrawableVRL
+ .setGradientType(GradientDrawable.LINEAR_GRADIENT);
+
+ mFrontShadowDrawableHTB = new GradientDrawable(
+ GradientDrawable.Orientation.TOP_BOTTOM, mFrontShadowColors);
+ mFrontShadowDrawableHTB
+ .setGradientType(GradientDrawable.LINEAR_GRADIENT);
+
+ mFrontShadowDrawableHBT = new GradientDrawable(
+ GradientDrawable.Orientation.BOTTOM_TOP, mFrontShadowColors);
+ mFrontShadowDrawableHBT
+ .setGradientType(GradientDrawable.LINEAR_GRADIENT);
+ }
+
+ /**
+ * 是否能够拖动过去
+ *
+ * @return
+ */
+ public boolean canDragOver() {
+ return mTouchToCornerDis > mScreenWidth / 10;
+ }
+
+ public boolean right() {
+ return mCornerX <= -4;
+ }
+
+ /**
+ * 绘制翻起页背面
+ *
+ * @param canvas
+ * @param bitmap
+ */
+ private void drawCurrentBackArea(Canvas canvas, Bitmap bitmap) {
+ int i = (int) (mBezierStart1.x + mBezierControl1.x) / 2;
+ float f1 = Math.abs(i - mBezierControl1.x);
+ int i1 = (int) (mBezierStart2.y + mBezierControl2.y) / 2;
+ float f2 = Math.abs(i1 - mBezierControl2.y);
+ float f3 = Math.min(f1, f2);
+ mPath1.reset();
+ mPath1.moveTo(mBeziervertex2.x, mBeziervertex2.y);
+ mPath1.lineTo(mBeziervertex1.x, mBeziervertex1.y);
+ mPath1.lineTo(mBezierEnd1.x, mBezierEnd1.y);
+ mPath1.lineTo(mTouchX, mTouchY);
+ mPath1.lineTo(mBezierEnd2.x, mBezierEnd2.y);
+ mPath1.close();
+ GradientDrawable mFolderShadowDrawable;
+ int left;
+ int right;
+ if (mIsRTandLB) {
+ left = (int) (mBezierStart1.x - 1);
+ right = (int) (mBezierStart1.x + f3 + 1);
+ mFolderShadowDrawable = mFolderShadowDrawableLR;
+ } else {
+ left = (int) (mBezierStart1.x - f3 - 1);
+ right = (int) (mBezierStart1.x + 1);
+ mFolderShadowDrawable = mFolderShadowDrawableRL;
+ }
+ canvas.save();
+ try {
+ canvas.clipPath(mPath0);
+ canvas.clipPath(mPath1, Region.Op.INTERSECT);
+ } catch (Exception e) {
+ }
+
+ mPaint.setColorFilter(mColorMatrixFilter);
+ //对Bitmap进行取色
+ int color = bitmap.getPixel(1, 1);
+ //获取对应的三色
+ int red = (color & 0xff0000) >> 16;
+ int green = (color & 0x00ff00) >> 8;
+ int blue = (color & 0x0000ff);
+ //转换成含有透明度的颜色
+ int tempColor = Color.argb(200, red, green, blue);
+
+
+ float dis = (float) Math.hypot(mCornerX - mBezierControl1.x,
+ mBezierControl2.y - mCornerY);
+ float f8 = (mCornerX - mBezierControl1.x) / dis;
+ float f9 = (mBezierControl2.y - mCornerY) / dis;
+ mMatrixArray[0] = 1 - 2 * f9 * f9;
+ mMatrixArray[1] = 2 * f8 * f9;
+ mMatrixArray[3] = mMatrixArray[1];
+ mMatrixArray[4] = 1 - 2 * f8 * f8;
+ mMatrix.reset();
+ mMatrix.setValues(mMatrixArray);
+ mMatrix.preTranslate(-mBezierControl1.x, -mBezierControl1.y);
+ mMatrix.postTranslate(mBezierControl1.x, mBezierControl1.y);
+ canvas.drawBitmap(bitmap, mMatrix, mPaint);
+ //背景叠加
+ canvas.drawColor(tempColor);
+
+ mPaint.setColorFilter(null);
+
+ canvas.rotate(mDegrees, mBezierStart1.x, mBezierStart1.y);
+ mFolderShadowDrawable.setBounds(left, (int) mBezierStart1.y, right,
+ (int) (mBezierStart1.y + mMaxLength));
+ mFolderShadowDrawable.draw(canvas);
+ canvas.restore();
+ }
+
+ /**
+ * 绘制翻起页的阴影
+ *
+ * @param canvas
+ */
+ private void drawCurrentPageShadow(Canvas canvas) {
+ double degree;
+ if (mIsRTandLB) {
+ degree = Math.PI
+ / 4
+ - Math.atan2(mBezierControl1.y - mTouchY, mTouchX
+ - mBezierControl1.x);
+ } else {
+ degree = Math.PI
+ / 4
+ - Math.atan2(mTouchY - mBezierControl1.y, mTouchX
+ - mBezierControl1.x);
+ }
+ // 翻起页阴影顶点与touch点的距离
+ double d1 = (float) 25 * 1.414 * Math.cos(degree);
+ double d2 = (float) 25 * 1.414 * Math.sin(degree);
+ float x = (float) (mTouchX + d1);
+ float y;
+ if (mIsRTandLB) {
+ y = (float) (mTouchY + d2);
+ } else {
+ y = (float) (mTouchY - d2);
+ }
+ mPath1.reset();
+ mPath1.moveTo(x, y);
+ mPath1.lineTo(mTouchX, mTouchY);
+ mPath1.lineTo(mBezierControl1.x, mBezierControl1.y);
+ mPath1.lineTo(mBezierStart1.x, mBezierStart1.y);
+ mPath1.close();
+ float rotateDegrees;
+ canvas.save();
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ mXORPath.reset();
+ mXORPath.moveTo(0f, 0f);
+ mXORPath.lineTo(canvas.getWidth(), 0f);
+ mXORPath.lineTo(canvas.getWidth(), canvas.getHeight());
+ mXORPath.lineTo(0f, canvas.getHeight());
+ mXORPath.close();
+
+ // 取 path 的补集,作为 canvas 的交集
+ mXORPath.op(mPath0, Path.Op.XOR);
+ canvas.clipPath(mXORPath);
+ } else {
+ canvas.clipPath(mPath0, Region.Op.XOR);
+ }
+ canvas.clipPath(mPath1, Region.Op.INTERSECT);
+ } catch (Exception e) {
+ // TODO: handle exception
+ }
+
+ int leftx;
+ int rightx;
+ GradientDrawable mCurrentPageShadow;
+ if (mIsRTandLB) {
+ leftx = (int) (mBezierControl1.x);
+ rightx = (int) mBezierControl1.x + 25;
+ mCurrentPageShadow = mFrontShadowDrawableVLR;
+ } else {
+ leftx = (int) (mBezierControl1.x - 25);
+ rightx = (int) mBezierControl1.x + 1;
+ mCurrentPageShadow = mFrontShadowDrawableVRL;
+ }
+
+ rotateDegrees = (float) Math.toDegrees(Math.atan2(mTouchX
+ - mBezierControl1.x, mBezierControl1.y - mTouchY));
+ canvas.rotate(rotateDegrees, mBezierControl1.x, mBezierControl1.y);
+ mCurrentPageShadow.setBounds(leftx,
+ (int) (mBezierControl1.y - mMaxLength), rightx,
+ (int) (mBezierControl1.y));
+ mCurrentPageShadow.draw(canvas);
+ canvas.restore();
+
+ mPath1.reset();
+ mPath1.moveTo(x, y);
+ mPath1.lineTo(mTouchX, mTouchY);
+ mPath1.lineTo(mBezierControl2.x, mBezierControl2.y);
+ mPath1.lineTo(mBezierStart2.x, mBezierStart2.y);
+ mPath1.close();
+ canvas.save();
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ mXORPath.reset();
+ mXORPath.moveTo(0f, 0f);
+ mXORPath.lineTo(canvas.getWidth(), 0f);
+ mXORPath.lineTo(canvas.getWidth(), canvas.getHeight());
+ mXORPath.lineTo(0f, canvas.getHeight());
+ mXORPath.close();
+
+ // 取 path 的补给,作为 canvas 的交集
+ mXORPath.op(mPath0, Path.Op.XOR);
+ canvas.clipPath(mXORPath);
+ } else {
+ canvas.clipPath(mPath0, Region.Op.XOR);
+ }
+ canvas.clipPath(mPath1, Region.Op.INTERSECT);
+ } catch (Exception e) {
+ }
+
+ if (mIsRTandLB) {
+ leftx = (int) (mBezierControl2.y);
+ rightx = (int) (mBezierControl2.y + 25);
+ mCurrentPageShadow = mFrontShadowDrawableHTB;
+ } else {
+ leftx = (int) (mBezierControl2.y - 25);
+ rightx = (int) (mBezierControl2.y + 1);
+ mCurrentPageShadow = mFrontShadowDrawableHBT;
+ }
+ rotateDegrees = (float) Math.toDegrees(Math.atan2(mBezierControl2.y
+ - mTouchY, mBezierControl2.x - mTouchX));
+ canvas.rotate(rotateDegrees, mBezierControl2.x, mBezierControl2.y);
+ float temp;
+ if (mBezierControl2.y < 0)
+ temp = mBezierControl2.y - mScreenHeight;
+ else
+ temp = mBezierControl2.y;
+
+ int hmg = (int) Math.hypot(mBezierControl2.x, temp);
+ if (hmg > mMaxLength)
+ mCurrentPageShadow
+ .setBounds((int) (mBezierControl2.x - 25) - hmg, leftx,
+ (int) (mBezierControl2.x + mMaxLength) - hmg,
+ rightx);
+ else
+ mCurrentPageShadow.setBounds(
+ (int) (mBezierControl2.x - mMaxLength), leftx,
+ (int) (mBezierControl2.x), rightx);
+
+ mCurrentPageShadow.draw(canvas);
+ canvas.restore();
+ }
+
+ private void drawNextPageAreaAndShadow(Canvas canvas, Bitmap bitmap) {
+ mPath1.reset();
+ mPath1.moveTo(mBezierStart1.x, mBezierStart1.y);
+ mPath1.lineTo(mBeziervertex1.x, mBeziervertex1.y);
+ mPath1.lineTo(mBeziervertex2.x, mBeziervertex2.y);
+ mPath1.lineTo(mBezierStart2.x, mBezierStart2.y);
+ mPath1.lineTo(mCornerX, mCornerY);
+ mPath1.close();
+
+ mDegrees = (float) Math.toDegrees(Math.atan2(mBezierControl1.x
+ - mCornerX, mBezierControl2.y - mCornerY));
+ int leftx;
+ int rightx;
+ GradientDrawable mBackShadowDrawable;
+ if (mIsRTandLB) { //左下及右上
+ leftx = (int) (mBezierStart1.x);
+ rightx = (int) (mBezierStart1.x + mTouchToCornerDis / 4);
+ mBackShadowDrawable = mBackShadowDrawableLR;
+ } else {
+ leftx = (int) (mBezierStart1.x - mTouchToCornerDis / 4);
+ rightx = (int) mBezierStart1.x;
+ mBackShadowDrawable = mBackShadowDrawableRL;
+ }
+ canvas.save();
+ try {
+ canvas.clipPath(mPath0);
+ canvas.clipPath(mPath1, Region.Op.INTERSECT);
+ } catch (Exception e) {
+ }
+
+
+ canvas.drawBitmap(bitmap, 0, 0, null);
+ canvas.rotate(mDegrees, mBezierStart1.x, mBezierStart1.y);
+ mBackShadowDrawable.setBounds(leftx, (int) mBezierStart1.y, rightx,
+ (int) (mMaxLength + mBezierStart1.y));//左上及右下角的xy坐标值,构成一个矩形
+ mBackShadowDrawable.draw(canvas);
+ canvas.restore();
+ }
+
+ private void drawCurrentPageArea(Canvas canvas, Bitmap bitmap, Path path) {
+ mPath0.reset();
+ mPath0.moveTo(mBezierStart1.x, mBezierStart1.y);
+ mPath0.quadTo(mBezierControl1.x, mBezierControl1.y, mBezierEnd1.x,
+ mBezierEnd1.y);
+ mPath0.lineTo(mTouchX, mTouchY);
+ mPath0.lineTo(mBezierEnd2.x, mBezierEnd2.y);
+ mPath0.quadTo(mBezierControl2.x, mBezierControl2.y, mBezierStart2.x,
+ mBezierStart2.y);
+ mPath0.lineTo(mCornerX, mCornerY);
+ mPath0.close();
+
+ canvas.save();
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ mXORPath.reset();
+ mXORPath.moveTo(0f, 0f);
+ mXORPath.lineTo(canvas.getWidth(), 0f);
+ mXORPath.lineTo(canvas.getWidth(), canvas.getHeight());
+ mXORPath.lineTo(0f, canvas.getHeight());
+ mXORPath.close();
+
+ // 取 path 的补给,作为 canvas 的交集
+ mXORPath.op(path, Path.Op.XOR);
+ canvas.clipPath(mXORPath);
+ } else {
+ canvas.clipPath(path, Region.Op.XOR);
+ }
+ canvas.drawBitmap(bitmap, 0, 0, null);
+ try {
+ canvas.restore();
+ } catch (Exception e) {
+
+ }
+
+ }
+
+ /**
+ * 计算拖拽点对应的拖拽脚
+ *
+ * @param x
+ * @param y
+ */
+ public void calcCornerXY(float x, float y) {
+ if (x <= mScreenWidth / 2) {
+ mCornerX = 0;
+ } else {
+ mCornerX = mScreenWidth;
+ }
+ if (y <= mScreenHeight / 2) {
+ mCornerY = 0;
+ } else {
+ mCornerY = mScreenHeight;
+ }
+
+ if ((mCornerX == 0 && mCornerY == mScreenHeight)
+ || (mCornerX == mScreenWidth && mCornerY == 0)) {
+ mIsRTandLB = true;
+ } else {
+ mIsRTandLB = false;
+ }
+
+ }
+
+ private void calcPoints() {
+ mMiddleX = (mTouchX + mCornerX) / 2;
+ mMiddleY = (mTouchY + mCornerY) / 2;
+ mBezierControl1.x = mMiddleX - (mCornerY - mMiddleY)
+ * (mCornerY - mMiddleY) / (mCornerX - mMiddleX);
+ mBezierControl1.y = mCornerY;
+ mBezierControl2.x = mCornerX;
+
+ float f4 = mCornerY - mMiddleY;
+ if (f4 == 0) {
+ mBezierControl2.y = mMiddleY - (mCornerX - mMiddleX)
+ * (mCornerX - mMiddleX) / 0.1f;
+
+ } else {
+ mBezierControl2.y = mMiddleY - (mCornerX - mMiddleX)
+ * (mCornerX - mMiddleX) / (mCornerY - mMiddleY);
+ }
+ mBezierStart1.x = mBezierControl1.x - (mCornerX - mBezierControl1.x)
+ / 2;
+ mBezierStart1.y = mCornerY;
+
+ // 当mBezierStart1.x < 0或者mBezierStart1.x > 480时
+ // 如果继续翻页,会出现BUG故在此限制
+ if (mTouchX > 0 && mTouchX < mScreenWidth) {
+ if (mBezierStart1.x < 0 || mBezierStart1.x > mScreenWidth) {
+ if (mBezierStart1.x < 0)
+ mBezierStart1.x = mScreenWidth - mBezierStart1.x;
+
+ float f1 = Math.abs(mCornerX - mTouchX);
+ float f2 = mScreenWidth * f1 / mBezierStart1.x;
+ mTouchX = Math.abs(mCornerX - f2);
+
+ float f3 = Math.abs(mCornerX - mTouchX)
+ * Math.abs(mCornerY - mTouchY) / f1;
+ mTouchY = Math.abs(mCornerY - f3);
+
+ mMiddleX = (mTouchX + mCornerX) / 2;
+ mMiddleY = (mTouchY + mCornerY) / 2;
+
+ mBezierControl1.x = mMiddleX - (mCornerY - mMiddleY)
+ * (mCornerY - mMiddleY) / (mCornerX - mMiddleX);
+ mBezierControl1.y = mCornerY;
+
+ mBezierControl2.x = mCornerX;
+
+ float f5 = mCornerY - mMiddleY;
+ if (f5 == 0) {
+ mBezierControl2.y = mMiddleY - (mCornerX - mMiddleX)
+ * (mCornerX - mMiddleX) / 0.1f;
+ } else {
+ mBezierControl2.y = mMiddleY - (mCornerX - mMiddleX)
+ * (mCornerX - mMiddleX) / (mCornerY - mMiddleY);
+ }
+
+ mBezierStart1.x = mBezierControl1.x
+ - (mCornerX - mBezierControl1.x) / 2;
+ }
+ }
+ mBezierStart2.x = mCornerX;
+ mBezierStart2.y = mBezierControl2.y - (mCornerY - mBezierControl2.y)
+ / 2;
+
+ mTouchToCornerDis = (float) Math.hypot((mTouchX - mCornerX),
+ (mTouchY - mCornerY));
+
+ mBezierEnd1 = getCross(new PointF(mTouchX, mTouchY), mBezierControl1, mBezierStart1,
+ mBezierStart2);
+ mBezierEnd2 = getCross(new PointF(mTouchX, mTouchY), mBezierControl2, mBezierStart1,
+ mBezierStart2);
+
+ mBeziervertex1.x = (mBezierStart1.x + 2 * mBezierControl1.x + mBezierEnd1.x) / 4;
+ mBeziervertex1.y = (2 * mBezierControl1.y + mBezierStart1.y + mBezierEnd1.y) / 4;
+ mBeziervertex2.x = (mBezierStart2.x + 2 * mBezierControl2.x + mBezierEnd2.x) / 4;
+ mBeziervertex2.y = (2 * mBezierControl2.y + mBezierStart2.y + mBezierEnd2.y) / 4;
+ }
+
+ /**
+ * 求解直线P1P2和直线P3P4的交点坐标
+ *
+ * @param P1
+ * @param P2
+ * @param P3
+ * @param P4
+ * @return
+ */
+ private PointF getCross(PointF P1, PointF P2, PointF P3, PointF P4) {
+ PointF CrossP = new PointF();
+ // 二元函数通式: y=ax+b
+ float a1 = (P2.y - P1.y) / (P2.x - P1.x);
+ float b1 = ((P1.x * P2.y) - (P2.x * P1.y)) / (P1.x - P2.x);
+
+ float a2 = (P4.y - P3.y) / (P4.x - P3.x);
+ float b2 = ((P3.x * P4.y) - (P4.x * P3.y)) / (P3.x - P4.x);
+ CrossP.x = (b2 - b1) / (a1 - a2);
+ CrossP.y = a1 * CrossP.x + b1;
+ return CrossP;
+ }
+}
diff --git a/app/src/main/java/com/yzx/webebook/widget/animation/SlidePageAnim.java b/app/src/main/java/com/yzx/webebook/widget/animation/SlidePageAnim.java
new file mode 100644
index 0000000..c52cdb6
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/widget/animation/SlidePageAnim.java
@@ -0,0 +1,101 @@
+package com.yzx.webebook.widget.animation;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.view.View;
+
+/**
+ * Created by newbiechen on 17-7-24.
+ */
+
+public class SlidePageAnim extends HorizonPageAnim {
+ private Rect mSrcRect, mDestRect,mNextSrcRect,mNextDestRect;
+
+ public SlidePageAnim(int w, int h, View view, OnPageChangeListener listener) {
+ super(w, h, view, listener);
+ mSrcRect = new Rect(0, 0, mViewWidth, mViewHeight);
+ mDestRect = new Rect(0, 0, mViewWidth, mViewHeight);
+ mNextSrcRect = new Rect(0, 0, mViewWidth, mViewHeight);
+ mNextDestRect = new Rect(0, 0, mViewWidth, mViewHeight);
+ }
+
+ @Override
+ public void drawStatic(Canvas canvas) {
+ if (isCancel){
+ canvas.drawBitmap(mCurBitmap, 0, 0, null);
+ }else {
+ canvas.drawBitmap(mNextBitmap, 0, 0, null);
+ }
+ }
+
+ @Override
+ public void drawMove(Canvas canvas) {
+ int dis = 0;
+ switch (mDirection){
+ case NEXT:
+ //左半边的剩余区域
+ dis = (int) (mScreenWidth - mStartX + mTouchX);
+ if (dis > mScreenWidth){
+ dis = mScreenWidth;
+ }
+ //计算bitmap截取的区域
+ mSrcRect.left = mScreenWidth - dis;
+ //计算bitmap在canvas显示的区域
+ mDestRect.right = dis;
+ //计算下一页截取的区域
+ mNextSrcRect.right = mScreenWidth - dis;
+ //计算下一页在canvas显示的区域
+ mNextDestRect.left = dis;
+
+ canvas.drawBitmap(mNextBitmap,mNextSrcRect,mNextDestRect,null);
+ canvas.drawBitmap(mCurBitmap,mSrcRect,mDestRect,null);
+ break;
+ default:
+ dis = (int) (mTouchX - mStartX);
+ if (dis < 0){
+ dis = 0;
+ mStartX = mTouchX;
+ }
+ mSrcRect.left = mScreenWidth - dis;
+ mDestRect.right = dis;
+
+ //计算下一页截取的区域
+ mNextSrcRect.right = mScreenWidth - dis;
+ //计算下一页在canvas显示的区域
+ mNextDestRect.left = dis;
+
+ canvas.drawBitmap(mCurBitmap,mNextSrcRect,mNextDestRect,null);
+ canvas.drawBitmap(mNextBitmap,mSrcRect,mDestRect,null);
+ break;
+ }
+ }
+
+ @Override
+ public void startAnim() {
+ super.startAnim();
+ int dx = 0;
+ switch (mDirection){
+ case NEXT:
+ if (isCancel){
+ int dis = (int)((mScreenWidth - mStartX) + mTouchX);
+ if (dis > mScreenWidth){
+ dis = mScreenWidth;
+ }
+ dx = mScreenWidth - dis;
+ }else{
+ dx = (int) -(mTouchX + (mScreenWidth - mStartX));
+ }
+ break;
+ default:
+ if (isCancel){
+ dx = (int)-Math.abs(mTouchX - mStartX);
+ }else{
+ dx = (int) (mScreenWidth - (mTouchX - mStartX));
+ }
+ break;
+ }
+ //滑动速度保持一致
+ int duration = (400 * Math.abs(dx)) / mScreenWidth;
+ mScroller.startScroll((int) mTouchX, 0, dx, 0, duration);
+ }
+}
diff --git a/app/src/main/java/com/yzx/webebook/widget/page/LocalPageLoader.java b/app/src/main/java/com/yzx/webebook/widget/page/LocalPageLoader.java
new file mode 100644
index 0000000..80e84c7
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/widget/page/LocalPageLoader.java
@@ -0,0 +1,440 @@
+package com.yzx.webebook.widget.page;
+
+
+import com.yzx.webebook.model.bean.BookChapterBean;
+import com.yzx.webebook.model.bean.CollBookBean;
+import com.yzx.webebook.model.local.BookRepository;
+import com.yzx.webebook.model.local.Void;
+import com.yzx.webebook.utils.Charset;
+import com.yzx.webebook.utils.Constant;
+import com.yzx.webebook.utils.FileUtils;
+import com.yzx.webebook.utils.IOUtils;
+import com.yzx.webebook.utils.LogUtils;
+import com.yzx.webebook.utils.MD5Utils;
+import com.yzx.webebook.utils.RxUtils;
+import com.yzx.webebook.utils.StringUtils;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.RandomAccessFile;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import io.reactivex.Single;
+import io.reactivex.SingleEmitter;
+import io.reactivex.SingleObserver;
+import io.reactivex.SingleOnSubscribe;
+import io.reactivex.disposables.Disposable;
+
+/**
+ * Created by newbiechen on 17-7-1.
+ * 问题:
+ * 1. 异常处理没有做好
+ */
+
+public class LocalPageLoader extends PageLoader {
+ private static final String TAG = "LocalPageLoader";
+ //默认从文件中获取数据的长度
+ private final static int BUFFER_SIZE = 512 * 1024;
+ //没有标题的时候,每个章节的最大长度
+ private final static int MAX_LENGTH_WITH_NO_CHAPTER = 10 * 1024;
+
+ // "序(章)|前言"
+ private final static Pattern mPreChapterPattern = Pattern.compile("^(\\s{0,10})((\u5e8f[\u7ae0\u8a00]?)|(\u524d\u8a00)|(\u6954\u5b50))(\\s{0,10})$", Pattern.MULTILINE);
+
+ //正则表达式章节匹配模式
+ // "(第)([0-9零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10})([章节回集卷])(.*)"
+ private static final String[] CHAPTER_PATTERNS = new String[]{"^(.{0,8})(\u7b2c)([0-9\u96f6\u4e00\u4e8c\u4e24\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u5341\u767e\u5343\u4e07\u58f9\u8d30\u53c1\u8086\u4f0d\u9646\u67d2\u634c\u7396\u62fe\u4f70\u4edf]{1,10})([\u7ae0\u8282\u56de\u96c6\u5377])(.{0,30})$",
+ "^(\\s{0,4})([\\(\u3010\u300a]?(\u5377)?)([0-9\u96f6\u4e00\u4e8c\u4e24\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u5341\u767e\u5343\u4e07\u58f9\u8d30\u53c1\u8086\u4f0d\u9646\u67d2\u634c\u7396\u62fe\u4f70\u4edf]{1,10})([\\.:\uff1a\u0020\f\t])(.{0,30})$",
+ "^(\\s{0,4})([\\(\uff08\u3010\u300a])(.{0,30})([\\)\uff09\u3011\u300b])(\\s{0,2})$",
+ "^(\\s{0,4})(\u6b63\u6587)(.{0,20})$",
+ "^(.{0,4})(Chapter|chapter)(\\s{0,4})([0-9]{1,4})(.{0,30})$"};
+
+ //章节解析模式
+ private Pattern mChapterPattern = null;
+ //获取书本的文件
+ private File mBookFile;
+ //编码类型
+ private Charset mCharset;
+
+ private Disposable mChapterDisp = null;
+
+ public LocalPageLoader(PageView pageView, CollBookBean collBook) {
+ super(pageView, collBook);
+ mStatus = STATUS_PARING;
+ }
+
+ private List convertTxtChapter(List bookChapters) {
+ List txtChapters = new ArrayList<>(bookChapters.size());
+ for (BookChapterBean bean : bookChapters) {
+ TxtChapter chapter = new TxtChapter();
+ chapter.title = bean.getTitle();
+ chapter.start = bean.getStart();
+ chapter.end = bean.getEnd();
+ txtChapters.add(chapter);
+ }
+ return txtChapters;
+ }
+
+ /**
+ * 未完成的部分:
+ * 1. 序章的添加
+ * 2. 章节存在的书本的虚拟分章效果
+ *
+ * @throws IOException
+ */
+ private void loadChapters() throws IOException {
+ List chapters = new ArrayList<>();
+ //获取文件流
+ RandomAccessFile bookStream = new RandomAccessFile(mBookFile, "r");
+ //寻找匹配文章标题的正则表达式,判断是否存在章节名
+ boolean hasChapter = checkChapterType(bookStream);
+ //加载章节
+ byte[] buffer = new byte[BUFFER_SIZE];
+ //获取到的块起始点,在文件中的位置
+ long curOffset = 0;
+ //block的个数
+ int blockPos = 0;
+ //读取的长度
+ int length;
+
+ //获取文件中的数据到buffer,直到没有数据为止
+ while ((length = bookStream.read(buffer, 0, buffer.length)) > 0) {
+ ++blockPos;
+ //如果存在Chapter
+ if (hasChapter) {
+ //将数据转换成String
+ String blockContent = new String(buffer, 0, length, mCharset.getName());
+ //当前Block下使过的String的指针
+ int seekPos = 0;
+ //进行正则匹配
+ Matcher matcher = mChapterPattern.matcher(blockContent);
+ //如果存在相应章节
+ while (matcher.find()) {
+ //获取匹配到的字符在字符串中的起始位置
+ int chapterStart = matcher.start();
+
+ //如果 seekPos == 0 && nextChapterPos != 0 表示当前block处前面有一段内容
+ //第一种情况一定是序章 第二种情况可能是上一个章节的内容
+ if (seekPos == 0 && chapterStart != 0) {
+ //获取当前章节的内容
+ String chapterContent = blockContent.substring(seekPos, chapterStart);
+ //设置指针偏移
+ seekPos += chapterContent.length();
+
+ //如果当前对整个文件的偏移位置为0的话,那么就是序章
+ if (curOffset == 0) {
+ //创建序章
+ TxtChapter preChapter = new TxtChapter();
+ preChapter.title = "序章";
+ preChapter.start = 0;
+ preChapter.end = chapterContent.getBytes(mCharset.getName()).length; //获取String的byte值,作为最终值
+
+ //如果序章大小大于30才添加进去
+ if (preChapter.end - preChapter.start > 30) {
+ chapters.add(preChapter);
+ }
+
+ //创建当前章节
+ TxtChapter curChapter = new TxtChapter();
+ curChapter.title = matcher.group();
+ curChapter.start = preChapter.end;
+ chapters.add(curChapter);
+ }
+ //否则就block分割之后,上一个章节的剩余内容
+ else {
+ //获取上一章节
+ TxtChapter lastChapter = chapters.get(chapters.size() - 1);
+ //将当前段落添加上一章去
+ lastChapter.end += chapterContent.getBytes(mCharset.getName()).length;
+
+ //如果章节内容太小,则移除
+ if (lastChapter.end - lastChapter.start < 30) {
+ chapters.remove(lastChapter);
+ }
+
+ //创建当前章节
+ TxtChapter curChapter = new TxtChapter();
+ curChapter.title = matcher.group();
+ curChapter.start = lastChapter.end;
+ chapters.add(curChapter);
+ }
+ } else {
+ //是否存在章节
+ if (chapters.size() != 0) {
+ //获取章节内容
+ String chapterContent = blockContent.substring(seekPos, matcher.start());
+ seekPos += chapterContent.length();
+
+ //获取上一章节
+ TxtChapter lastChapter = chapters.get(chapters.size() - 1);
+ lastChapter.end = lastChapter.start + chapterContent.getBytes(mCharset.getName()).length;
+
+ //如果章节内容太小,则移除
+ if (lastChapter.end - lastChapter.start < 30) {
+ chapters.remove(lastChapter);
+ }
+
+ //创建当前章节
+ TxtChapter curChapter = new TxtChapter();
+ curChapter.title = matcher.group();
+ curChapter.start = lastChapter.end;
+ chapters.add(curChapter);
+ }
+ //如果章节不存在则创建章节
+ else {
+ TxtChapter curChapter = new TxtChapter();
+ curChapter.title = matcher.group();
+ curChapter.start = 0;
+ chapters.add(curChapter);
+ }
+ }
+ }
+ }
+ //进行本地虚拟分章
+ else {
+ //章节在buffer的偏移量
+ int chapterOffset = 0;
+ //当前剩余可分配的长度
+ int strLength = length;
+ //分章的位置
+ int chapterPos = 0;
+
+ while (strLength > 0) {
+ ++chapterPos;
+ //是否长度超过一章
+ if (strLength > MAX_LENGTH_WITH_NO_CHAPTER) {
+ //在buffer中一章的终止点
+ int end = length;
+ //寻找换行符作为终止点
+ for (int i = chapterOffset + MAX_LENGTH_WITH_NO_CHAPTER; i < length; ++i) {
+ if (buffer[i] == Charset.BLANK) {
+ end = i;
+ break;
+ }
+ }
+ TxtChapter chapter = new TxtChapter();
+ chapter.title = "第" + blockPos + "章" + "(" + chapterPos + ")";
+ chapter.start = curOffset + chapterOffset + 1;
+ chapter.end = curOffset + end;
+ chapters.add(chapter);
+ //减去已经被分配的长度
+ strLength = strLength - (end - chapterOffset);
+ //设置偏移的位置
+ chapterOffset = end;
+ } else {
+ TxtChapter chapter = new TxtChapter();
+ chapter.title = "第" + blockPos + "章" + "(" + chapterPos + ")";
+ chapter.start = curOffset + chapterOffset + 1;
+ chapter.end = curOffset + length;
+ chapters.add(chapter);
+ strLength = 0;
+ }
+ }
+ }
+
+ //block的偏移点
+ curOffset += length;
+
+ if (hasChapter) {
+ //设置上一章的结尾
+ TxtChapter lastChapter = chapters.get(chapters.size() - 1);
+ lastChapter.end = curOffset;
+ }
+
+ //当添加的block太多的时候,执行GC
+ if (blockPos % 15 == 0) {
+ System.gc();
+ System.runFinalization();
+ }
+ }
+
+ mChapterList = chapters;
+ IOUtils.close(bookStream);
+
+ System.gc();
+ System.runFinalization();
+ }
+
+ /**
+ * 从文件中提取一章的内容
+ *
+ * @param chapter
+ * @return
+ */
+ private byte[] getChapterContent(TxtChapter chapter) {
+ RandomAccessFile bookStream = null;
+ try {
+ bookStream = new RandomAccessFile(mBookFile, "r");
+ bookStream.seek(chapter.start);
+ int extent = (int) (chapter.end - chapter.start);
+ byte[] content = new byte[extent];
+ bookStream.read(content, 0, extent);
+ return content;
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ IOUtils.close(bookStream);
+ }
+
+ return new byte[0];
+ }
+
+ /**
+ * 1. 检查文件中是否存在章节名
+ * 2. 判断文件中使用的章节名类型的正则表达式
+ *
+ * @return 是否存在章节名
+ */
+ private boolean checkChapterType(RandomAccessFile bookStream) throws IOException {
+ //首先获取128k的数据
+ byte[] buffer = new byte[BUFFER_SIZE / 4];
+ int length = bookStream.read(buffer, 0, buffer.length);
+ //进行章节匹配
+ for (String str : CHAPTER_PATTERNS) {
+ Pattern pattern = Pattern.compile(str, Pattern.MULTILINE);
+ Matcher matcher = pattern.matcher(new String(buffer, 0, length, mCharset.getName()));
+ //如果匹配存在,那么就表示当前章节使用这种匹配方式
+ if (matcher.find()) {
+ mChapterPattern = pattern;
+ //重置指针位置
+ bookStream.seek(0);
+ return true;
+ }
+ }
+
+ //重置指针位置
+ bookStream.seek(0);
+ return false;
+ }
+
+ @Override
+ public void saveRecord() {
+ super.saveRecord();
+ //修改当前COllBook记录
+ if (mCollBook != null && isChapterListPrepare) {
+ //表示当前CollBook已经阅读
+ mCollBook.setIsUpdate(false);
+ mCollBook.setLastChapter(mChapterList.get(mCurChapterPos).getTitle());
+ mCollBook.setLastRead(StringUtils.
+ dateConvert(System.currentTimeMillis(), Constant.FORMAT_BOOK_DATE));
+ //直接更新
+ BookRepository.getInstance()
+ .saveCollBook(mCollBook);
+ }
+ }
+
+ @Override
+ public void closeBook() {
+ super.closeBook();
+ if (mChapterDisp != null) {
+ mChapterDisp.dispose();
+ mChapterDisp = null;
+ }
+ }
+
+ @Override
+ public void refreshChapterList() {
+ // 对于文件是否存在,或者为空的判断,不作处理。 ==> 在文件打开前处理过了。
+ mBookFile = new File(mCollBook.getCover());
+ //获取文件编码
+ mCharset = FileUtils.getCharset(mBookFile.getAbsolutePath());
+
+ String lastModified = StringUtils.dateConvert(mBookFile.lastModified(), Constant.FORMAT_BOOK_DATE);
+
+ // 判断文件是否已经加载过,并具有缓存
+ if (!mCollBook.isUpdate() && mCollBook.getUpdated() != null
+ && mCollBook.getUpdated().equals(lastModified)
+ && mCollBook.getBookChapters() != null) {
+
+ mChapterList = convertTxtChapter(mCollBook.getBookChapters());
+ isChapterListPrepare = true;
+
+ //提示目录加载完成
+ if (mPageChangeListener != null) {
+ mPageChangeListener.onCategoryFinish(mChapterList);
+ }
+
+ // 加载并显示当前章节
+ openChapter();
+
+ return;
+ }
+
+ // 通过RxJava异步处理分章事件
+ Single.create(new SingleOnSubscribe() {
+ @Override
+ public void subscribe(SingleEmitter e) throws Exception {
+ loadChapters();
+ e.onSuccess(new Void());
+ }
+ }).compose(RxUtils::toSimpleSingle)
+ .subscribe(new SingleObserver() {
+ @Override
+ public void onSubscribe(Disposable d) {
+ mChapterDisp = d;
+ }
+
+ @Override
+ public void onSuccess(Void value) {
+ mChapterDisp = null;
+ isChapterListPrepare = true;
+
+ // 提示目录加载完成
+ if (mPageChangeListener != null) {
+ mPageChangeListener.onCategoryFinish(mChapterList);
+ }
+
+ // 存储章节到数据库
+ List bookChapterBeanList = new ArrayList<>();
+ for (int i = 0; i < mChapterList.size(); ++i) {
+ TxtChapter chapter = mChapterList.get(i);
+ BookChapterBean bean = new BookChapterBean();
+ bean.setId(MD5Utils.strToMd5By16(mBookFile.getAbsolutePath()
+ + File.separator + chapter.title)); // 将路径+i 作为唯一值
+ bean.setTitle(chapter.getTitle());
+ bean.setStart(chapter.getStart());
+ bean.setUnreadble(false);
+ bean.setEnd(chapter.getEnd());
+ bookChapterBeanList.add(bean);
+ }
+ mCollBook.setBookChapters(bookChapterBeanList);
+ mCollBook.setUpdated(lastModified);
+
+ BookRepository.getInstance().saveBookChaptersWithAsync(bookChapterBeanList);
+ BookRepository.getInstance().saveCollBook(mCollBook);
+
+ // 加载并显示当前章节
+ openChapter();
+ }
+
+ @Override
+ public void onError(Throwable e) {
+ chapterError();
+ LogUtils.d(TAG, "file load error:" + e.toString());
+ }
+ });
+ }
+
+ @Override
+ protected BufferedReader getChapterReader(TxtChapter chapter) throws Exception {
+ //从文件中获取数据
+ byte[] content = getChapterContent(chapter);
+ ByteArrayInputStream bais = new ByteArrayInputStream(content);
+ BufferedReader br = new BufferedReader(new InputStreamReader(bais, mCharset.getName()));
+ return br;
+ }
+
+ @Override
+ protected boolean hasChapterData(TxtChapter chapter) {
+ return true;
+ }
+}
diff --git a/app/src/main/java/com/yzx/webebook/widget/page/NetPageLoader.java b/app/src/main/java/com/yzx/webebook/widget/page/NetPageLoader.java
new file mode 100644
index 0000000..8f4652b
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/widget/page/NetPageLoader.java
@@ -0,0 +1,226 @@
+package com.yzx.webebook.widget.page;
+
+
+
+
+import com.yzx.webebook.model.bean.BookChapterBean;
+import com.yzx.webebook.model.bean.CollBookBean;
+import com.yzx.webebook.model.local.BookRepository;
+import com.yzx.webebook.utils.BookManager;
+import com.yzx.webebook.utils.Constant;
+import com.yzx.webebook.utils.FileUtils;
+import com.yzx.webebook.utils.StringUtils;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by newbiechen on 17-5-29.
+ * 网络页面加载器
+ */
+
+public class NetPageLoader extends PageLoader {
+ private static final String TAG = "PageFactory";
+
+ public NetPageLoader(PageView pageView, CollBookBean collBook) {
+ super(pageView, collBook);
+ }
+
+ private List convertTxtChapter(List bookChapters) {
+ List txtChapters = new ArrayList<>(bookChapters.size());
+ for (BookChapterBean bean : bookChapters) {
+ TxtChapter chapter = new TxtChapter();
+ chapter.bookId = bean.getBookId();
+ chapter.title = bean.getTitle();
+ chapter.link = bean.getLink();
+ txtChapters.add(chapter);
+ }
+ return txtChapters;
+ }
+
+ @Override
+ public void refreshChapterList() {
+ if (mCollBook.getBookChapters() == null) return;
+
+ // 将 BookChapter 转换成当前可用的 Chapter
+ mChapterList = convertTxtChapter(mCollBook.getBookChapters());
+ isChapterListPrepare = true;
+
+ // 目录加载完成,执行回调操作。
+ if (mPageChangeListener != null) {
+ mPageChangeListener.onCategoryFinish(mChapterList);
+ }
+
+ // 如果章节未打开
+ if (!isChapterOpen()) {
+ // 打开章节
+ openChapter();
+ }
+ }
+
+ @Override
+ protected BufferedReader getChapterReader(TxtChapter chapter) throws Exception {
+ File file = new File(Constant.BOOK_CACHE_PATH + mCollBook.get_id()
+ + File.separator + chapter.title + FileUtils.SUFFIX_NB);
+ if (!file.exists()) return null;
+
+ Reader reader = new FileReader(file);
+ BufferedReader br = new BufferedReader(reader);
+ return br;
+ }
+
+ @Override
+ protected boolean hasChapterData(TxtChapter chapter) {
+ return BookManager.isChapterCached(mCollBook.get_id(), chapter.title);
+ }
+
+ // 装载上一章节的内容
+ @Override
+ boolean parsePrevChapter() {
+ boolean isRight = super.parsePrevChapter();
+
+ if (mStatus == STATUS_FINISH) {
+ loadPrevChapter();
+ } else if (mStatus == STATUS_LOADING) {
+ loadCurrentChapter();
+ }
+ return isRight;
+ }
+
+ // 装载当前章内容。
+ @Override
+ boolean parseCurChapter() {
+ boolean isRight = super.parseCurChapter();
+
+ if (mStatus == STATUS_LOADING) {
+ loadCurrentChapter();
+ }
+ return isRight;
+ }
+
+ // 装载下一章节的内容
+ @Override
+ boolean parseNextChapter() {
+ boolean isRight = super.parseNextChapter();
+
+ if (mStatus == STATUS_FINISH) {
+ loadNextChapter();
+ } else if (mStatus == STATUS_LOADING) {
+ loadCurrentChapter();
+ }
+
+ return isRight;
+ }
+
+ /**
+ * 加载当前页的前面两个章节
+ */
+ private void loadPrevChapter() {
+ if (mPageChangeListener != null) {
+ int end = mCurChapterPos;
+ int begin = end - 2;
+ if (begin < 0) {
+ begin = 0;
+ }
+
+ requestChapters(begin, end);
+ }
+ }
+
+ /**
+ * 加载前一页,当前页,后一页。
+ */
+ private void loadCurrentChapter() {
+ if (mPageChangeListener != null) {
+ int begin = mCurChapterPos;
+ int end = mCurChapterPos;
+
+ // 是否当前不是最后一章
+ if (end < mChapterList.size()) {
+ end = end + 1;
+ if (end >= mChapterList.size()) {
+ end = mChapterList.size() - 1;
+ }
+ }
+
+ // 如果当前不是第一章
+ if (begin != 0) {
+ begin = begin - 1;
+ if (begin < 0) {
+ begin = 0;
+ }
+ }
+
+ requestChapters(begin, end);
+ }
+ }
+
+ /**
+ * 加载当前页的后两个章节
+ */
+ private void loadNextChapter() {
+ if (mPageChangeListener != null) {
+
+ // 提示加载后两章
+ int begin = mCurChapterPos + 1;
+ int end = begin + 1;
+
+ // 判断是否大于最后一章
+ if (begin >= mChapterList.size()) {
+ // 如果下一章超出目录了,就没有必要加载了
+ return;
+ }
+
+ if (end > mChapterList.size()) {
+ end = mChapterList.size() - 1;
+ }
+
+ requestChapters(begin, end);
+ }
+ }
+
+ private void requestChapters(int start, int end) {
+ // 检验输入值
+ if (start < 0) {
+ start = 0;
+ }
+
+ if (end >= mChapterList.size()) {
+ end = mChapterList.size() - 1;
+ }
+
+
+ List chapters = new ArrayList<>();
+
+ // 过滤,哪些数据已经加载了
+ for (int i = start; i <= end; ++i) {
+ TxtChapter txtChapter = mChapterList.get(i);
+ if (!hasChapterData(txtChapter)) {
+ chapters.add(txtChapter);
+ }
+ }
+
+ if (!chapters.isEmpty()) {
+ mPageChangeListener.requestChapters(chapters);
+ }
+ }
+
+ @Override
+ public void saveRecord() {
+ super.saveRecord();
+ if (mCollBook != null && isChapterListPrepare) {
+ //表示当前CollBook已经阅读
+ mCollBook.setIsUpdate(false);
+ mCollBook.setLastRead(StringUtils.
+ dateConvert(System.currentTimeMillis(), Constant.FORMAT_BOOK_DATE));
+ //直接更新
+ BookRepository.getInstance()
+ .saveCollBook(mCollBook);
+ }
+ }
+}
+
diff --git a/app/src/main/java/com/yzx/webebook/widget/page/PageLoader.java b/app/src/main/java/com/yzx/webebook/widget/page/PageLoader.java
new file mode 100644
index 0000000..af6683c
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/widget/page/PageLoader.java
@@ -0,0 +1,1450 @@
+package com.yzx.webebook.widget.page;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.text.TextPaint;
+
+
+import androidx.core.content.ContextCompat;
+
+import com.yzx.webebook.model.bean.BookRecordBean;
+import com.yzx.webebook.model.bean.CollBookBean;
+import com.yzx.webebook.model.local.BookRepository;
+import com.yzx.webebook.model.local.ReadSettingManager;
+import com.yzx.webebook.utils.Constant;
+import com.yzx.webebook.utils.IOUtils;
+import com.yzx.webebook.utils.RxUtils;
+import com.yzx.webebook.utils.ScreenUtils;
+import com.yzx.webebook.utils.StringUtils;
+
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import io.reactivex.Single;
+import io.reactivex.SingleEmitter;
+import io.reactivex.SingleObserver;
+import io.reactivex.SingleOnSubscribe;
+import io.reactivex.disposables.Disposable;
+
+/**
+ * Created by newbiechen on 17-7-1.
+ */
+
+public abstract class PageLoader {
+ private static final String TAG = "PageLoader";
+
+ // 当前页面的状态
+ public static final int STATUS_LOADING = 1; // 正在加载
+ public static final int STATUS_FINISH = 2; // 加载完成
+ public static final int STATUS_ERROR = 3; // 加载错误 (一般是网络加载情况)
+ public static final int STATUS_EMPTY = 4; // 空数据
+ public static final int STATUS_PARING = 5; // 正在解析 (装载本地数据)
+ public static final int STATUS_PARSE_ERROR = 6; // 本地文件解析错误(暂未被使用)
+ public static final int STATUS_CATEGORY_EMPTY = 7; // 获取到的目录为空
+ // 默认的显示参数配置
+ private static final int DEFAULT_MARGIN_HEIGHT = 28;
+ private static final int DEFAULT_MARGIN_WIDTH = 15;
+ private static final int DEFAULT_TIP_SIZE = 12;
+ private static final int EXTRA_TITLE_SIZE = 4;
+
+ // 当前章节列表
+ protected List mChapterList;
+ // 书本对象
+ protected CollBookBean mCollBook;
+ // 监听器
+ protected OnPageChangeListener mPageChangeListener;
+
+ private Context mContext;
+ // 页面显示类
+ private PageView mPageView;
+ // 当前显示的页
+ private TxtPage mCurPage;
+ // 上一章的页面列表缓存
+ private List mPrePageList;
+ // 当前章节的页面列表
+ private List mCurPageList;
+ // 下一章的页面列表缓存
+ private List mNextPageList;
+
+ // 绘制电池的画笔
+ private Paint mBatteryPaint;
+ // 绘制提示的画笔
+ private Paint mTipPaint;
+ // 绘制标题的画笔
+ private Paint mTitlePaint;
+ // 绘制背景颜色的画笔(用来擦除需要重绘的部分)
+ private Paint mBgPaint;
+ // 绘制小说内容的画笔
+ private TextPaint mTextPaint;
+ // 阅读器的配置选项
+ private ReadSettingManager mSettingManager;
+ // 被遮盖的页,或者认为被取消显示的页
+ private TxtPage mCancelPage;
+ // 存储阅读记录类
+ private BookRecordBean mBookRecord;
+
+ private Disposable mPreLoadDisp;
+
+ /*****************params**************************/
+ // 当前的状态
+ protected int mStatus = STATUS_LOADING;
+ // 判断章节列表是否加载完成
+ protected boolean isChapterListPrepare;
+
+ // 是否打开过章节
+ private boolean isChapterOpen;
+ private boolean isFirstOpen = true;
+ private boolean isClose;
+ // 页面的翻页效果模式
+ private PageMode mPageMode;
+ // 加载器的颜色主题
+ private PageStyle mPageStyle;
+ //当前是否是夜间模式
+ private boolean isNightMode;
+ //书籍绘制区域的宽高
+ private int mVisibleWidth;
+ private int mVisibleHeight;
+ //应用的宽高
+ private int mDisplayWidth;
+ private int mDisplayHeight;
+ //间距
+ private int mMarginWidth;
+ private int mMarginHeight;
+ //字体的颜色
+ private int mTextColor;
+ //标题的大小
+ private int mTitleSize;
+ //字体的大小
+ private int mTextSize;
+ //行间距
+ private int mTextInterval;
+ //标题的行间距
+ private int mTitleInterval;
+ //段落距离(基于行间距的额外距离)
+ private int mTextPara;
+ private int mTitlePara;
+ //电池的百分比
+ private int mBatteryLevel;
+ //当前页面的背景
+ private int mBgColor;
+
+ // 当前章
+ protected int mCurChapterPos = 0;
+ //上一章的记录
+ private int mLastChapterPos = 0;
+
+ /*****************************init params*******************************/
+ public PageLoader(PageView pageView, CollBookBean collBook) {
+ mPageView = pageView;
+ mContext = pageView.getContext();
+ mCollBook = collBook;
+ mChapterList = new ArrayList<>(1);
+
+ // 初始化数据
+ initData();
+ // 初始化画笔
+ initPaint();
+ // 初始化PageView
+ initPageView();
+ // 初始化书籍
+ prepareBook();
+ }
+
+ private void initData() {
+ // 获取配置管理器
+ mSettingManager = ReadSettingManager.getInstance();
+ // 获取配置参数
+ mPageMode = mSettingManager.getPageMode();
+ mPageStyle = mSettingManager.getPageStyle();
+ // 初始化参数
+ mMarginWidth = ScreenUtils.dpToPx(DEFAULT_MARGIN_WIDTH);
+ mMarginHeight = ScreenUtils.dpToPx(DEFAULT_MARGIN_HEIGHT);
+ // 配置文字有关的参数
+ setUpTextParams(mSettingManager.getTextSize());
+ }
+
+ /**
+ * 作用:设置与文字相关的参数
+ *
+ * @param textSize
+ */
+ private void setUpTextParams(int textSize) {
+ // 文字大小
+ mTextSize = textSize;
+ mTitleSize = mTextSize + ScreenUtils.spToPx(EXTRA_TITLE_SIZE);
+ // 行间距(大小为字体的一半)
+ mTextInterval = mTextSize / 2;
+ mTitleInterval = mTitleSize / 2;
+ // 段落间距(大小为字体的高度)
+ mTextPara = mTextSize;
+ mTitlePara = mTitleSize;
+ }
+
+ private void initPaint() {
+ // 绘制提示的画笔
+ mTipPaint = new Paint();
+ mTipPaint.setColor(mTextColor);
+ mTipPaint.setTextAlign(Paint.Align.LEFT); // 绘制的起始点
+ mTipPaint.setTextSize(ScreenUtils.spToPx(DEFAULT_TIP_SIZE)); // Tip默认的字体大小
+ mTipPaint.setAntiAlias(true);
+ mTipPaint.setSubpixelText(true);
+
+ // 绘制页面内容的画笔
+ mTextPaint = new TextPaint();
+ mTextPaint.setColor(mTextColor);
+ mTextPaint.setTextSize(mTextSize);
+ mTextPaint.setAntiAlias(true);
+
+ // 绘制标题的画笔
+ mTitlePaint = new TextPaint();
+ mTitlePaint.setColor(mTextColor);
+ mTitlePaint.setTextSize(mTitleSize);
+ mTitlePaint.setStyle(Paint.Style.FILL_AND_STROKE);
+ mTitlePaint.setTypeface(Typeface.DEFAULT_BOLD);
+ mTitlePaint.setAntiAlias(true);
+
+ // 绘制背景的画笔
+ mBgPaint = new Paint();
+ mBgPaint.setColor(mBgColor);
+
+ // 绘制电池的画笔
+ mBatteryPaint = new Paint();
+ mBatteryPaint.setAntiAlias(true);
+ mBatteryPaint.setDither(true);
+
+ // 初始化页面样式
+ setNightMode(mSettingManager.isNightMode());
+ }
+
+ private void initPageView() {
+ //配置参数
+ mPageView.setPageMode(mPageMode);
+ mPageView.setBgColor(mBgColor);
+ }
+
+ /****************************** public method***************************/
+ /**
+ * 跳转到上一章
+ *
+ * @return
+ */
+ public boolean skipPreChapter() {
+ if (!hasPrevChapter()) {
+ return false;
+ }
+
+ // 载入上一章。
+ if (parsePrevChapter()) {
+ mCurPage = getCurPage(0);
+ } else {
+ mCurPage = new TxtPage();
+ }
+ mPageView.drawCurPage(false);
+ return true;
+ }
+
+ /**
+ * 跳转到下一章
+ *
+ * @return
+ */
+ public boolean skipNextChapter() {
+ if (!hasNextChapter()) {
+ return false;
+ }
+
+ //判断是否达到章节的终止点
+ if (parseNextChapter()) {
+ mCurPage = getCurPage(0);
+ } else {
+ mCurPage = new TxtPage();
+ }
+ mPageView.drawCurPage(false);
+ return true;
+ }
+
+ /**
+ * 跳转到指定章节
+ *
+ * @param pos:从 0 开始。
+ */
+ public void skipToChapter(int pos) {
+ // 设置参数
+ mCurChapterPos = pos;
+
+ // 将上一章的缓存设置为null
+ mPrePageList = null;
+ // 如果当前下一章缓存正在执行,则取消
+ if (mPreLoadDisp != null) {
+ mPreLoadDisp.dispose();
+ }
+ // 将下一章缓存设置为null
+ mNextPageList = null;
+
+ // 打开指定章节
+ openChapter();
+ }
+
+ /**
+ * 跳转到指定的页
+ *
+ * @param pos
+ */
+ public boolean skipToPage(int pos) {
+ if (!isChapterListPrepare) {
+ return false;
+ }
+ mCurPage = getCurPage(pos);
+ mPageView.drawCurPage(false);
+ return true;
+ }
+
+ /**
+ * 翻到上一页
+ *
+ * @return
+ */
+ public boolean skipToPrePage() {
+ return mPageView.autoPrevPage();
+ }
+
+ /**
+ * 翻到下一页
+ *
+ * @return
+ */
+ public boolean skipToNextPage() {
+ return mPageView.autoNextPage();
+ }
+
+ /**
+ * 更新时间
+ */
+ public void updateTime() {
+ if (!mPageView.isRunning()) {
+ mPageView.drawCurPage(true);
+ }
+ }
+
+ /**
+ * 更新电量
+ *
+ * @param level
+ */
+ public void updateBattery(int level) {
+ mBatteryLevel = level;
+
+ if (!mPageView.isRunning()) {
+ mPageView.drawCurPage(true);
+ }
+ }
+
+ /**
+ * 设置提示的文字大小
+ *
+ * @param textSize:单位为 px。
+ */
+ public void setTipTextSize(int textSize) {
+ mTipPaint.setTextSize(textSize);
+
+ // 如果屏幕大小加载完成
+ mPageView.drawCurPage(false);
+ }
+
+ /**
+ * 设置文字相关参数
+ *
+ * @param textSize
+ */
+ public void setTextSize(int textSize) {
+ // 设置文字相关参数
+ setUpTextParams(textSize);
+
+ // 设置画笔的字体大小
+ mTextPaint.setTextSize(mTextSize);
+ // 设置标题的字体大小
+ mTitlePaint.setTextSize(mTitleSize);
+ // 存储文字大小
+ mSettingManager.setTextSize(mTextSize);
+ // 取消缓存
+ mPrePageList = null;
+ mNextPageList = null;
+
+ // 如果当前已经显示数据
+ if (isChapterListPrepare && mStatus == STATUS_FINISH) {
+ // 重新计算当前页面
+ dealLoadPageList(mCurChapterPos);
+
+ // 防止在最后一页,通过修改字体,以至于页面数减少导致崩溃的问题
+ if (mCurPage.position >= mCurPageList.size()) {
+ mCurPage.position = mCurPageList.size() - 1;
+ }
+
+ // 重新获取指定页面
+ mCurPage = mCurPageList.get(mCurPage.position);
+ }
+
+ mPageView.drawCurPage(false);
+ }
+
+ /**
+ * 设置夜间模式
+ *
+ * @param nightMode
+ */
+ public void setNightMode(boolean nightMode) {
+ mSettingManager.setNightMode(nightMode);
+ isNightMode = nightMode;
+
+ if (isNightMode) {
+ mBatteryPaint.setColor(Color.WHITE);
+ setPageStyle(PageStyle.NIGHT);
+ } else {
+ mBatteryPaint.setColor(Color.BLACK);
+ setPageStyle(mPageStyle);
+ }
+ }
+
+ /**
+ * 设置页面样式
+ *
+ * @param pageStyle:页面样式
+ */
+ public void setPageStyle(PageStyle pageStyle) {
+ if (pageStyle != PageStyle.NIGHT) {
+ mPageStyle = pageStyle;
+ mSettingManager.setPageStyle(pageStyle);
+ }
+
+ if (isNightMode && pageStyle != PageStyle.NIGHT) {
+ return;
+ }
+
+ // 设置当前颜色样式
+ mTextColor = ContextCompat.getColor(mContext, pageStyle.getFontColor());
+ mBgColor = ContextCompat.getColor(mContext, pageStyle.getBgColor());
+
+ mTipPaint.setColor(mTextColor);
+ mTitlePaint.setColor(mTextColor);
+ mTextPaint.setColor(mTextColor);
+
+ mBgPaint.setColor(mBgColor);
+
+ mPageView.drawCurPage(false);
+ }
+
+ /**
+ * 翻页动画
+ *
+ * @param pageMode:翻页模式
+ * @see PageMode
+ */
+ public void setPageMode(PageMode pageMode) {
+ mPageMode = pageMode;
+
+ mPageView.setPageMode(mPageMode);
+ mSettingManager.setPageMode(mPageMode);
+
+ // 重新绘制当前页
+ mPageView.drawCurPage(false);
+ }
+
+ /**
+ * 设置内容与屏幕的间距
+ *
+ * @param marginWidth :单位为 px
+ * @param marginHeight :单位为 px
+ */
+ public void setMargin(int marginWidth, int marginHeight) {
+ mMarginWidth = marginWidth;
+ mMarginHeight = marginHeight;
+
+ // 如果是滑动动画,则需要重新创建了
+ if (mPageMode == PageMode.SCROLL) {
+ mPageView.setPageMode(PageMode.SCROLL);
+ }
+
+ mPageView.drawCurPage(false);
+ }
+
+ /**
+ * 设置页面切换监听
+ *
+ * @param listener
+ */
+ public void setOnPageChangeListener(OnPageChangeListener listener) {
+ mPageChangeListener = listener;
+
+ // 如果目录加载完之后才设置监听器,那么会默认回调
+ if (isChapterListPrepare) {
+ mPageChangeListener.onCategoryFinish(mChapterList);
+ }
+ }
+
+ /**
+ * 获取当前页的状态
+ *
+ * @return
+ */
+ public int getPageStatus() {
+ return mStatus;
+ }
+
+ /**
+ * 获取书籍信息
+ *
+ * @return
+ */
+ public CollBookBean getCollBook() {
+ return mCollBook;
+ }
+
+ /**
+ * 获取章节目录。
+ *
+ * @return
+ */
+ public List getChapterCategory() {
+ return mChapterList;
+ }
+
+ /**
+ * 获取当前页的页码
+ *
+ * @return
+ */
+ public int getPagePos() {
+ return mCurPage.position;
+ }
+
+ /**
+ * 获取当前章节的章节位置
+ *
+ * @return
+ */
+ public int getChapterPos() {
+ return mCurChapterPos;
+ }
+
+ /**
+ * 获取距离屏幕的高度
+ *
+ * @return
+ */
+ public int getMarginHeight() {
+ return mMarginHeight;
+ }
+
+ /**
+ * 保存阅读记录
+ */
+ public void saveRecord() {
+
+ if (mChapterList.isEmpty()) {
+ return;
+ }
+
+ mBookRecord.setBookId(mCollBook.get_id());
+ mBookRecord.setChapter(mCurChapterPos);
+
+ if (mCurPage != null) {
+ mBookRecord.setPagePos(mCurPage.position);
+ } else {
+ mBookRecord.setPagePos(0);
+ }
+
+ //存储到数据库
+ BookRepository.getInstance()
+ .saveBookRecord(mBookRecord);
+ }
+
+ /**
+ * 初始化书籍
+ */
+ private void prepareBook() {
+ mBookRecord = BookRepository.getInstance()
+ .getBookRecord(mCollBook.get_id());
+
+ if (mBookRecord == null) {
+ mBookRecord = new BookRecordBean();
+ }
+
+ mCurChapterPos = mBookRecord.getChapter();
+ mLastChapterPos = mCurChapterPos;
+ }
+
+ /**
+ * 打开指定章节
+ */
+ public void openChapter() {
+ isFirstOpen = false;
+
+ if (!mPageView.isPrepare()) {
+ return;
+ }
+
+ // 如果章节目录没有准备好
+ if (!isChapterListPrepare) {
+ mStatus = STATUS_LOADING;
+ mPageView.drawCurPage(false);
+ return;
+ }
+
+ // 如果获取到的章节目录为空
+ if (mChapterList.isEmpty()) {
+ mStatus = STATUS_CATEGORY_EMPTY;
+ mPageView.drawCurPage(false);
+ return;
+ }
+
+ if (parseCurChapter()) {
+ // 如果章节从未打开
+ if (!isChapterOpen) {
+ int position = mBookRecord.getPagePos();
+
+ // 防止记录页的页号,大于当前最大页号
+ if (position >= mCurPageList.size()) {
+ position = mCurPageList.size() - 1;
+ }
+ mCurPage = getCurPage(position);
+ mCancelPage = mCurPage;
+ // 切换状态
+ isChapterOpen = true;
+ } else {
+ mCurPage = getCurPage(0);
+ }
+ } else {
+ mCurPage = new TxtPage();
+ }
+
+ mPageView.drawCurPage(false);
+ }
+
+ public void chapterError() {
+ //加载错误
+ mStatus = STATUS_ERROR;
+ mPageView.drawCurPage(false);
+ }
+
+ /**
+ * 关闭书本
+ */
+ public void closeBook() {
+ isChapterListPrepare = false;
+ isClose = true;
+
+ if (mPreLoadDisp != null) {
+ mPreLoadDisp.dispose();
+ }
+
+ clearList(mChapterList);
+ clearList(mCurPageList);
+ clearList(mNextPageList);
+
+ mChapterList = null;
+ mCurPageList = null;
+ mNextPageList = null;
+ mPageView = null;
+ mCurPage = null;
+ }
+
+ private void clearList(List list) {
+ if (list != null) {
+ list.clear();
+ }
+ }
+
+ public boolean isClose() {
+ return isClose;
+ }
+
+ public boolean isChapterOpen() {
+ return isChapterOpen;
+ }
+
+ /**
+ * 加载页面列表
+ *
+ * @param chapterPos:章节序号
+ * @return
+ */
+ private List loadPageList(int chapterPos) throws Exception {
+ // 获取章节
+ TxtChapter chapter = mChapterList.get(chapterPos);
+ // 判断章节是否存在
+ if (!hasChapterData(chapter)) {
+ return null;
+ }
+ // 获取章节的文本流
+ BufferedReader reader = getChapterReader(chapter);
+ List chapters = loadPages(chapter, reader);
+
+ return chapters;
+ }
+
+ /*******************************abstract method***************************************/
+
+ /**
+ * 刷新章节列表
+ */
+ public abstract void refreshChapterList();
+
+ /**
+ * 获取章节的文本流
+ *
+ * @param chapter
+ * @return
+ */
+ protected abstract BufferedReader getChapterReader(TxtChapter chapter) throws Exception;
+
+ /**
+ * 章节数据是否存在
+ *
+ * @return
+ */
+ protected abstract boolean hasChapterData(TxtChapter chapter);
+
+ /***********************************default method***********************************************/
+
+ void drawPage(Bitmap bitmap, boolean isUpdate) {
+ drawBackground(mPageView.getBgBitmap(), isUpdate);
+ if (!isUpdate) {
+ drawContent(bitmap);
+ }
+ //更新绘制
+ mPageView.invalidate();
+ }
+
+ private void drawBackground(Bitmap bitmap, boolean isUpdate) {
+ Canvas canvas = new Canvas(bitmap);
+ int tipMarginHeight = ScreenUtils.dpToPx(3);
+ if (!isUpdate) {
+ /****绘制背景****/
+ canvas.drawColor(mBgColor);
+
+ if (!mChapterList.isEmpty()) {
+ /*****初始化标题的参数********/
+ //需要注意的是:绘制text的y的起始点是text的基准线的位置,而不是从text的头部的位置
+ float tipTop = tipMarginHeight - mTipPaint.getFontMetrics().top;
+ //根据状态不一样,数据不一样
+ if (mStatus != STATUS_FINISH) {
+ if (isChapterListPrepare) {
+ canvas.drawText(mChapterList.get(mCurChapterPos).getTitle()
+ , mMarginWidth, tipTop, mTipPaint);
+ }
+ } else {
+ canvas.drawText(mCurPage.title, mMarginWidth, tipTop, mTipPaint);
+ }
+
+ /******绘制页码********/
+ // 底部的字显示的位置Y
+ float y = mDisplayHeight - mTipPaint.getFontMetrics().bottom - tipMarginHeight;
+ // 只有finish的时候采用页码
+ if (mStatus == STATUS_FINISH) {
+ String percent = (mCurPage.position + 1) + "/" + mCurPageList.size();
+ canvas.drawText(percent, mMarginWidth, y, mTipPaint);
+ }
+ }
+ } else {
+ //擦除区域
+ mBgPaint.setColor(mBgColor);
+ canvas.drawRect(mDisplayWidth / 2, mDisplayHeight - mMarginHeight + ScreenUtils.dpToPx(2), mDisplayWidth, mDisplayHeight, mBgPaint);
+ }
+
+ /******绘制电池********/
+
+ int visibleRight = mDisplayWidth - mMarginWidth;
+ int visibleBottom = mDisplayHeight - tipMarginHeight;
+
+ int outFrameWidth = (int) mTipPaint.measureText("xxx");
+ int outFrameHeight = (int) mTipPaint.getTextSize();
+
+ int polarHeight = ScreenUtils.dpToPx(6);
+ int polarWidth = ScreenUtils.dpToPx(2);
+ int border = 1;
+ int innerMargin = 1;
+
+ //电极的制作
+ int polarLeft = visibleRight - polarWidth;
+ int polarTop = visibleBottom - (outFrameHeight + polarHeight) / 2;
+ Rect polar = new Rect(polarLeft, polarTop, visibleRight,
+ polarTop + polarHeight - ScreenUtils.dpToPx(2));
+
+ mBatteryPaint.setStyle(Paint.Style.FILL);
+ canvas.drawRect(polar, mBatteryPaint);
+
+ //外框的制作
+ int outFrameLeft = polarLeft - outFrameWidth;
+ int outFrameTop = visibleBottom - outFrameHeight;
+ int outFrameBottom = visibleBottom - ScreenUtils.dpToPx(2);
+ Rect outFrame = new Rect(outFrameLeft, outFrameTop, polarLeft, outFrameBottom);
+
+ mBatteryPaint.setStyle(Paint.Style.STROKE);
+ mBatteryPaint.setStrokeWidth(border);
+ canvas.drawRect(outFrame, mBatteryPaint);
+
+ //内框的制作
+ float innerWidth = (outFrame.width() - innerMargin * 2 - border) * (mBatteryLevel / 100.0f);
+ RectF innerFrame = new RectF(outFrameLeft + border + innerMargin, outFrameTop + border + innerMargin,
+ outFrameLeft + border + innerMargin + innerWidth, outFrameBottom - border - innerMargin);
+
+ mBatteryPaint.setStyle(Paint.Style.FILL);
+ canvas.drawRect(innerFrame, mBatteryPaint);
+
+ /******绘制当前时间********/
+ //底部的字显示的位置Y
+ float y = mDisplayHeight - mTipPaint.getFontMetrics().bottom - tipMarginHeight;
+ String time = StringUtils.dateConvert(System.currentTimeMillis(), Constant.FORMAT_TIME);
+ float x = outFrameLeft - mTipPaint.measureText(time) - ScreenUtils.dpToPx(4);
+ canvas.drawText(time, x, y, mTipPaint);
+ }
+
+ private void drawContent(Bitmap bitmap) {
+ Canvas canvas = new Canvas(bitmap);
+
+ if (mPageMode == PageMode.SCROLL) {
+ canvas.drawColor(mBgColor);
+ }
+ /******绘制内容****/
+
+ if (mStatus != STATUS_FINISH) {
+ //绘制字体
+ String tip = "";
+ switch (mStatus) {
+ case STATUS_LOADING:
+ tip = "正在拼命加载中...";
+ break;
+ case STATUS_ERROR:
+ tip = "加载失败(点击边缘重试)";
+ break;
+ case STATUS_EMPTY:
+ tip = "文章内容为空";
+ break;
+ case STATUS_PARING:
+ tip = "正在排版请等待...";
+ break;
+ case STATUS_PARSE_ERROR:
+ tip = "文件解析错误";
+ break;
+ case STATUS_CATEGORY_EMPTY:
+ tip = "目录列表为空";
+ break;
+ }
+
+ //将提示语句放到正中间
+ Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
+ float textHeight = fontMetrics.top - fontMetrics.bottom;
+ float textWidth = mTextPaint.measureText(tip);
+ float pivotX = (mDisplayWidth - textWidth) / 2;
+ float pivotY = (mDisplayHeight - textHeight) / 2;
+ canvas.drawText(tip, pivotX, pivotY, mTextPaint);
+ } else {
+ float top;
+
+ if (mPageMode == PageMode.SCROLL) {
+ top = -mTextPaint.getFontMetrics().top;
+ } else {
+ top = mMarginHeight - mTextPaint.getFontMetrics().top;
+ }
+
+ //设置总距离
+ int interval = mTextInterval + (int) mTextPaint.getTextSize();
+ int para = mTextPara + (int) mTextPaint.getTextSize();
+ int titleInterval = mTitleInterval + (int) mTitlePaint.getTextSize();
+ int titlePara = mTitlePara + (int) mTextPaint.getTextSize();
+ String str = null;
+
+ //对标题进行绘制
+ for (int i = 0; i < mCurPage.titleLines; ++i) {
+ str = mCurPage.lines.get(i);
+
+ //设置顶部间距
+ if (i == 0) {
+ top += mTitlePara;
+ }
+
+ //计算文字显示的起始点
+ int start = (int) (mDisplayWidth - mTitlePaint.measureText(str)) / 2;
+ //进行绘制
+ canvas.drawText(str, start, top, mTitlePaint);
+
+ //设置尾部间距
+ if (i == mCurPage.titleLines - 1) {
+ top += titlePara;
+ } else {
+ //行间距
+ top += titleInterval;
+ }
+ }
+
+ //对内容进行绘制
+ for (int i = mCurPage.titleLines; i < mCurPage.lines.size(); ++i) {
+ str = mCurPage.lines.get(i);
+
+ canvas.drawText(str, mMarginWidth, top, mTextPaint);
+ if (str.endsWith("\n")) {
+ top += para;
+ } else {
+ top += interval;
+ }
+ }
+ }
+ }
+
+ void prepareDisplay(int w, int h) {
+ // 获取PageView的宽高
+ mDisplayWidth = w;
+ mDisplayHeight = h;
+
+ // 获取内容显示位置的大小
+ mVisibleWidth = mDisplayWidth - mMarginWidth * 2;
+ mVisibleHeight = mDisplayHeight - mMarginHeight * 2;
+
+ // 重置 PageMode
+ mPageView.setPageMode(mPageMode);
+
+ if (!isChapterOpen) {
+ // 展示加载界面
+ mPageView.drawCurPage(false);
+ // 如果在 display 之前调用过 openChapter 肯定是无法打开的。
+ // 所以需要通过 display 再重新调用一次。
+ if (!isFirstOpen) {
+ // 打开书籍
+ openChapter();
+ }
+ } else {
+ // 如果章节已显示,那么就重新计算页面
+ if (mStatus == STATUS_FINISH) {
+ dealLoadPageList(mCurChapterPos);
+ // 重新设置文章指针的位置
+ mCurPage = getCurPage(mCurPage.position);
+ }
+ mPageView.drawCurPage(false);
+ }
+ }
+
+ /**
+ * 翻阅上一页
+ *
+ * @return
+ */
+ boolean prev() {
+ // 以下情况禁止翻页
+ if (!canTurnPage()) {
+ return false;
+ }
+
+ if (mStatus == STATUS_FINISH) {
+ // 先查看是否存在上一页
+ TxtPage prevPage = getPrevPage();
+ if (prevPage != null) {
+ mCancelPage = mCurPage;
+ mCurPage = prevPage;
+ mPageView.drawNextPage();
+ return true;
+ }
+ }
+
+ if (!hasPrevChapter()) {
+ return false;
+ }
+
+ mCancelPage = mCurPage;
+ if (parsePrevChapter()) {
+ mCurPage = getPrevLastPage();
+ } else {
+ mCurPage = new TxtPage();
+ }
+ mPageView.drawNextPage();
+ return true;
+ }
+
+ /**
+ * 解析上一章数据
+ *
+ * @return:数据是否解析成功
+ */
+ boolean parsePrevChapter() {
+ // 加载上一章数据
+ int prevChapter = mCurChapterPos - 1;
+
+ mLastChapterPos = mCurChapterPos;
+ mCurChapterPos = prevChapter;
+
+ // 当前章缓存为下一章
+ mNextPageList = mCurPageList;
+
+ // 判断是否具有上一章缓存
+ if (mPrePageList != null) {
+ mCurPageList = mPrePageList;
+ mPrePageList = null;
+
+ // 回调
+ chapterChangeCallback();
+ } else {
+ dealLoadPageList(prevChapter);
+ }
+ return mCurPageList != null ? true : false;
+ }
+
+ private boolean hasPrevChapter() {
+ //判断是否上一章节为空
+ if (mCurChapterPos - 1 < 0) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * 翻到下一页
+ *
+ * @return:是否允许翻页
+ */
+ boolean next() {
+ // 以下情况禁止翻页
+ if (!canTurnPage()) {
+ return false;
+ }
+
+ if (mStatus == STATUS_FINISH) {
+ // 先查看是否存在下一页
+ TxtPage nextPage = getNextPage();
+ if (nextPage != null) {
+ mCancelPage = mCurPage;
+ mCurPage = nextPage;
+ mPageView.drawNextPage();
+ return true;
+ }
+ }
+
+ if (!hasNextChapter()) {
+ return false;
+ }
+
+ mCancelPage = mCurPage;
+ // 解析下一章数据
+ if (parseNextChapter()) {
+ mCurPage = mCurPageList.get(0);
+ } else {
+ mCurPage = new TxtPage();
+ }
+ mPageView.drawNextPage();
+ return true;
+ }
+
+ private boolean hasNextChapter() {
+ // 判断是否到达目录最后一章
+ if (mCurChapterPos + 1 >= mChapterList.size()) {
+ return false;
+ }
+ return true;
+ }
+
+ boolean parseCurChapter() {
+ // 解析数据
+ dealLoadPageList(mCurChapterPos);
+ // 预加载下一页面
+ preLoadNextChapter();
+ return mCurPageList != null ? true : false;
+ }
+
+ /**
+ * 解析下一章数据
+ *
+ * @return:返回解析成功还是失败
+ */
+ boolean parseNextChapter() {
+ int nextChapter = mCurChapterPos + 1;
+
+ mLastChapterPos = mCurChapterPos;
+ mCurChapterPos = nextChapter;
+
+ // 将当前章的页面列表,作为上一章缓存
+ mPrePageList = mCurPageList;
+
+ // 是否下一章数据已经预加载了
+ if (mNextPageList != null) {
+ mCurPageList = mNextPageList;
+ mNextPageList = null;
+ // 回调
+ chapterChangeCallback();
+ } else {
+ // 处理页面解析
+ dealLoadPageList(nextChapter);
+ }
+ // 预加载下一页面
+ preLoadNextChapter();
+ return mCurPageList != null ? true : false;
+ }
+
+ private void dealLoadPageList(int chapterPos) {
+ try {
+ mCurPageList = loadPageList(chapterPos);
+ if (mCurPageList != null) {
+ if (mCurPageList.isEmpty()) {
+ mStatus = STATUS_EMPTY;
+
+ // 添加一个空数据
+ TxtPage page = new TxtPage();
+ page.lines = new ArrayList<>(1);
+ mCurPageList.add(page);
+ } else {
+ mStatus = STATUS_FINISH;
+ }
+ } else {
+ mStatus = STATUS_LOADING;
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+
+ mCurPageList = null;
+ mStatus = STATUS_ERROR;
+ }
+
+ // 回调
+ chapterChangeCallback();
+ }
+
+ private void chapterChangeCallback() {
+ if (mPageChangeListener != null) {
+ mPageChangeListener.onChapterChange(mCurChapterPos);
+ mPageChangeListener.onPageCountChange(mCurPageList != null ? mCurPageList.size() : 0);
+ }
+ }
+
+ // 预加载下一章
+ private void preLoadNextChapter() {
+ int nextChapter = mCurChapterPos + 1;
+
+ // 如果不存在下一章,且下一章没有数据,则不进行加载。
+ if (!hasNextChapter()
+ || !hasChapterData(mChapterList.get(nextChapter))) {
+ return;
+ }
+
+ //如果之前正在加载则取消
+ if (mPreLoadDisp != null) {
+ mPreLoadDisp.dispose();
+ }
+
+ //调用异步进行预加载加载
+ Single.create(new SingleOnSubscribe>() {
+ @Override
+ public void subscribe(SingleEmitter> e) throws Exception {
+ e.onSuccess(loadPageList(nextChapter));
+ }
+ }).compose(RxUtils::toSimpleSingle)
+ .subscribe(new SingleObserver>() {
+ @Override
+ public void onSubscribe(Disposable d) {
+ mPreLoadDisp = d;
+ }
+
+ @Override
+ public void onSuccess(List pages) {
+ mNextPageList = pages;
+ }
+
+ @Override
+ public void onError(Throwable e) {
+ //无视错误
+ }
+ });
+ }
+
+ // 取消翻页
+ void pageCancel() {
+ if (mCurPage.position == 0 && mCurChapterPos > mLastChapterPos) { // 加载到下一章取消了
+ if (mPrePageList != null) {
+ cancelNextChapter();
+ } else {
+ if (parsePrevChapter()) {
+ mCurPage = getPrevLastPage();
+ } else {
+ mCurPage = new TxtPage();
+ }
+ }
+ } else if (mCurPageList == null
+ || (mCurPage.position == mCurPageList.size() - 1
+ && mCurChapterPos < mLastChapterPos)) { // 加载上一章取消了
+
+ if (mNextPageList != null) {
+ cancelPreChapter();
+ } else {
+ if (parseNextChapter()) {
+ mCurPage = mCurPageList.get(0);
+ } else {
+ mCurPage = new TxtPage();
+ }
+ }
+ } else {
+ // 假设加载到下一页,又取消了。那么需要重新装载。
+ mCurPage = mCancelPage;
+ }
+ }
+
+ private void cancelNextChapter() {
+ int temp = mLastChapterPos;
+ mLastChapterPos = mCurChapterPos;
+ mCurChapterPos = temp;
+
+ mNextPageList = mCurPageList;
+ mCurPageList = mPrePageList;
+ mPrePageList = null;
+
+ chapterChangeCallback();
+
+ mCurPage = getPrevLastPage();
+ mCancelPage = null;
+ }
+
+ private void cancelPreChapter() {
+ // 重置位置点
+ int temp = mLastChapterPos;
+ mLastChapterPos = mCurChapterPos;
+ mCurChapterPos = temp;
+ // 重置页面列表
+ mPrePageList = mCurPageList;
+ mCurPageList = mNextPageList;
+ mNextPageList = null;
+
+ chapterChangeCallback();
+
+ mCurPage = getCurPage(0);
+ mCancelPage = null;
+ }
+
+ /**************************************private method********************************************/
+ /**
+ * 将章节数据,解析成页面列表
+ *
+ * @param chapter:章节信息
+ * @param br:章节的文本流
+ * @return
+ */
+ private List loadPages(TxtChapter chapter, BufferedReader br) {
+ //生成的页面
+ List pages = new ArrayList<>();
+ //使用流的方式加载
+ List lines = new ArrayList<>();
+ int rHeight = mVisibleHeight;
+ int titleLinesCount = 0;
+ boolean showTitle = true; // 是否展示标题
+ String paragraph = chapter.getTitle();//默认展示标题
+ try {
+ while (showTitle || (paragraph = br.readLine()) != null) {
+ paragraph = StringUtils.convertCC(paragraph, mContext);
+ // 重置段落
+ if (!showTitle) {
+ paragraph = paragraph.replaceAll("\\s", "");
+ // 如果只有换行符,那么就不执行
+ if (paragraph.equals("")) continue;
+ paragraph = StringUtils.halfToFull(" " + paragraph + "\n");
+ } else {
+ //设置 title 的顶部间距
+ rHeight -= mTitlePara;
+ }
+ int wordCount = 0;
+ String subStr = null;
+ while (paragraph.length() > 0) {
+ //当前空间,是否容得下一行文字
+ if (showTitle) {
+ rHeight -= mTitlePaint.getTextSize();
+ } else {
+ rHeight -= mTextPaint.getTextSize();
+ }
+ // 一页已经填充满了,创建 TextPage
+ if (rHeight <= 0) {
+ // 创建Page
+ TxtPage page = new TxtPage();
+ page.position = pages.size();
+ page.title = StringUtils.convertCC(chapter.getTitle(), mContext);
+ page.lines = new ArrayList<>(lines);
+ page.titleLines = titleLinesCount;
+ pages.add(page);
+ // 重置Lines
+ lines.clear();
+ rHeight = mVisibleHeight;
+ titleLinesCount = 0;
+
+ continue;
+ }
+
+ //测量一行占用的字节数
+ if (showTitle) {
+ wordCount = mTitlePaint.breakText(paragraph,
+ true, mVisibleWidth, null);
+ } else {
+ wordCount = mTextPaint.breakText(paragraph,
+ true, mVisibleWidth, null);
+ }
+
+ subStr = paragraph.substring(0, wordCount);
+ if (!subStr.equals("\n")) {
+ //将一行字节,存储到lines中
+ lines.add(subStr);
+
+ //设置段落间距
+ if (showTitle) {
+ titleLinesCount += 1;
+ rHeight -= mTitleInterval;
+ } else {
+ rHeight -= mTextInterval;
+ }
+ }
+ //裁剪
+ paragraph = paragraph.substring(wordCount);
+ }
+
+ //增加段落的间距
+ if (!showTitle && lines.size() != 0) {
+ rHeight = rHeight - mTextPara + mTextInterval;
+ }
+
+ if (showTitle) {
+ rHeight = rHeight - mTitlePara + mTitleInterval;
+ showTitle = false;
+ }
+ }
+
+ if (lines.size() != 0) {
+ //创建Page
+ TxtPage page = new TxtPage();
+ page.position = pages.size();
+ page.title = StringUtils.convertCC(chapter.getTitle(), mContext);
+ page.lines = new ArrayList<>(lines);
+ page.titleLines = titleLinesCount;
+ pages.add(page);
+ //重置Lines
+ lines.clear();
+ }
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ IOUtils.close(br);
+ }
+ return pages;
+ }
+
+
+ /**
+ * @return:获取初始显示的页面
+ */
+ private TxtPage getCurPage(int pos) {
+ if (mPageChangeListener != null) {
+ mPageChangeListener.onPageChange(pos);
+ }
+ return mCurPageList.get(pos);
+ }
+
+ /**
+ * @return:获取上一个页面
+ */
+ private TxtPage getPrevPage() {
+ int pos = mCurPage.position - 1;
+ if (pos < 0) {
+ return null;
+ }
+ if (mPageChangeListener != null) {
+ mPageChangeListener.onPageChange(pos);
+ }
+ return mCurPageList.get(pos);
+ }
+
+ /**
+ * @return:获取下一的页面
+ */
+ private TxtPage getNextPage() {
+ int pos = mCurPage.position + 1;
+ if (pos >= mCurPageList.size()) {
+ return null;
+ }
+ if (mPageChangeListener != null) {
+ mPageChangeListener.onPageChange(pos);
+ }
+ return mCurPageList.get(pos);
+ }
+
+ /**
+ * @return:获取上一个章节的最后一页
+ */
+ private TxtPage getPrevLastPage() {
+ int pos = mCurPageList.size() - 1;
+
+ if (mPageChangeListener != null) {
+ mPageChangeListener.onPageChange(pos);
+ }
+
+ return mCurPageList.get(pos);
+ }
+
+ /**
+ * 根据当前状态,决定是否能够翻页
+ *
+ * @return
+ */
+ private boolean canTurnPage() {
+
+ if (!isChapterListPrepare) {
+ return false;
+ }
+
+ if (mStatus == STATUS_PARSE_ERROR
+ || mStatus == STATUS_PARING) {
+ return false;
+ } else if (mStatus == STATUS_ERROR) {
+ mStatus = STATUS_LOADING;
+ }
+ return true;
+ }
+
+ /*****************************************interface*****************************************/
+
+ public interface OnPageChangeListener {
+ /**
+ * 作用:章节切换的时候进行回调
+ *
+ * @param pos:切换章节的序号
+ */
+ void onChapterChange(int pos);
+
+ /**
+ * 作用:请求加载章节内容
+ *
+ * @param requestChapters:需要下载的章节列表
+ */
+ void requestChapters(List requestChapters);
+
+ /**
+ * 作用:章节目录加载完成时候回调
+ *
+ * @param chapters:返回章节目录
+ */
+ void onCategoryFinish(List chapters);
+
+ /**
+ * 作用:章节页码数量改变之后的回调。==> 字体大小的调整,或者是否关闭虚拟按钮功能都会改变页面的数量。
+ *
+ * @param count:页面的数量
+ */
+ void onPageCountChange(int count);
+
+ /**
+ * 作用:当页面改变的时候回调
+ *
+ * @param pos:当前的页面的序号
+ */
+ void onPageChange(int pos);
+ }
+}
diff --git a/app/src/main/java/com/yzx/webebook/widget/page/PageMode.java b/app/src/main/java/com/yzx/webebook/widget/page/PageMode.java
new file mode 100644
index 0000000..7099dda
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/widget/page/PageMode.java
@@ -0,0 +1,10 @@
+package com.yzx.webebook.widget.page;
+
+/**
+ * Created by newbiechen on 2018/2/5.
+ * 作用:翻页动画的模式
+ */
+
+public enum PageMode {
+ SIMULATION, COVER, SLIDE, NONE, SCROLL
+}
diff --git a/app/src/main/java/com/yzx/webebook/widget/page/PageStyle.java b/app/src/main/java/com/yzx/webebook/widget/page/PageStyle.java
new file mode 100644
index 0000000..1319fe9
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/widget/page/PageStyle.java
@@ -0,0 +1,38 @@
+package com.yzx.webebook.widget.page;
+
+
+
+import androidx.annotation.ColorRes;
+
+import com.yzx.webebook.R;
+
+
+/**
+ * Created by newbiechen on 2018/2/5.
+ * 作用:页面的展示风格。
+ */
+
+public enum PageStyle {
+ BG_0(R.color.nb_read_font_1, R.color.nb_read_bg_1),
+ BG_1(R.color.nb_read_font_2, R.color.nb_read_bg_2),
+ BG_2(R.color.nb_read_font_3, R.color.nb_read_bg_3),
+ BG_3(R.color.nb_read_font_4, R.color.nb_read_bg_4),
+ BG_4(R.color.nb_read_font_5, R.color.nb_read_bg_5),
+ NIGHT(R.color.nb_read_font_night, R.color.nb_read_bg_night),;
+
+ private int fontColor;
+ private int bgColor;
+
+ PageStyle(@ColorRes int fontColor, @ColorRes int bgColor) {
+ this.fontColor = fontColor;
+ this.bgColor = bgColor;
+ }
+
+ public int getFontColor() {
+ return fontColor;
+ }
+
+ public int getBgColor() {
+ return bgColor;
+ }
+}
diff --git a/app/src/main/java/com/yzx/webebook/widget/page/PageView.java b/app/src/main/java/com/yzx/webebook/widget/page/PageView.java
new file mode 100644
index 0000000..248e585
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/widget/page/PageView.java
@@ -0,0 +1,378 @@
+package com.yzx.webebook.widget.page;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+import com.yzx.webebook.model.bean.CollBookBean;
+import com.yzx.webebook.widget.animation.CoverPageAnim;
+import com.yzx.webebook.widget.animation.HorizonPageAnim;
+import com.yzx.webebook.widget.animation.NonePageAnim;
+import com.yzx.webebook.widget.animation.PageAnimation;
+import com.yzx.webebook.widget.animation.ScrollPageAnim;
+import com.yzx.webebook.widget.animation.SimulationPageAnim;
+import com.yzx.webebook.widget.animation.SlidePageAnim;
+
+
+/**
+ * Created by Administrator on 2016/8/29 0029.
+ * 原作者的GitHub Project Path:(https://github.com/PeachBlossom/treader)
+ * 绘制页面显示内容的类
+ */
+public class PageView extends View {
+
+ private final static String TAG = "BookPageWidget";
+
+ private int mViewWidth = 0; // 当前View的宽
+ private int mViewHeight = 0; // 当前View的高
+
+ private int mStartX = 0;
+ private int mStartY = 0;
+ private boolean isMove = false;
+ // 初始化参数
+ private int mBgColor = 0xFFCEC29C;
+ private PageMode mPageMode = PageMode.SIMULATION;
+ // 是否允许点击
+ private boolean canTouch = true;
+ // 唤醒菜单的区域
+ private RectF mCenterRect = null;
+ private boolean isPrepare;
+ // 动画类
+ private PageAnimation mPageAnim;
+ // 动画监听类
+ private PageAnimation.OnPageChangeListener mPageAnimListener = new PageAnimation.OnPageChangeListener() {
+ @Override
+ public boolean hasPrev() {
+ return PageView.this.hasPrevPage();
+ }
+
+ @Override
+ public boolean hasNext() {
+ return PageView.this.hasNextPage();
+ }
+
+ @Override
+ public void pageCancel() {
+ PageView.this.pageCancel();
+ }
+ };
+
+ //点击监听
+ private TouchListener mTouchListener;
+ //内容加载器
+ private PageLoader mPageLoader;
+
+ public PageView(Context context) {
+ this(context, null);
+ }
+
+ public PageView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public PageView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ mViewWidth = w;
+ mViewHeight = h;
+
+ isPrepare = true;
+
+ if (mPageLoader != null) {
+ mPageLoader.prepareDisplay(w, h);
+ }
+ }
+
+ //设置翻页的模式
+ void setPageMode(PageMode pageMode) {
+ mPageMode = pageMode;
+ //视图未初始化的时候,禁止调用
+ if (mViewWidth == 0 || mViewHeight == 0) return;
+
+ switch (mPageMode) {
+ case SIMULATION:
+ mPageAnim = new SimulationPageAnim(mViewWidth, mViewHeight, this, mPageAnimListener);
+ break;
+ case COVER:
+ mPageAnim = new CoverPageAnim(mViewWidth, mViewHeight, this, mPageAnimListener);
+ break;
+ case SLIDE:
+ mPageAnim = new SlidePageAnim(mViewWidth, mViewHeight, this, mPageAnimListener);
+ break;
+ case NONE:
+ mPageAnim = new NonePageAnim(mViewWidth, mViewHeight, this, mPageAnimListener);
+ break;
+ case SCROLL:
+ mPageAnim = new ScrollPageAnim(mViewWidth, mViewHeight, 0,
+ mPageLoader.getMarginHeight(), this, mPageAnimListener);
+ break;
+ default:
+ mPageAnim = new SimulationPageAnim(mViewWidth, mViewHeight, this, mPageAnimListener);
+ }
+ }
+
+ public Bitmap getNextBitmap() {
+ if (mPageAnim == null) return null;
+ return mPageAnim.getNextBitmap();
+ }
+
+ public Bitmap getBgBitmap() {
+ if (mPageAnim == null) return null;
+ return mPageAnim.getBgBitmap();
+ }
+
+ public boolean autoPrevPage() {
+ //滚动暂时不支持自动翻页
+ if (mPageAnim instanceof ScrollPageAnim) {
+ return false;
+ } else {
+ startPageAnim(PageAnimation.Direction.PRE);
+ return true;
+ }
+ }
+
+ public boolean autoNextPage() {
+ if (mPageAnim instanceof ScrollPageAnim) {
+ return false;
+ } else {
+ startPageAnim(PageAnimation.Direction.NEXT);
+ return true;
+ }
+ }
+
+ private void startPageAnim(PageAnimation.Direction direction) {
+ if (mTouchListener == null) return;
+ //是否正在执行动画
+ abortAnimation();
+ if (direction == PageAnimation.Direction.NEXT) {
+ int x = mViewWidth;
+ int y = mViewHeight;
+ //初始化动画
+ mPageAnim.setStartPoint(x, y);
+ //设置点击点
+ mPageAnim.setTouchPoint(x, y);
+ //设置方向
+ Boolean hasNext = hasNextPage();
+
+ mPageAnim.setDirection(direction);
+ if (!hasNext) {
+ return;
+ }
+ } else {
+ int x = 0;
+ int y = mViewHeight;
+ //初始化动画
+ mPageAnim.setStartPoint(x, y);
+ //设置点击点
+ mPageAnim.setTouchPoint(x, y);
+ mPageAnim.setDirection(direction);
+ //设置方向方向
+ Boolean hashPrev = hasPrevPage();
+ if (!hashPrev) {
+ return;
+ }
+ }
+ mPageAnim.startAnim();
+ this.postInvalidate();
+ }
+
+ public void setBgColor(int color) {
+ mBgColor = color;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+
+ //绘制背景
+ canvas.drawColor(mBgColor);
+
+ //绘制动画
+ mPageAnim.draw(canvas);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ super.onTouchEvent(event);
+
+ if (!canTouch && event.getAction() != MotionEvent.ACTION_DOWN) return true;
+
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mStartX = x;
+ mStartY = y;
+ isMove = false;
+ canTouch = mTouchListener.onTouch();
+ mPageAnim.onTouchEvent(event);
+ break;
+ case MotionEvent.ACTION_MOVE:
+ // 判断是否大于最小滑动值。
+ int slop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
+ if (!isMove) {
+ isMove = Math.abs(mStartX - event.getX()) > slop || Math.abs(mStartY - event.getY()) > slop;
+ }
+
+ // 如果滑动了,则进行翻页。
+ if (isMove) {
+ mPageAnim.onTouchEvent(event);
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ if (!isMove) {
+ //设置中间区域范围
+ if (mCenterRect == null) {
+ mCenterRect = new RectF(mViewWidth / 5, mViewHeight / 3,
+ mViewWidth * 4 / 5, mViewHeight * 2 / 3);
+ }
+
+ //是否点击了中间
+ if (mCenterRect.contains(x, y)) {
+ if (mTouchListener != null) {
+ mTouchListener.center();
+ }
+ return true;
+ }
+ }
+ mPageAnim.onTouchEvent(event);
+ break;
+ }
+ return true;
+ }
+
+ /**
+ * 判断是否存在上一页
+ *
+ * @return
+ */
+ private boolean hasPrevPage() {
+ mTouchListener.prePage();
+ return mPageLoader.prev();
+ }
+
+ /**
+ * 判断是否下一页存在
+ *
+ * @return
+ */
+ private boolean hasNextPage() {
+ mTouchListener.nextPage();
+ return mPageLoader.next();
+ }
+
+ private void pageCancel() {
+ mTouchListener.cancel();
+ mPageLoader.pageCancel();
+ }
+
+ @Override
+ public void computeScroll() {
+ //进行滑动
+ mPageAnim.scrollAnim();
+ super.computeScroll();
+ }
+
+ //如果滑动状态没有停止就取消状态,重新设置Anim的触碰点
+ public void abortAnimation() {
+ mPageAnim.abortAnim();
+ }
+
+ public boolean isRunning() {
+ if (mPageAnim == null) {
+ return false;
+ }
+ return mPageAnim.isRunning();
+ }
+
+ public boolean isPrepare() {
+ return isPrepare;
+ }
+
+ public void setTouchListener(TouchListener mTouchListener) {
+ this.mTouchListener = mTouchListener;
+ }
+
+ public void drawNextPage() {
+ if (!isPrepare) return;
+
+ if (mPageAnim instanceof HorizonPageAnim) {
+ ((HorizonPageAnim) mPageAnim).changePage();
+ }
+ mPageLoader.drawPage(getNextBitmap(), false);
+ }
+
+ /**
+ * 绘制当前页。
+ *
+ * @param isUpdate
+ */
+ public void drawCurPage(boolean isUpdate) {
+ if (!isPrepare) return;
+
+ if (!isUpdate){
+ if (mPageAnim instanceof ScrollPageAnim) {
+ ((ScrollPageAnim) mPageAnim).resetBitmap();
+ }
+ }
+
+ mPageLoader.drawPage(getNextBitmap(), isUpdate);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mPageAnim.abortAnim();
+ mPageAnim.clear();
+
+ mPageLoader = null;
+ mPageAnim = null;
+ }
+
+ /**
+ * 获取 PageLoader
+ *
+ * @param collBook
+ * @return
+ */
+ public PageLoader getPageLoader(CollBookBean collBook) {
+ // 判是否已经存在
+ if (mPageLoader != null) {
+ return mPageLoader;
+ }
+ // 根据书籍类型,获取具体的加载器
+ if (collBook.isLocal()) {
+ mPageLoader = new LocalPageLoader(this, collBook);
+ } else {
+ mPageLoader = new NetPageLoader(this, collBook);
+ }
+ // 判断是否 PageView 已经初始化完成
+ if (mViewWidth != 0 || mViewHeight != 0) {
+ // 初始化 PageLoader 的屏幕大小
+ mPageLoader.prepareDisplay(mViewWidth, mViewHeight);
+ }
+ Log.e(TAG, "getPageLoader: "+mPageLoader.toString() );
+ return mPageLoader;
+ }
+
+ public interface TouchListener {
+ boolean onTouch();
+
+ void center();
+
+ void prePage();
+
+ void nextPage();
+
+ void cancel();
+ }
+}
diff --git a/app/src/main/java/com/yzx/webebook/widget/page/TxtChapter.java b/app/src/main/java/com/yzx/webebook/widget/page/TxtChapter.java
new file mode 100644
index 0000000..8f0feab
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/widget/page/TxtChapter.java
@@ -0,0 +1,70 @@
+package com.yzx.webebook.widget.page;
+
+/**
+ * Created by newbiechen on 17-7-1.
+ */
+
+public class TxtChapter{
+
+ //章节所属的小说(网络)
+ String bookId;
+ //章节的链接(网络)
+ String link;
+
+ //章节名(共用)
+ String title;
+
+ //章节内容在文章中的起始位置(本地)
+ long start;
+ //章节内容在文章中的终止位置(本地)
+ long end;
+
+ public String getBookId() {
+ return bookId;
+ }
+
+ public void setBookId(String id) {
+ this.bookId = id;
+ }
+
+ public String getLink() {
+ return link;
+ }
+
+ public void setLink(String link) {
+ this.link = link;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public long getStart() {
+ return start;
+ }
+
+ public void setStart(long start) {
+ this.start = start;
+ }
+
+ public long getEnd() {
+ return end;
+ }
+
+ public void setEnd(long end) {
+ this.end = end;
+ }
+
+ @Override
+ public String toString() {
+ return "TxtChapter{" +
+ "title='" + title + '\'' +
+ ", start=" + start +
+ ", end=" + end +
+ '}';
+ }
+}
diff --git a/app/src/main/java/com/yzx/webebook/widget/page/TxtPage.java b/app/src/main/java/com/yzx/webebook/widget/page/TxtPage.java
new file mode 100644
index 0000000..dbcc9a3
--- /dev/null
+++ b/app/src/main/java/com/yzx/webebook/widget/page/TxtPage.java
@@ -0,0 +1,14 @@
+package com.yzx.webebook.widget.page;
+
+import java.util.List;
+
+/**
+ * Created by newbiechen on 17-7-1.
+ */
+
+public class TxtPage {
+ int position;
+ String title;
+ int titleLines; //当前 lines 中为 title 的行数。
+ List lines;
+}
diff --git a/app/src/main/res/anim/rotate_0_to_180.xml b/app/src/main/res/anim/rotate_0_to_180.xml
new file mode 100644
index 0000000..cc143e4
--- /dev/null
+++ b/app/src/main/res/anim/rotate_0_to_180.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/rotate_180_to_360.xml b/app/src/main/res/anim/rotate_180_to_360.xml
new file mode 100644
index 0000000..1e2668b
--- /dev/null
+++ b/app/src/main/res/anim/rotate_180_to_360.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/slide_bottom_in.xml b/app/src/main/res/anim/slide_bottom_in.xml
new file mode 100644
index 0000000..1dd6c09
--- /dev/null
+++ b/app/src/main/res/anim/slide_bottom_in.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/slide_bottom_out.xml b/app/src/main/res/anim/slide_bottom_out.xml
new file mode 100644
index 0000000..d333fb7
--- /dev/null
+++ b/app/src/main/res/anim/slide_bottom_out.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/slide_left_in.xml b/app/src/main/res/anim/slide_left_in.xml
new file mode 100644
index 0000000..842a616
--- /dev/null
+++ b/app/src/main/res/anim/slide_left_in.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/slide_left_out.xml b/app/src/main/res/anim/slide_left_out.xml
new file mode 100644
index 0000000..d9c3bd9
--- /dev/null
+++ b/app/src/main/res/anim/slide_left_out.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/slide_right_in.xml b/app/src/main/res/anim/slide_right_in.xml
new file mode 100644
index 0000000..c3ccec1
--- /dev/null
+++ b/app/src/main/res/anim/slide_right_in.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/slide_right_out.xml b/app/src/main/res/anim/slide_right_out.xml
new file mode 100644
index 0000000..77ee771
--- /dev/null
+++ b/app/src/main/res/anim/slide_right_out.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/slide_top_in.xml b/app/src/main/res/anim/slide_top_in.xml
new file mode 100644
index 0000000..177e700
--- /dev/null
+++ b/app/src/main/res/anim/slide_top_in.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/slide_top_out.xml b/app/src/main/res/anim/slide_top_out.xml
new file mode 100644
index 0000000..7b51c98
--- /dev/null
+++ b/app/src/main/res/anim/slide_top_out.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/color/selector_btn_read_setting.xml b/app/src/main/res/color/selector_btn_read_setting.xml
new file mode 100644
index 0000000..385291d
--- /dev/null
+++ b/app/src/main/res/color/selector_btn_read_setting.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/color/selector_chapter.xml b/app/src/main/res/color/selector_chapter.xml
new file mode 100644
index 0000000..b0ef349
--- /dev/null
+++ b/app/src/main/res/color/selector_chapter.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/seekbar_bg.xml b/app/src/main/res/drawable/seekbar_bg.xml
new file mode 100644
index 0000000..fbbe04f
--- /dev/null
+++ b/app/src/main/res/drawable/seekbar_bg.xml
@@ -0,0 +1,25 @@
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/seekbar_thumb.xml b/app/src/main/res/drawable/seekbar_thumb.xml
new file mode 100644
index 0000000..af1fe8b
--- /dev/null
+++ b/app/src/main/res/drawable/seekbar_thumb.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/selector_btn_read_setting.xml b/app/src/main/res/drawable/selector_btn_read_setting.xml
new file mode 100644
index 0000000..27396b6
--- /dev/null
+++ b/app/src/main/res/drawable/selector_btn_read_setting.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/selector_category_load.xml b/app/src/main/res/drawable/selector_category_load.xml
new file mode 100644
index 0000000..b86093c
--- /dev/null
+++ b/app/src/main/res/drawable/selector_category_load.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/selector_category_unload.xml b/app/src/main/res/drawable/selector_category_unload.xml
new file mode 100644
index 0000000..bf8af91
--- /dev/null
+++ b/app/src/main/res/drawable/selector_category_unload.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/shape_btn_read_setting_checked.xml b/app/src/main/res/drawable/shape_btn_read_setting_checked.xml
new file mode 100644
index 0000000..f67604f
--- /dev/null
+++ b/app/src/main/res/drawable/shape_btn_read_setting_checked.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/shape_btn_read_setting_normal.xml b/app/src/main/res/drawable/shape_btn_read_setting_normal.xml
new file mode 100644
index 0000000..0703ec3
--- /dev/null
+++ b/app/src/main/res/drawable/shape_btn_read_setting_normal.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_base_weex.xml b/app/src/main/res/layout/activity_base_weex.xml
new file mode 100644
index 0000000..ae71c41
--- /dev/null
+++ b/app/src/main/res/layout/activity_base_weex.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index fe224fc..82949f8 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -127,7 +127,31 @@
android:id="@+id/btn7"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="电子书页面(测试)"
+ android:text="Read"
+ android:visibility="visible"
+ android:layout_marginTop="@dimen/d_20"/>
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_read.xml b/app/src/main/res/layout/activity_read.xml
new file mode 100644
index 0000000..faf6c1e
--- /dev/null
+++ b/app/src/main/res/layout/activity_read.xml
@@ -0,0 +1,194 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_read_setting.xml b/app/src/main/res/layout/dialog_read_setting.xml
new file mode 100644
index 0000000..31b2cb8
--- /dev/null
+++ b/app/src/main/res/layout/dialog_read_setting.xml
@@ -0,0 +1,216 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_category.xml b/app/src/main/res/layout/item_category.xml
new file mode 100644
index 0000000..927c075
--- /dev/null
+++ b/app/src/main/res/layout/item_category.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_read_bg.xml b/app/src/main/res/layout/item_read_bg.xml
new file mode 100644
index 0000000..b6d485a
--- /dev/null
+++ b/app/src/main/res/layout/item_read_bg.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-xhdpi/ic_brightness_minus.png b/app/src/main/res/mipmap-xhdpi/ic_brightness_minus.png
new file mode 100644
index 0000000..9ec9e92
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_brightness_minus.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_brightness_plus.png b/app/src/main/res/mipmap-xhdpi/ic_brightness_plus.png
new file mode 100644
index 0000000..efff575
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_brightness_plus.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_checked.png b/app/src/main/res/mipmap-xhdpi/ic_checked.png
new file mode 100644
index 0000000..881a033
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_checked.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_home_classwork.png b/app/src/main/res/mipmap-xhdpi/ic_home_classwork.png
new file mode 100644
index 0000000..dd45d6d
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_home_classwork.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_item_category_activated.png b/app/src/main/res/mipmap-xhdpi/ic_item_category_activated.png
new file mode 100644
index 0000000..8712b28
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_item_category_activated.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_item_category_download.png b/app/src/main/res/mipmap-xhdpi/ic_item_category_download.png
new file mode 100644
index 0000000..bb61a3c
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_item_category_download.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_item_category_normal.png b/app/src/main/res/mipmap-xhdpi/ic_item_category_normal.png
new file mode 100644
index 0000000..b4e9b49
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_item_category_normal.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_read_menu_category.png b/app/src/main/res/mipmap-xhdpi/ic_read_menu_category.png
new file mode 100644
index 0000000..149b65d
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_read_menu_category.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_read_menu_download.png b/app/src/main/res/mipmap-xhdpi/ic_read_menu_download.png
new file mode 100644
index 0000000..8da9972
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_read_menu_download.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_read_menu_font.png b/app/src/main/res/mipmap-xhdpi/ic_read_menu_font.png
new file mode 100644
index 0000000..ad0f11b
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_read_menu_font.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_read_menu_morning.png b/app/src/main/res/mipmap-xhdpi/ic_read_menu_morning.png
new file mode 100644
index 0000000..b76869d
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_read_menu_morning.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_read_menu_night.png b/app/src/main/res/mipmap-xhdpi/ic_read_menu_night.png
new file mode 100644
index 0000000..3c1133a
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_read_menu_night.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/seekbar_thumb_normal.png b/app/src/main/res/mipmap-xhdpi/seekbar_thumb_normal.png
new file mode 100644
index 0000000..d043cb9
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/seekbar_thumb_normal.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/seekbar_thumb_selected.png b/app/src/main/res/mipmap-xhdpi/seekbar_thumb_selected.png
new file mode 100644
index 0000000..16106bb
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/seekbar_thumb_selected.png differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 083b37f..8c675b3 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -22,4 +22,82 @@
#4d92dc
#ff8635
@android:color/transparent
+ @color/white
+
+
+
+
+ #99343434
+ #fb9090
+ #EEEEEE
+ #A58D5E
+ #EC4A48
+ #039BE5
+ #23000000
+ #EEEEEE
+
+
+ #727272
+ @color/light_white
+
+ #E3E3E3
+
+ @color/light_white
+
+
+
+
+ @color/black
+
+ #212121
+
+ #727272
+
+ #B2B2B2
+
+ @color/light_coffee
+ #96ffffff
+
+
+ @color/white
+ #E3E3E3
+
+
+ #B0E2FF
+ #B2DFEE
+ #EEA9B8
+ #EEA2AD
+
+
+ @color/light_pink
+
+ #88303F9F
+
+ #8bd715
+ #f3aa41
+ #afafad
+ #e33a35
+ #8bd715
+
+
+ #CEC29C
+ #CCEBCC
+ #AAAAAA
+ #D1CEC5
+ #001C27
+ #DDCEC29C
+
+
+ #2C2C2C
+ #2F332D
+ #92918C
+ #383429
+ #627176
+
+ #000000
+ #99ffffff
+
+ #191919
+ #CBCBCB
+ #88000000
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 747e629..e3ebf39 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -39,4 +39,50 @@
28dp
29dp
30dp
+
+
+
+ 16dp
+ 16dp
+
+
+
+
+ 4dp
+ 8dp
+ 16dp
+ 24dp
+
+ 4dp
+ 8dp
+ 16dp
+ 24dp
+
+
+ 0.5dp
+ '
+
+
+ 18sp
+
+ 16sp
+
+ 12sp
+
+
+
+ 45dp
+ 60dp
+
+
+ 16sp
+ 12dp
+
+
+ 20dp
+ 35dp
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index d03d865..fc1fcab 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -16,4 +16,117 @@
- true
- @null
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/styles_text.xml b/app/src/main/res/values/styles_text.xml
new file mode 100644
index 0000000..b1de9e8
--- /dev/null
+++ b/app/src/main/res/values/styles_text.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/styles_widget.xml b/app/src/main/res/values/styles_widget.xml
new file mode 100644
index 0000000..7bae60d
--- /dev/null
+++ b/app/src/main/res/values/styles_widget.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml
new file mode 100644
index 0000000..dca93c0
--- /dev/null
+++ b/app/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 40b45e8..76f9868 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,6 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
+ apply from: 'versions.gradle'
ext.kotlin_version = '1.3.71'
ext.anko_version='0.10.8'
repositories {
@@ -12,6 +13,7 @@ buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:3.6.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ classpath deps.greendao.plugin
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
diff --git a/versions.gradle b/versions.gradle
new file mode 100644
index 0000000..c97c184
--- /dev/null
+++ b/versions.gradle
@@ -0,0 +1,84 @@
+ext.deps = [:]
+
+// 版本号
+def versions = [:]
+versions.gradle = '3.6.3'
+versions.android = '28.0.0'
+versions.glide = '3.7.0'
+versions.rxjava2 = '2.2.19'
+versions.rxandroid = "2.1.1"
+versions.okhttp = '3.6.0'
+versions.retrofit = '2.7.2'
+versions.stetho = '1.5.1'
+versions.greendao = '3.2.2'
+versions.butterknife = '8.5.1'
+versions.junit = '4.13'
+versions.leakcanary = '1.5.4'
+
+// 依赖
+def deps = [:]
+
+// support
+def android =[:]
+android.plugin = "com.android.tools.build:gradle:$versions.gradle"
+android.appcompat = "com.android.support:appcompat-v7:$versions.android"
+android.design = "com.android.support:design:$versions.android"
+android.support = "com.android.support:support-v4:$versions.android"
+android.cardview = "com.android.support:cardview-v7:$versions.android"
+deps.android = android
+
+// butterknife
+def butterknife = [:]
+butterknife.runtime = "com.jakewharton:butterknife:$versions.butterknife"
+butterknife.compiler = "com.jakewharton:butterknife-compiler:$versions.butterknife"
+deps.butterknife = butterknife
+
+// rxjava
+def reactivex = [:]
+reactivex.rxandroid = "io.reactivex.rxjava2:rxandroid:$versions.rxandroid"
+reactivex.rxjava2 = "io.reactivex.rxjava2:rxjava:$versions.rxjava2"
+deps.reactivex = reactivex
+
+// okhttp
+def okhttp = [:]
+okhttp.runtime = "com.squareup.okhttp3:okhttp:$versions.okhttp"
+okhttp.logging = "com.squareup.okhttp3:logging-interceptor:$versions.okhttp"
+deps.okhttp = okhttp
+
+// retrofit
+def retrofit = [:]
+retrofit.runtime = "com.squareup.retrofit2:retrofit:$versions.retrofit"
+retrofit.gson = "com.squareup.retrofit2:converter-gson:$versions.retrofit"
+retrofit.rxjava2 = "com.squareup.retrofit2:adapter-rxjava2:$versions.retrofit"
+deps.retrofit = retrofit
+
+// 图片加载库
+def glide = [:]
+glide.runtime = "com.github.bumptech.glide:glide:$versions.glide"
+glide.compiler = "com.github.bumptech.glide:compiler:$versions.glide"
+deps.glide = glide
+
+def greendao = [:]
+greendao.runtime = "org.greenrobot:greendao:$versions.greendao"
+greendao.plugin = "org.greenrobot:greendao-gradle-plugin:$versions.greendao"
+deps.greendao = greendao
+
+def testing = [:]
+testing.junit = "junit:junit:$versions.junit"
+deps.testing = testing
+
+// 测试相关
+def leakcanary = [:]
+leakcanary.release = "com.squareup.leakcanary:leakcanary-android-no-op:$versions.leakcanary"
+leakcanary.debug = "com.squareup.leakcanary:leakcanary-android:$versions.leakcanary"
+deps.leakcanary = leakcanary
+
+ext.deps = deps
+
+def build_versions = [:]
+build_versions.min_sdk = 21
+build_versions.target_sdk = 25
+build_versions.compileSdkVersion = 30
+build_versions.tools_version = '30.0.3'
+
+ext.build_versions = build_versions
\ No newline at end of file