diff --git a/.idea/assetWizardSettings.xml b/.idea/assetWizardSettings.xml
index 42bd19f..a462dcb 100644
--- a/.idea/assetWizardSettings.xml
+++ b/.idea/assetWizardSettings.xml
@@ -19,8 +19,8 @@
diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser
index a93c5b7..3100652 100644
Binary files a/.idea/caches/build_file_checksums.ser and b/.idea/caches/build_file_checksums.ser differ
diff --git a/README.md b/README.md
index 0002321..15a73fc 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
风月读书,一款开源、无广告的小说阅读软件。
-成品下载(v1.7.8):[https://fycz.lanzous.com/ieSvwk7vucf](https://fycz.lanzous.com/ieSvwk7vucf)
+成品下载(v1.7.9):[https://fycz.lanzous.com/icrU3kkqrud](https://fycz.lanzous.com/icrU3kkqrud)
#### 一、关于书源
diff --git a/app/build.gradle b/app/build.gradle
index 1261006..52a58f4 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -200,7 +200,8 @@ dependencies {
implementation "com.afollestad.material-dialogs:input:$dialog_version"
implementation "com.afollestad.material-dialogs:files:$dialog_version"
implementation "com.afollestad.material-dialogs:bottomsheets:$dialog_version"*/
-
+ //左滑菜单
+ implementation 'com.github.mcxtzhang:SwipeDelMenuLayout:V1.3.0'
}
greendao {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 80471be..b5d10dc 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -101,6 +101,7 @@
+
diff --git a/app/src/main/assets/updatelog.fy b/app/src/main/assets/updatelog.fy
index f486693..1966ae5 100644
--- a/app/src/main/assets/updatelog.fy
+++ b/app/src/main/assets/updatelog.fy
@@ -1,6 +1,11 @@
-2021.01.10
+2021.01.19
风月读书v1.7.9
-1、修复私密书架相关bug
+1、新增阅读长按选择文字
+2、新增选择文字悬浮菜单(拷贝、替换、发声、搜索、分享)
+3、新增阅读内容替换(可用于过滤广告文字等)
+4、新增内容替换规则管理界面(规则列表左滑可禁用\启用、分享、删除单个规则,单击编辑规则,
+菜单栏:新建规则、导入规则、导入所有、反转规则可用性、删除禁用规则)
+5、修复私密书架相关bug
2021.01.09
风月读书v1.7.8
diff --git a/app/src/main/java/xyz/fycz/myreader/common/APPCONST.java b/app/src/main/java/xyz/fycz/myreader/common/APPCONST.java
index 73b7921..e11b55c 100644
--- a/app/src/main/java/xyz/fycz/myreader/common/APPCONST.java
+++ b/app/src/main/java/xyz/fycz/myreader/common/APPCONST.java
@@ -46,6 +46,7 @@ public class APPCONST {
public static final String RESULT_LAST_READ_POSITION = "result_last_read_position";
public static final String RESULT_HISTORY_CHAPTER = "result_history_chapter";
public static final String RESULT_UP_MENU = "result_up_meu";
+ public static final String RESULT_REPLACE_RULE = "result_up_meu";
public static final String[] READ_STYLE_NIGHT = {"#94928c", "#393431"};//黑夜
@@ -69,7 +70,7 @@ public class APPCONST {
public static final int REQUEST_SELECT_BG = 1005;
public static final int REQUEST_IMPORT_LAYOUT = 1006;
public static final int REQUEST_QR_SCAN = 1007;
-
+ public static final int REQUEST_IMPORT_REPLACE_RULE = 1008;
public static final int REQUEST_READ = 1;
diff --git a/app/src/main/java/xyz/fycz/myreader/common/URLCONST.java b/app/src/main/java/xyz/fycz/myreader/common/URLCONST.java
index 14f6320..97d3d41 100644
--- a/app/src/main/java/xyz/fycz/myreader/common/URLCONST.java
+++ b/app/src/main/java/xyz/fycz/myreader/common/URLCONST.java
@@ -35,6 +35,11 @@ public class URLCONST {
public static final String APP_WEB_URL = "http://fyreader.fycz.xyz:8080/FYReader/";
+ public static final String BAI_DU_SEARCH = "https://m.baidu.com/s?word={key}";
+
+ public static final String GOOGLE_SEARCH = "https://www.google.com/search?q={key}";
+
+ public static final String YOU_DAO_SEARCH = "http://m.youdao.com/dict?le=eng&q={key}";
}
diff --git a/app/src/main/java/xyz/fycz/myreader/entity/Setting.java b/app/src/main/java/xyz/fycz/myreader/entity/Setting.java
index 2d41c10..5234728 100644
--- a/app/src/main/java/xyz/fycz/myreader/entity/Setting.java
+++ b/app/src/main/java/xyz/fycz/myreader/entity/Setting.java
@@ -78,6 +78,10 @@ public class Setting implements Serializable {
private int sortStyle;//排序方式:0-手动排序,1-按时间排序,2-按照书名排序
+ private boolean canSelectText;//是否长按选择
+
+ private boolean lightNovelParagraph;//是否自动重分段落
+
private int sourceVersion;//书源版本号
private int settingVersion;//设置版本号
@@ -688,4 +692,20 @@ public class Setting implements Serializable {
public void setSortStyle(int sortStyle) {
this.sortStyle = sortStyle;
}
+
+ public boolean isCanSelectText() {
+ return canSelectText;
+ }
+
+ public void setCanSelectText(boolean canSelectText) {
+ this.canSelectText = canSelectText;
+ }
+
+ public boolean isLightNovelParagraph() {
+ return lightNovelParagraph;
+ }
+
+ public void setLightNovelParagraph(boolean lightNovelParagraph) {
+ this.lightNovelParagraph = lightNovelParagraph;
+ }
}
diff --git a/app/src/main/java/xyz/fycz/myreader/enums/BookSource.java b/app/src/main/java/xyz/fycz/myreader/enums/BookSource.java
index f591fed..10445b3 100644
--- a/app/src/main/java/xyz/fycz/myreader/enums/BookSource.java
+++ b/app/src/main/java/xyz/fycz/myreader/enums/BookSource.java
@@ -9,7 +9,7 @@ import xyz.fycz.myreader.util.ToastUtils;
*/
public enum BookSource {
-
+ local("本地书籍"),
fynovel("风月小说"),
tianlai(MyApplication.getApplication().getString(R.string.read_tianlai)),
biquge44(MyApplication.getApplication().getString(R.string.read_biquge44)),
@@ -37,9 +37,9 @@ public enum BookSource {
chaoxing(MyApplication.getApplication().getString(R.string.read_chaoxing)),
zuopin(MyApplication.getApplication().getString(R.string.read_zuopin)),
cangshu99(MyApplication.getApplication().getString(R.string.read_cangshu99)),
- ben100(MyApplication.getApplication().getString(R.string.read_ben100)),
+ ben100(MyApplication.getApplication().getString(R.string.read_ben100));
//liulangcat("流浪猫·实体"),
- local("本地书籍");
+
public String text;
BookSource(String text) {
diff --git a/app/src/main/java/xyz/fycz/myreader/greendao/entity/ReplaceRuleBean.java b/app/src/main/java/xyz/fycz/myreader/greendao/entity/ReplaceRuleBean.java
index 321a7d2..0710111 100644
--- a/app/src/main/java/xyz/fycz/myreader/greendao/entity/ReplaceRuleBean.java
+++ b/app/src/main/java/xyz/fycz/myreader/greendao/entity/ReplaceRuleBean.java
@@ -166,4 +166,5 @@ public class ReplaceRuleBean implements Parcelable {
public void setIsRegex(Boolean isRegex) {
this.isRegex = isRegex;
}
+
}
diff --git a/app/src/main/java/xyz/fycz/myreader/model/ReplaceRuleManager.java b/app/src/main/java/xyz/fycz/myreader/model/ReplaceRuleManager.java
index d9ef951..a2c6860 100644
--- a/app/src/main/java/xyz/fycz/myreader/model/ReplaceRuleManager.java
+++ b/app/src/main/java/xyz/fycz/myreader/model/ReplaceRuleManager.java
@@ -3,6 +3,8 @@ package xyz.fycz.myreader.model;
import android.text.TextUtils;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import io.reactivex.Observable;
@@ -34,7 +36,94 @@ public class ReplaceRuleManager {
}
return replaceRuleBeansEnabled;
}
+ // 合并广告话术规则
+ public static Single mergeAdRules(ReplaceRuleBean replaceRuleBean) {
+
+ String rule = formateAdRule(replaceRuleBean.getRegex());
+
+/* String summary=replaceRuleBean.getReplaceSummary();
+ if(summary==null)
+ summary="";
+ String sumary_pre=summary.split("-")[0];*/
+
+ int sn = replaceRuleBean.getSerialNumber();
+ if (sn == 0) {
+ sn = (int) (GreenDaoManager.getDaoSession().getReplaceRuleBeanDao().queryBuilder().count() + 1);
+ replaceRuleBean.setSerialNumber(sn);
+ }
+
+ List list = GreenDaoManager.getDaoSession()
+ .getReplaceRuleBeanDao().queryBuilder()
+ .where(ReplaceRuleBeanDao.Properties.Enable.eq(true))
+ .where(ReplaceRuleBeanDao.Properties.ReplaceSummary.eq(replaceRuleBean.getReplaceSummary()))
+ .where(ReplaceRuleBeanDao.Properties.SerialNumber.notEq(sn))
+ .orderAsc(ReplaceRuleBeanDao.Properties.SerialNumber)
+ .list();
+ if (list.size() < 1) {
+ replaceRuleBean.setRegex(rule);
+ return saveData(replaceRuleBean);
+ } else {
+ StringBuffer buffer = new StringBuffer(rule);
+ for (ReplaceRuleBean li : list) {
+ buffer.append('\n');
+ buffer.append(li.getRegex());
+// buffer.append(formateAdRule(rule.getRegex()));
+ }
+ replaceRuleBean.setRegex(formateAdRule(buffer.toString()));
+
+ return Single.create((SingleOnSubscribe) emitter -> {
+
+ GreenDaoManager.getDaoSession().getReplaceRuleBeanDao().insertOrReplace(replaceRuleBean);
+ for (ReplaceRuleBean li : list) {
+ GreenDaoManager.getDaoSession().getReplaceRuleBeanDao().delete(li);
+ }
+ refreshDataS();
+ emitter.onSuccess(true);
+ }).compose(RxUtils::toSimpleSingle);
+
+ }
+ }
+
+ // 把输入的规则进行预处理(分段、排序、去重)。保存的是普通多行文本。
+ public static String formateAdRule(String rule) {
+
+ if (rule == null)
+ return "";
+ String result = rule.trim();
+ if (result.length() < 1)
+ return "";
+
+ String string = rule
+// 用中文中的.视为。进行分段
+ .replaceAll("(?<=([^a-zA-Z\\p{P}]{4,8}))\\.+(?![^a-zA-Z\\p{P}]{4,8})","\n")
+// 用常见的适合分段的标点进行分段,句首句尾除外
+// .replaceAll("([^\\p{P}\n^])([…,,::?。!?!~<>《》【】()()]+)([^\\p{P}\n$])", "$1\n$3")
+// 表达式无法解决句尾连续多个符号的问题
+// .replaceAll("[…,,::?。!?!~<>《》【】()()]+(?!\\s*\n|$)", "\n")
+ .replaceAll("(?《》【】()()]+)(?![\\p{P}\n$])", "\n")
+
+ ;
+
+ String[] lines = string.split("\n");
+ List list = new ArrayList<>();
+
+ for (String s : lines) {
+ s = s.trim()
+// .replaceAll("\\s+", "\\s")
+ ;
+ if (!list.contains(s)) {
+ list.add(s);
+ }
+ }
+ Collections.sort(list);
+ StringBuffer buffer = new StringBuffer(rule.length() + 1);
+ for (int i = 0; i < list.size(); i++) {
+ buffer.append('\n');
+ buffer.append(list.get(i));
+ }
+ return buffer.toString().trim();
+ }
public static Single> getAll() {
return Single.create((SingleOnSubscribe>) emitter -> emitter.onSuccess(GreenDaoManager.getDaoSession()
.getReplaceRuleBeanDao().queryBuilder()
@@ -42,6 +131,13 @@ public class ReplaceRuleManager {
.list())).compose(RxUtils::toSimpleSingle);
}
+ public static List getAllRules() {
+ return GreenDaoManager.getDaoSession()
+ .getReplaceRuleBeanDao().queryBuilder()
+ .orderAsc(ReplaceRuleBeanDao.Properties.SerialNumber)
+ .list();
+ }
+
public static Single saveData(ReplaceRuleBean replaceRuleBean) {
return Single.create((SingleOnSubscribe) emitter -> {
if (replaceRuleBean.getSerialNumber() == 0) {
@@ -111,4 +207,5 @@ public class ReplaceRuleManager {
e.onComplete();
});
}
+
}
diff --git a/app/src/main/java/xyz/fycz/myreader/ui/activity/MoreSettingActivity.java b/app/src/main/java/xyz/fycz/myreader/ui/activity/MoreSettingActivity.java
index b696d9a..e4c4ffa 100644
--- a/app/src/main/java/xyz/fycz/myreader/ui/activity/MoreSettingActivity.java
+++ b/app/src/main/java/xyz/fycz/myreader/ui/activity/MoreSettingActivity.java
@@ -71,6 +71,12 @@ public class MoreSettingActivity extends BaseActivity {
RelativeLayout mRlShowStatus;
@BindView(R.id.sc_show_status)
SwitchCompat mScShowStatus;
+ @BindView(R.id.rl_long_press)
+ RelativeLayout mRlLongPress;
+ @BindView(R.id.sc_long_press)
+ SwitchCompat mScLongPress;
+ @BindView(R.id.rl_content_replace)
+ RelativeLayout mRlContentReplace;
@BindView(R.id.rl_read_aloud_volume_turn_page)
RelativeLayout mRlReadAloudVolumeTurnPage;
@BindView(R.id.sc_read_aloud_volume_turn_page)
@@ -136,6 +142,7 @@ public class MoreSettingActivity extends BaseActivity {
private float matchChapterSuitability;
private int catheCap;
private boolean isShowStatusBar;
+ private boolean isLongPress;
private boolean alwaysNext;
private boolean noMenuTitle;
private boolean readAloudVolumeTurnPage;
@@ -176,6 +183,7 @@ public class MoreSettingActivity extends BaseActivity {
sortStyle = mSetting.getSortStyle();
autoRefresh = mSetting.isRefreshWhenStart();
isShowStatusBar = mSetting.isShowStatusBar();
+ isLongPress = mSetting.isCanSelectText();
noMenuTitle = mSetting.isNoMenuChTitle();
readAloudVolumeTurnPage = mSetting.isReadAloudVolumeTurnPage();
threadNum = SharedPreUtils.getInstance().getInt(getString(R.string.threadNum), 8);
@@ -251,6 +259,7 @@ public class MoreSettingActivity extends BaseActivity {
mScMatchChapter.setChecked(isMatchChapter);
mScAutoRefresh.setChecked(autoRefresh);
mScShowStatus.setChecked(isShowStatusBar);
+ mScLongPress.setChecked(isLongPress);
mScNoMenuTitle.setChecked(noMenuTitle);
mScReadAloudVolumeTurnPage.setChecked(readAloudVolumeTurnPage);
}
@@ -309,6 +318,20 @@ public class MoreSettingActivity extends BaseActivity {
SysManager.saveSetting(mSetting);
}
);
+ mRlLongPress.setOnClickListener(
+ (v) -> {
+ needRefresh = false;
+ if (isLongPress) {
+ isLongPress = false;
+ } else {
+ isLongPress = true;
+ }
+ mScLongPress.setChecked(isLongPress);
+ mSetting.setCanSelectText(isLongPress);
+ SysManager.saveSetting(mSetting);
+ }
+ );
+ mRlContentReplace.setOnClickListener(v -> startActivity(new Intent(this, RuleActivity.class)));
mRlReadAloudVolumeTurnPage.setOnClickListener(
(v) -> {
if (readAloudVolumeTurnPage) {
diff --git a/app/src/main/java/xyz/fycz/myreader/ui/activity/ReadActivity.java b/app/src/main/java/xyz/fycz/myreader/ui/activity/ReadActivity.java
index 3868204..7addb3e 100644
--- a/app/src/main/java/xyz/fycz/myreader/ui/activity/ReadActivity.java
+++ b/app/src/main/java/xyz/fycz/myreader/ui/activity/ReadActivity.java
@@ -2,23 +2,36 @@ package xyz.fycz.myreader.ui.activity;
import android.annotation.SuppressLint;
import android.app.Notification;
-import android.content.*;
+import android.content.BroadcastReceiver;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.graphics.BitmapFactory;
import android.graphics.Color;
-import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
-import android.os.PersistableBundle;
+import android.speech.tts.TextToSpeech;
import android.util.Log;
-import android.view.*;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.FrameLayout;
+import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.TextView;
@@ -28,26 +41,32 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
-import butterknife.BindView;
-import butterknife.OnClick;
-
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.gyf.immersionbar.ImmersionBar;
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import butterknife.BindView;
+import butterknife.OnClick;
import xyz.fycz.myreader.ActivityManage;
import xyz.fycz.myreader.R;
import xyz.fycz.myreader.application.MyApplication;
import xyz.fycz.myreader.application.SysManager;
import xyz.fycz.myreader.base.BaseActivity;
import xyz.fycz.myreader.common.APPCONST;
+import xyz.fycz.myreader.common.URLCONST;
import xyz.fycz.myreader.entity.Setting;
import xyz.fycz.myreader.enums.BookSource;
import xyz.fycz.myreader.enums.Font;
import xyz.fycz.myreader.greendao.entity.Book;
import xyz.fycz.myreader.greendao.entity.BookMark;
import xyz.fycz.myreader.greendao.entity.Chapter;
+import xyz.fycz.myreader.greendao.entity.ReplaceRuleBean;
import xyz.fycz.myreader.greendao.service.BookGroupService;
import xyz.fycz.myreader.greendao.service.BookMarkService;
import xyz.fycz.myreader.greendao.service.BookService;
@@ -58,34 +77,37 @@ import xyz.fycz.myreader.ui.dialog.AudioPlayerDialog;
import xyz.fycz.myreader.ui.dialog.CopyContentDialog;
import xyz.fycz.myreader.ui.dialog.DialogCreator;
import xyz.fycz.myreader.ui.dialog.MyAlertDialog;
+import xyz.fycz.myreader.ui.dialog.ReplaceDialog;
+import xyz.fycz.myreader.ui.dialog.SourceExchangeDialog;
import xyz.fycz.myreader.ui.popmenu.AutoPageMenu;
import xyz.fycz.myreader.ui.popmenu.BrightnessEyeMenu;
import xyz.fycz.myreader.ui.popmenu.CustomizeComMenu;
import xyz.fycz.myreader.ui.popmenu.CustomizeLayoutMenu;
import xyz.fycz.myreader.ui.popmenu.ReadSettingMenu;
-import xyz.fycz.myreader.ui.dialog.SourceExchangeDialog;
-import xyz.fycz.myreader.ui.presenter.CatalogPresenter;
-import xyz.fycz.myreader.util.*;
-import xyz.fycz.myreader.util.llog.LLog;
+import xyz.fycz.myreader.util.BrightUtil;
+import xyz.fycz.myreader.util.DateHelper;
+import xyz.fycz.myreader.util.ShareUtils;
+import xyz.fycz.myreader.util.SharedPreUtils;
+import xyz.fycz.myreader.util.StringHelper;
+import xyz.fycz.myreader.util.SystemUtil;
+import xyz.fycz.myreader.util.ToastUtils;
import xyz.fycz.myreader.util.notification.NotificationClickReceiver;
import xyz.fycz.myreader.util.notification.NotificationUtil;
import xyz.fycz.myreader.util.utils.ColorUtil;
import xyz.fycz.myreader.util.utils.NetworkUtils;
+import xyz.fycz.myreader.util.utils.ScreenUtils;
+import xyz.fycz.myreader.util.utils.StringUtils;
import xyz.fycz.myreader.util.utils.SystemBarUtils;
import xyz.fycz.myreader.webapi.CommonApi;
import xyz.fycz.myreader.webapi.callback.ResultCallback;
import xyz.fycz.myreader.webapi.crawler.ReadCrawlerUtil;
import xyz.fycz.myreader.webapi.crawler.base.ReadCrawler;
+import xyz.fycz.myreader.widget.BubblePopupView;
import xyz.fycz.myreader.widget.page.LocalPageLoader;
import xyz.fycz.myreader.widget.page.PageLoader;
import xyz.fycz.myreader.widget.page.PageMode;
import xyz.fycz.myreader.widget.page.PageView;
-import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static xyz.fycz.myreader.util.UriFileUtil.getPath;
@@ -94,10 +116,12 @@ import static xyz.fycz.myreader.util.UriFileUtil.getPath;
* @author fengyue
* @date 2020/10/21 16:46
*/
-public class ReadActivity extends BaseActivity implements ColorPickerDialogListener {
+public class ReadActivity extends BaseActivity implements ColorPickerDialogListener, View.OnTouchListener {
private static final String TAG = ReadActivity.class.getSimpleName();
/*****************************View***********************************/
+ @BindView(R.id.rl_content)
+ RelativeLayout rlContent;
@BindView(R.id.toolbar)
Toolbar toolbar;
@BindView(R.id.read_abl_top_menu)
@@ -112,6 +136,10 @@ public class ReadActivity extends BaseActivity implements ColorPickerDialogListe
PageView pageView;
@BindView(R.id.pb_loading)
ProgressBar pbLoading;
+ @BindView(R.id.cursor_left)
+ ImageView cursorLeft;
+ @BindView(R.id.cursor_right)
+ ImageView cursorRight;
@BindView(R.id.read_tv_page_tip)
TextView readTvPageTip;
@BindView(R.id.read_tv_pre_chapter)
@@ -198,6 +226,7 @@ public class ReadActivity extends BaseActivity implements ColorPickerDialogListe
private Animation mTopOutAnim;
private Animation mBottomInAnim;
private Animation mBottomOutAnim;
+ private int lastX, lastY;
// 接收电池信息和时间更新的广播
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@@ -297,7 +326,7 @@ public class ReadActivity extends BaseActivity implements ColorPickerDialogListe
if (savedInstanceState != null) {
pagePos = savedInstanceState.getInt("pagePos");
chapterPos = savedInstanceState.getInt("chapterPos");
- }else {
+ } else {
pagePos = -1;
chapterPos = -1;
}
@@ -330,7 +359,7 @@ public class ReadActivity extends BaseActivity implements ColorPickerDialogListe
finish();
return;
}
- if (pagePos != -1 && chapterPos != -1){
+ if (pagePos != -1 && chapterPos != -1) {
mBook.setHisttoryChapterNum(chapterPos);
mBook.setLastReadPosition(pagePos);
}
@@ -375,12 +404,14 @@ public class ReadActivity extends BaseActivity implements ColorPickerDialogListe
}
+ @SuppressLint("ClickableViewAccessibility")
@Override
protected void initClick() {
super.initClick();
pageView.setTouchListener(new PageView.TouchListener() {
@Override
public boolean onTouch() {
+ screenOffTimerStart();
return !hideReadMenu();
}
@@ -407,6 +438,21 @@ public class ReadActivity extends BaseActivity implements ColorPickerDialogListe
@Override
public void cancel() {
}
+
+ @Override
+ public void onTouchClearCursor() {
+ cursorLeft.setVisibility(View.INVISIBLE);
+ cursorRight.setVisibility(View.INVISIBLE);
+ longPressMenu.hidePopupListWindow();
+ }
+
+ @Override
+ public void onLongPress() {
+ if (!pageView.isRunning()) {
+ selectTextCursorShow();
+ showAction();
+ }
+ }
});
mPageLoader.setOnPageChangeListener(
@@ -538,8 +584,13 @@ public class ReadActivity extends BaseActivity implements ColorPickerDialogListe
}
mAudioPlayerDialog.show();
});
+ initReadLongPressPop();
+ cursorLeft.setOnTouchListener(this);
+ cursorRight.setOnTouchListener(this);
+ rlContent.setOnTouchListener(this);
}
+
@Override
protected void processLogic() {
super.processLogic();
@@ -575,6 +626,9 @@ public class ReadActivity extends BaseActivity implements ColorPickerDialogListe
customizeComMenu.getVisibility() != View.VISIBLE &&
readSettingMenu.getVisibility() != View.VISIBLE &&
brightnessEyeMenu.getVisibility() != View.VISIBLE) {
+ if (pageView.getSelectMode() != PageView.SelectMode.Normal) {
+ clearSelect();
+ }
switch (keyCode) {
case KeyEvent.KEYCODE_VOLUME_UP:
if (ReadAloudService.running) {
@@ -724,6 +778,10 @@ public class ReadActivity extends BaseActivity implements ColorPickerDialogListe
"》:" + bookMark.getTitle() + "[" + (bookMark.getBookMarkReadPosition() + 1) +
"]\n书签添加成功,书签列表可在目录界面查看!");
return true;
+ case R.id.action_replace_content:
+ Intent ruleIntent = new Intent(this, RuleActivity.class);
+ startActivityForResult(ruleIntent, APPCONST.REQUEST_REFRESH_READ_UI);
+ break;
case R.id.action_copy_content:
new CopyContentDialog(this, mPageLoader.getContent()).show();
break;
@@ -912,10 +970,10 @@ public class ReadActivity extends BaseActivity implements ColorPickerDialogListe
Book book = new Book();
book.setName(file.getName().replace(".txt", ""));
book.setChapterUrl(path);
- book.setType("本地书籍");
+ book.setType(getString(R.string.local_book));
book.setHistoryChapterId("未开始阅读");
book.setNewestChapterTitle("未拆分章节");
- book.setAuthor("本地书籍");
+ book.setAuthor(getString(R.string.local_book));
book.setSource(BookSource.local.toString());
book.setDesc("无");
book.setIsCloseUpdate(true);
@@ -926,9 +984,9 @@ public class ReadActivity extends BaseActivity implements ColorPickerDialogListe
return;
}
- if (BookGroupService.getInstance().curGroupIsPrivate()){
+ if (BookGroupService.getInstance().curGroupIsPrivate()) {
mBookService.addBookNoGroup(book);
- }else {
+ } else {
mBookService.addBook(book);
}
mBook = book;
@@ -953,14 +1011,14 @@ public class ReadActivity extends BaseActivity implements ColorPickerDialogListe
private void getData() {
mChapters = (ArrayList) mChapterService.findBookAllChapterByBookId(mBook.getId());
if (!isCollected || mChapters.size() == 0 || ("本地书籍".equals(mBook.getType()) && !ChapterService.isChapterCached(mBook.getId(), mChapters.get(0).getTitle())
- )) {
+ )) {
if ("本地书籍".equals(mBook.getType())) {
if (!new File(mBook.getChapterUrl()).exists()) {
ToastUtils.showWarring("书籍缓存为空且源文件不存在,书籍加载失败!");
finish();
return;
}
- if (mChapters.size() != 0 && mChapters.get(0).getEnd() > 0){
+ if (mChapters.size() != 0 && mChapters.get(0).getEnd() > 0) {
initChapters();
return;
}
@@ -1853,4 +1911,256 @@ public class ReadActivity extends BaseActivity implements ColorPickerDialogListe
return Color.argb(a, r, g, b);
}
+ /***********************长按弹出菜单相关*************************/
+ @SuppressLint("ClickableViewAccessibility")
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (v.getId() == R.id.cursor_left || v.getId() == R.id.cursor_right) {
+ int ea = event.getAction();
+ //final int screenWidth = dm.widthPixels;
+ //final int screenHeight = dm.heightPixels;
+ switch (ea) {
+ case MotionEvent.ACTION_DOWN:
+ lastX = (int) event.getRawX();// 获取触摸事件触摸位置的原始X坐标
+ lastY = (int) event.getRawY();
+ longPressMenu.hidePopupListWindow();
+ break;
+ case MotionEvent.ACTION_MOVE:
+ int dx = (int) event.getRawX() - lastX;
+ int dy = (int) event.getRawY() - lastY;
+ int l = v.getLeft() + dx;
+ int b = v.getBottom() + dy;
+ int r = v.getRight() + dx;
+ int t = v.getTop() + dy;
+
+ v.layout(l, t, r, b);
+ lastX = (int) event.getRawX();
+ lastY = (int) event.getRawY();
+ v.postInvalidate();
+
+ //移动过程中要画线
+ pageView.setSelectMode(PageView.SelectMode.SelectMoveForward);
+
+ int hh = cursorLeft.getHeight();
+ int ww = cursorLeft.getWidth();
+
+ if (v.getId() == R.id.cursor_left) {
+ pageView.setFirstSelectTxtChar(pageView.getCurrentTxtChar(lastX + ww, lastY - hh));
+ if (pageView.getFirstSelectTxtChar() != null) {
+ cursorLeft.setX(pageView.getFirstSelectTxtChar().getTopLeftPosition().x - ww);
+ cursorLeft.setY(pageView.getFirstSelectTxtChar().getBottomLeftPosition().y);
+ }
+ } else {
+ pageView.setLastSelectTxtChar(pageView.getCurrentTxtChar(lastX - ww, lastY - hh));
+ if (pageView.getLastSelectTxtChar() != null) {
+ cursorRight.setX(pageView.getLastSelectTxtChar().getBottomRightPosition().x);
+ cursorRight.setY(pageView.getLastSelectTxtChar().getBottomRightPosition().y);
+ }
+ }
+
+ pageView.invalidate();
+
+ break;
+ case MotionEvent.ACTION_UP:
+ showAction();
+ //v.layout(l, t, r, b);
+ break;
+ default:
+ break;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * 显示长按菜单
+ */
+ public void showAction() {
+ float x, y;
+ if (cursorLeft.getX() - cursorRight.getX() > 0){
+ x = cursorRight.getX() + (cursorLeft.getX() - cursorRight.getX()) / 2 + ScreenUtils.dpToPx(12);
+ }else {
+ x = cursorLeft.getX() + (cursorRight.getX() - cursorLeft.getX()) / 2 + ScreenUtils.dpToPx(12);
+ }
+ if ((cursorLeft.getY() - ScreenUtils.spToPx(mSetting.getReadWordSize()) - ScreenUtils.dpToPx(60)) < 0) {
+ longPressMenu.setShowBottom(true);
+ y = cursorLeft.getY() + cursorLeft.getHeight() * 3 / 5;
+ } else {
+ longPressMenu.setShowBottom(false);
+ y = cursorLeft.getY() - ScreenUtils.spToPx(mSetting.getReadWordSize()) - ScreenUtils.dpToPx(5);
+ }
+ longPressMenu.showPopupListWindow(rlContent, 0, x, y,
+ longPressMenuItems, longPressMenuListener);
+ }
+
+ /**
+ * 显示
+ */
+ private void selectTextCursorShow() {
+ if (pageView.getFirstSelectTxtChar() == null || pageView.getLastSelectTxtChar() == null)
+ return;
+ //show Cursor on current position
+ cursorShow();
+ //set current word selected
+ pageView.invalidate();
+
+// hideSnackBar();
+ }
+
+ /**
+ * 显示选择
+ */
+ private void cursorShow() {
+ cursorLeft.setVisibility(View.VISIBLE);
+ cursorRight.setVisibility(View.VISIBLE);
+ int hh = cursorLeft.getHeight();
+ int ww = cursorLeft.getWidth();
+ if (pageView.getFirstSelectTxtChar() != null) {
+ cursorLeft.setX(pageView.getFirstSelectTxtChar().getTopLeftPosition().x - ww);
+ cursorLeft.setY(pageView.getFirstSelectTxtChar().getBottomLeftPosition().y);
+ cursorRight.setX(pageView.getFirstSelectTxtChar().getBottomRightPosition().x);
+ cursorRight.setY(pageView.getFirstSelectTxtChar().getBottomRightPosition().y);
+ }
+ }
+
+ private final List longPressMenuItems = new ArrayList<>();
+ private BubblePopupView longPressMenu;
+ private BubblePopupView.PopupListListener longPressMenuListener;
+
+ /**
+ * 长按选择按钮
+ */
+ private void initReadLongPressPop() {
+ longPressMenuItems.add("拷贝");
+ longPressMenuItems.add("替换");
+ longPressMenuItems.add("发声");
+ longPressMenuItems.add("搜索");
+ longPressMenuItems.add("分享");
+ longPressMenu = new BubblePopupView(this);
+ //是否跟随手指显示,默认false,设置true后翻转高度无效,永远在上方显示
+ longPressMenu.setShowTouchLocation(true);
+ longPressMenu.setFocusable(false);
+ longPressMenuListener = new BubblePopupView.PopupListListener() {
+ @Override
+ public boolean showPopupList(View adapterView, View contextView, int contextPosition) {
+ return true;
+ }
+
+ @Override
+ public void onPopupListClick(View contextView, int contextPosition, int position) {
+ String selectString;
+ switch (position) {
+ case 0:
+ ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clipData = ClipData.newPlainText(null, pageView.getSelectStr());
+ if (clipboard != null) {
+ clipboard.setPrimaryClip(clipData);
+ ToastUtils.showInfo("所选内容已经复制到剪贴板");
+ }
+ clearSelect();
+ break;
+ case 1:
+ ReplaceRuleBean oldRuleBean = new ReplaceRuleBean();
+ oldRuleBean.setReplaceSummary("");
+ oldRuleBean.setEnable(true);
+ oldRuleBean.setRegex(pageView.getSelectStr().trim());
+ oldRuleBean.setIsRegex(false);
+ oldRuleBean.setReplacement("");
+ oldRuleBean.setSerialNumber(0);
+ oldRuleBean.setUseTo(String.format("%s;%s", mBook.getSource(), mBook.getName() + "-" + mBook.getAuthor()));
+ ReplaceDialog replaceDialog = new ReplaceDialog(ReadActivity.this, oldRuleBean
+ , () -> {
+ ToastUtils.showSuccess("内容替换规则添加成功!");
+ clearSelect();
+ mPageLoader.refreshUi();
+ });
+ replaceDialog.show(getSupportFragmentManager(), "replaceRule");
+ break;
+ case 2:
+ selectString = pageView.getSelectStr();
+ speak(ReadActivity.this, selectString);
+ clearSelect();
+ break;
+ case 3:
+ selectString = StringUtils.deleteWhitespace(pageView.getSelectStr());
+ MyAlertDialog.build(ReadActivity.this)
+ .setTitle(R.string.search)
+ .setItems(R.array.search_way, (dialog, which) -> {
+ String url = "";
+ switch (which) {
+ case 0:
+ url = URLCONST.BAI_DU_SEARCH;
+ break;
+ case 1:
+ url = URLCONST.GOOGLE_SEARCH;
+ break;
+ case 2:
+ url = URLCONST.YOU_DAO_SEARCH;
+ break;
+ }
+ url = url.replace("{key}", selectString);
+ Log.d("SEARCH_URL", url);
+ try {
+ Uri uri = Uri.parse(url);
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ startActivity(intent);
+ } catch (Exception e) {
+ e.printStackTrace();
+ ToastUtils.showError(e.getLocalizedMessage());
+ }
+ }).setNegativeButton("取消", null)
+ .show();
+ clearSelect();
+ break;
+ case 4:
+ selectString = pageView.getSelectStr();
+ ShareUtils.share(ReadActivity.this, selectString);
+ clearSelect();
+ break;
+ }
+ }
+ };
+ }
+
+ /**
+ * 清除选择
+ */
+ private void clearSelect() {
+ cursorLeft.setVisibility(View.INVISIBLE);
+ cursorRight.setVisibility(View.INVISIBLE);
+ longPressMenu.hidePopupListWindow();
+ pageView.clearSelect();
+ }
+
+ private TextToSpeech textToSpeech;
+ private boolean ttsInitFinish = false;
+ private String lastText = "";
+
+ /**
+ * 发声
+ *
+ * @param context
+ * @param text
+ */
+ public void speak(Context context, String text) {
+ lastText = text;
+ if (textToSpeech == null) {
+ textToSpeech = new TextToSpeech(context, status -> {
+ if (status == TextToSpeech.SUCCESS) {
+ textToSpeech.setLanguage(Locale.CHINA);
+ ttsInitFinish = true;
+ speak(context, lastText);
+ } else {
+ ToastUtils.showError("TTS初始化失败!");
+ }
+ });
+ return;
+ }
+ if (!ttsInitFinish) return;
+ if ("".equals(text)) return;
+ if (textToSpeech.isSpeaking())
+ textToSpeech.stop();
+ textToSpeech.speak(text, TextToSpeech.QUEUE_ADD, null, "select_text");
+ lastText = "";
+ }
}
diff --git a/app/src/main/java/xyz/fycz/myreader/ui/activity/RuleActivity.java b/app/src/main/java/xyz/fycz/myreader/ui/activity/RuleActivity.java
new file mode 100644
index 0000000..e080b14
--- /dev/null
+++ b/app/src/main/java/xyz/fycz/myreader/ui/activity/RuleActivity.java
@@ -0,0 +1,265 @@
+package xyz.fycz.myreader.ui.activity;
+
+import android.annotation.SuppressLint;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.widget.LinearLayout;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import androidx.documentfile.provider.DocumentFile;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.mcxtzhang.swipemenulib.SwipeMenuLayout;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import butterknife.BindView;
+import io.reactivex.Observable;
+import io.reactivex.ObservableOnSubscribe;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.annotations.NonNull;
+import io.reactivex.schedulers.Schedulers;
+import xyz.fycz.myreader.R;
+import xyz.fycz.myreader.base.BaseActivity;
+import xyz.fycz.myreader.base.observer.MyObserver;
+import xyz.fycz.myreader.base.observer.MySingleObserver;
+import xyz.fycz.myreader.common.APPCONST;
+import xyz.fycz.myreader.greendao.entity.ReplaceRuleBean;
+import xyz.fycz.myreader.model.ReplaceRuleManager;
+import xyz.fycz.myreader.ui.adapter.ReplaceRuleAdapter;
+import xyz.fycz.myreader.ui.dialog.DialogCreator;
+import xyz.fycz.myreader.ui.dialog.MyAlertDialog;
+import xyz.fycz.myreader.ui.dialog.ReplaceDialog;
+import xyz.fycz.myreader.util.ToastUtils;
+import xyz.fycz.myreader.util.utils.ClipBoardUtil;
+import xyz.fycz.myreader.util.utils.FileUtils;
+import xyz.fycz.myreader.util.utils.GsonExtensionsKt;
+import xyz.fycz.myreader.widget.DividerItemDecoration;
+
+import static android.text.TextUtils.isEmpty;
+import static xyz.fycz.myreader.util.UriFileUtil.getPath;
+
+/**
+ * @author fengyue
+ * @date 2021/1/19 10:02
+ */
+public class RuleActivity extends BaseActivity {
+ @BindView(R.id.rv_rule_list)
+ RecyclerView rvRuleList;
+
+ private List mReplaceRules;
+ private ReplaceRuleAdapter mAdapter;
+
+ @Override
+ protected int getContentId() {
+ return R.layout.activity_rule;
+ }
+
+ @Override
+ protected void initData(Bundle savedInstanceState) {
+ super.initData(savedInstanceState);
+ ReplaceRuleManager.getAll().subscribe(new MySingleObserver>() {
+ @Override
+ public void onSuccess(@NonNull List replaceRuleBeans) {
+ mReplaceRules = replaceRuleBeans;
+ initRuleList();
+ setUpBarTitle();
+ }
+
+ @Override
+ public void onError(Throwable e) {
+ ToastUtils.showError("数据加载失败\n" + e.getLocalizedMessage());
+ }
+ });
+ }
+
+ @Override
+ protected void setUpToolbar(Toolbar toolbar) {
+ super.setUpToolbar(toolbar);
+ setUpBarTitle();
+ setStatusBarColor(R.color.colorPrimary, true);
+ }
+
+ private void setUpBarTitle() {
+ getSupportActionBar().setTitle(String.format("%s(共%s个)",
+ getString(R.string.replace_rule), mReplaceRules == null ? 0 : mReplaceRules.size()));
+ }
+
+ protected void initRuleList() {
+ mAdapter = new ReplaceRuleAdapter(this, (which, data) -> {
+ mReplaceRules.remove(data);
+ mAdapter.notifyItemRemoved(which);
+ mAdapter.removeItem2(data);
+ setUpBarTitle();
+ });
+ rvRuleList.setLayoutManager(new LinearLayoutManager(this));
+ rvRuleList.setAdapter(mAdapter);
+ //设置分割线
+ rvRuleList.addItemDecoration(new DividerItemDecoration(this));
+ mAdapter.refreshItems(mReplaceRules);
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ @Override
+ protected void initClick() {
+ super.initClick();
+ rvRuleList.setOnTouchListener((v, event) -> {
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ SwipeMenuLayout viewCache = SwipeMenuLayout.getViewCache();
+ if (null != viewCache) {
+ viewCache.smoothClose();
+ }
+ }
+ return false;
+ });
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_rule, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_add_rule:
+ ReplaceRuleBean newRuleBean = new ReplaceRuleBean();
+ newRuleBean.setReplaceSummary("");
+ newRuleBean.setEnable(true);
+ newRuleBean.setRegex("");
+ newRuleBean.setIsRegex(false);
+ newRuleBean.setReplacement("");
+ newRuleBean.setSerialNumber(0);
+ newRuleBean.setUseTo("");
+ ReplaceDialog replaceDialog = new ReplaceDialog(this, newRuleBean
+ , () -> {
+ ToastUtils.showSuccess("内容替换规则添加成功!");
+ mReplaceRules.add(newRuleBean);
+ mAdapter.addItem(newRuleBean);
+ setUpBarTitle();
+ refreshUI();
+ });
+ replaceDialog.show(getSupportFragmentManager(), "replaceRule");
+ break;
+ case R.id.action_import:
+ MyAlertDialog.build(this)
+ .setTitle("导入规则")
+ .setItems(R.array.import_rule, (dialog, which) -> {
+ if (which == 0) {
+ String text = ClipBoardUtil.paste(this);
+ if (!isEmpty(text)) {
+ importDataS(text);
+ } else {
+ ToastUtils.showError("剪切板内容为空,导入失败");
+ }
+ } else {
+ ToastUtils.showInfo("请选择内容替换规则JSON文件");
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.setType("application/json");
+ startActivityForResult(intent, APPCONST.REQUEST_IMPORT_REPLACE_RULE);
+ }
+ }).show();
+ break;
+ case R.id.action_export:
+ if (mReplaceRules == null || mReplaceRules.size() == 0){
+ ToastUtils.showWarring("当前没有任何规则,无法导出!");
+ return true;
+ }
+ if (FileUtils.writeText(GsonExtensionsKt.getGSON().toJson(mReplaceRules),
+ FileUtils.getFile(APPCONST.FILE_DIR + "ReplaceRule.json"))) {
+ DialogCreator.createTipDialog(this,
+ "内容替换规则导出成功,导出位置:" + APPCONST.FILE_DIR + "ReplaceRule.json");
+ }
+ break;
+ case R.id.action_reverse:
+ for (ReplaceRuleBean ruleBean : mReplaceRules) {
+ ruleBean.setEnable(!ruleBean.getEnable());
+ }
+ ReplaceRuleManager.addDataS(mReplaceRules);
+ mAdapter.notifyDataSetChanged();
+ refreshUI();
+ break;
+ case R.id.action_delete:
+ DialogCreator.createCommonDialog(this, "删除禁用规则",
+ "确定要删除所有禁用规则吗?", true,
+ (dialog, which) -> {
+ List ruleBeans = new ArrayList<>();
+ for (ReplaceRuleBean ruleBean : mReplaceRules) {
+ if (!ruleBean.getEnable()) {
+ ruleBeans.add(ruleBean);
+ }
+ }
+ ReplaceRuleManager.delDataS(ruleBeans);
+ mReplaceRules.removeAll(ruleBeans);
+ mAdapter.removeItems(ruleBeans);
+ ToastUtils.showSuccess("禁用规则删除成功");
+ setUpBarTitle();
+ refreshUI();
+ }, null);
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ if (resultCode == RESULT_OK) {
+ if (requestCode == APPCONST.REQUEST_IMPORT_REPLACE_RULE) {
+ String path = getPath(this, data.getData());
+ String json = FileUtils.readText(path);
+ if (!isEmpty(json)) {
+ importDataS(json);
+ } else {
+ ToastUtils.showError("文件读取失败");
+ }
+ }
+ }
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ public void importDataS(String text) {
+ Observable observable = ReplaceRuleManager.importReplaceRule(text);
+ if (observable != null) {
+ observable.subscribe(new MyObserver() {
+ @Override
+ public void onNext(Boolean aBoolean) {
+ if (aBoolean) {
+ mReplaceRules = ReplaceRuleManager.getAllRules();
+ mAdapter.refreshItems(mReplaceRules);
+ setUpBarTitle();
+ refreshUI();
+ ToastUtils.showSuccess("内容替换规则导入成功");
+ } else {
+ ToastUtils.showError("格式不对");
+ }
+ }
+
+ @Override
+ public void onError(Throwable e) {
+ ToastUtils.showError("格式不对");
+ }
+ });
+ } else {
+ ToastUtils.showError("导入失败");
+ }
+ }
+
+ private void refreshUI() {
+ Intent result = new Intent();
+ result.putExtra(APPCONST.RESULT_NEED_REFRESH, true);
+ setResult(AppCompatActivity.RESULT_OK, result);
+ }
+}
diff --git a/app/src/main/java/xyz/fycz/myreader/ui/adapter/ReplaceRuleAdapter.java b/app/src/main/java/xyz/fycz/myreader/ui/adapter/ReplaceRuleAdapter.java
new file mode 100644
index 0000000..6527f85
--- /dev/null
+++ b/app/src/main/java/xyz/fycz/myreader/ui/adapter/ReplaceRuleAdapter.java
@@ -0,0 +1,37 @@
+package xyz.fycz.myreader.ui.adapter;
+
+import android.app.Activity;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import xyz.fycz.myreader.base.adapter.BaseListAdapter;
+import xyz.fycz.myreader.base.adapter.IViewHolder;
+import xyz.fycz.myreader.greendao.entity.ReplaceRuleBean;
+import xyz.fycz.myreader.ui.adapter.holder.ReplaceRuleHolder;
+
+/**
+ * @author fengyue
+ * @date 2021/1/19 9:51
+ */
+public class ReplaceRuleAdapter extends BaseListAdapter {
+ private AppCompatActivity activity;
+ private OnDeleteListener onDeleteListener;
+
+ public ReplaceRuleAdapter(AppCompatActivity activity, OnDeleteListener onDeleteListener) {
+ this.activity = activity;
+ this.onDeleteListener = onDeleteListener;
+ }
+
+ @Override
+ protected IViewHolder createViewHolder(int viewType) {
+ return new ReplaceRuleHolder(activity, onDeleteListener);
+ }
+
+ public void removeItem2(ReplaceRuleBean ruleBean){
+ mList.remove(ruleBean);
+ }
+
+ public interface OnDeleteListener{
+ void success(int which, ReplaceRuleBean ruleBean);
+ }
+}
diff --git a/app/src/main/java/xyz/fycz/myreader/ui/adapter/holder/ReplaceRuleHolder.java b/app/src/main/java/xyz/fycz/myreader/ui/adapter/holder/ReplaceRuleHolder.java
new file mode 100644
index 0000000..53759b6
--- /dev/null
+++ b/app/src/main/java/xyz/fycz/myreader/ui/adapter/holder/ReplaceRuleHolder.java
@@ -0,0 +1,144 @@
+package xyz.fycz.myreader.ui.adapter.holder;
+
+import android.app.Activity;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.widget.Button;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.mcxtzhang.swipemenulib.SwipeMenuLayout;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.reactivex.Observable;
+import io.reactivex.ObservableOnSubscribe;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.annotations.NonNull;
+import io.reactivex.schedulers.Schedulers;
+import xyz.fycz.myreader.R;
+import xyz.fycz.myreader.base.adapter.ViewHolderImpl;
+import xyz.fycz.myreader.base.observer.MyObserver;
+import xyz.fycz.myreader.base.observer.MySingleObserver;
+import xyz.fycz.myreader.common.APPCONST;
+import xyz.fycz.myreader.greendao.entity.ReplaceRuleBean;
+import xyz.fycz.myreader.model.ReplaceRuleManager;
+import xyz.fycz.myreader.ui.adapter.ReplaceRuleAdapter;
+import xyz.fycz.myreader.ui.dialog.ReplaceDialog;
+import xyz.fycz.myreader.util.ShareUtils;
+import xyz.fycz.myreader.util.ToastUtils;
+import xyz.fycz.myreader.util.utils.GsonExtensionsKt;
+
+/**
+ * @author fengyue
+ * @date 2021/1/19 9:54
+ */
+public class ReplaceRuleHolder extends ViewHolderImpl {
+ private RelativeLayout rlContent;
+ private TextView tvRuleSummary;
+ private Button btBan;
+ private Button btShare;
+ private Button btDelete;
+ private AppCompatActivity activity;
+ private ReplaceRuleAdapter.OnDeleteListener onDeleteListener;
+
+ public ReplaceRuleHolder(AppCompatActivity activity, ReplaceRuleAdapter.OnDeleteListener onDeleteListener) {
+ this.activity = activity;
+ this.onDeleteListener = onDeleteListener;
+ }
+
+
+ @Override
+ protected int getItemLayoutId() {
+ return R.layout.item_replace_rule;
+ }
+
+ @Override
+ public void initView() {
+ rlContent = findById(R.id.rl_content);
+ tvRuleSummary = findById(R.id.tv_rule_summary);
+ btBan = findById(R.id.bt_ban);
+ btShare = findById(R.id.bt_share);
+ btDelete = findById(R.id.btnDelete);
+ }
+
+ @Override
+ public void onBind(ReplaceRuleBean data, int pos) {
+ banOrUse(data);
+
+ rlContent.setOnClickListener(v -> {
+ ReplaceDialog replaceDialog = new ReplaceDialog(activity, data,
+ () -> {
+ banOrUse(data);
+ ToastUtils.showSuccess("内容替换规则修改成功!");
+ refreshUI();
+ });
+ replaceDialog.show(activity.getSupportFragmentManager(), "");
+ });
+
+ btBan.setOnClickListener(v -> {
+ ((SwipeMenuLayout) getItemView()).smoothClose();
+ data.setEnable(!data.getEnable());
+ ReplaceRuleManager.saveData(data)
+ .subscribe(new MySingleObserver() {
+ @Override
+ public void onSuccess(@NonNull Boolean aBoolean) {
+ if (aBoolean) {
+ banOrUse(data);
+ refreshUI();
+ }
+ }
+ });
+ });
+ btShare.setOnClickListener(v -> {
+ ((SwipeMenuLayout) getItemView()).smoothClose();
+ List shareRuleBean = new ArrayList<>();
+ shareRuleBean.add(data);
+ ShareUtils.share(activity, GsonExtensionsKt.getGSON().toJson(shareRuleBean));
+ });
+ btDelete.setOnClickListener(v -> {
+ ((SwipeMenuLayout) getItemView()).smoothClose();
+ Observable.create((ObservableOnSubscribe) e -> {
+ ReplaceRuleManager.delData(data);
+ e.onNext(true);
+ }).subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(new MyObserver() {
+ @Override
+ public void onNext(Boolean aBoolean) {
+ onDeleteListener.success(pos, data);
+ refreshUI();
+ }
+
+ @Override
+ public void onError(Throwable e) {
+ ToastUtils.showError("删除失败");
+ }
+ });
+
+ });
+ }
+
+ private void banOrUse(ReplaceRuleBean data){
+ if (data.getEnable()) {
+ tvRuleSummary.setTextColor(getContext().getResources().getColor(R.color.textPrimary));
+ tvRuleSummary.setText(String.format("%s->%s", data.getRegex(), data.getReplacement()));
+ btBan.setText(getContext().getString(R.string.ban));
+ } else {
+ tvRuleSummary.setTextColor(getContext().getResources().getColor(R.color.textSecondary));
+ tvRuleSummary.setText(String.format("(禁用中)%s->%s", data.getRegex(), data.getReplacement()));
+ btBan.setText(R.string.enable_use);
+ }
+ }
+
+ private void refreshUI(){
+ Intent result = new Intent();
+ result.putExtra(APPCONST.RESULT_NEED_REFRESH, true);
+ activity.setResult(AppCompatActivity.RESULT_OK, result);
+ }
+}
diff --git a/app/src/main/java/xyz/fycz/myreader/ui/dialog/ReplaceDialog.java b/app/src/main/java/xyz/fycz/myreader/ui/dialog/ReplaceDialog.java
new file mode 100644
index 0000000..bb2937b
--- /dev/null
+++ b/app/src/main/java/xyz/fycz/myreader/ui/dialog/ReplaceDialog.java
@@ -0,0 +1,218 @@
+package xyz.fycz.myreader.ui.dialog;
+
+import android.app.Activity;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatEditText;
+import androidx.fragment.app.DialogFragment;
+
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+
+import xyz.fycz.myreader.R;
+import xyz.fycz.myreader.base.observer.MySingleObserver;
+import xyz.fycz.myreader.enums.BookSource;
+import xyz.fycz.myreader.greendao.entity.Book;
+import xyz.fycz.myreader.greendao.entity.ReplaceRuleBean;
+import xyz.fycz.myreader.greendao.service.BookService;
+import xyz.fycz.myreader.model.ReplaceRuleManager;
+import xyz.fycz.myreader.util.SharedPreUtils;
+import xyz.fycz.myreader.util.ToastUtils;
+import xyz.fycz.myreader.webapi.crawler.ReadCrawlerUtil;
+
+/**
+ * @author fengyue
+ * @date 2021/1/18 20:04
+ */
+public class ReplaceDialog extends DialogFragment {
+ private ReplaceRuleBean replaceRule;
+ private Activity activity;
+ private OnSaveReplaceRule onSaveReplaceRule;
+ private EditText etRuleDesc;
+ private EditText etRuleOld;
+ private EditText etRuleNew;
+ private EditText etRuleSource;
+ private EditText etRuleBook;
+ private CheckBox cbUseRegex;
+ private Button btSelectSource;
+ private Button btSelectBook;
+ private TextView tvConfirm;
+ private TextView tvCancel;
+
+ public ReplaceDialog(Activity activity, ReplaceRuleBean replaceRule, OnSaveReplaceRule onSaveReplaceRule) {
+ this.activity = activity;
+ this.replaceRule = replaceRule;
+ this.onSaveReplaceRule = onSaveReplaceRule;
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setStyle(DialogFragment.STYLE_NORMAL, R.style.alertDialogTheme);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.dialog_replace, container, false);
+ etRuleDesc = v.findViewById(R.id.et_rule_desc);
+ etRuleOld = v.findViewById(R.id.et_rule_old);
+ etRuleNew = v.findViewById(R.id.et_rule_new);
+ etRuleSource = v.findViewById(R.id.et_rule_source);
+ etRuleBook = v.findViewById(R.id.et_rule_book);
+ cbUseRegex = v.findViewById(R.id.cb_use_regex);
+ btSelectSource = v.findViewById(R.id.bt_select_source);
+ btSelectBook = v.findViewById(R.id.bt_select_book);
+ tvCancel = v.findViewById(R.id.tv_cancel);
+ tvConfirm = v.findViewById(R.id.tv_confirm);
+
+ etRuleDesc.setText(replaceRule.getReplaceSummary());
+ etRuleOld.setText(replaceRule.getRegex());
+ cbUseRegex.setChecked(replaceRule.getIsRegex());
+ etRuleNew.setText(replaceRule.getReplacement());
+ String[] useTo = replaceRule.getUseTo().split(";");
+ etRuleSource.setText(useTo.length > 0 ?useTo[0] : "");
+ etRuleBook.setText(useTo.length > 1 ? useTo[1] : "");
+ btSelectSource.setOnClickListener(v1 -> selectSource());
+ btSelectBook.setOnClickListener(v1 -> selectBook());
+
+ tvConfirm.setOnClickListener(v1 -> {
+ replaceRule.setReplaceSummary(etRuleDesc.getText().toString());
+ replaceRule.setRegex(etRuleOld.getText().toString());
+ replaceRule.setIsRegex(cbUseRegex.isChecked());
+ replaceRule.setReplacement(etRuleNew.getText().toString());
+ replaceRule.setUseTo(String.format("%s;%s", etRuleSource.getText().toString(), etRuleBook.getText().toString()));
+ ReplaceRuleManager.saveData(replaceRule).subscribe(new MySingleObserver() {
+ @Override
+ public void onSuccess(@NonNull Boolean aBoolean) {
+ onSaveReplaceRule.success();
+ dismiss();
+ }
+
+ @Override
+ public void onError(Throwable e) {
+ ToastUtils.showError("发生错误\n" + e.getLocalizedMessage());
+ }
+ });
+ });
+ tvCancel.setOnClickListener(v1 -> dismiss());
+ return v;
+ }
+
+ /**
+ * 选择书源
+ */
+ private void selectSource(){
+ List mSources = ReadCrawlerUtil.getAllSources();
+ CharSequence[] mSourcesName = new CharSequence[mSources.size()];
+ HashMap mSelectSources = new LinkedHashMap<>();
+ boolean[] isSelects = new boolean[mSources.size()];
+ int sSourceCount = 0;
+ int i = 0;
+
+ String selectSource = etRuleSource.getText().toString();
+
+ for (CharSequence sourceName : mSources) {
+ mSourcesName[i] = sourceName;
+ String source = BookSource.getFromName(String.valueOf(sourceName));
+ boolean isSelect = selectSource.contains(source);
+ if (isSelect) sSourceCount++;
+ mSelectSources.put(mSourcesName[i], isSelect);
+ isSelects[i++] = isSelect;
+ }
+
+ new MultiChoiceDialog().create(activity, "选择书源",
+ mSourcesName, isSelects, sSourceCount, (dialog, which) -> {
+ StringBuilder sb = new StringBuilder();
+ for (CharSequence sourceName : mSelectSources.keySet()) {
+ if (mSelectSources.get(sourceName)) {
+ sb.append(BookSource.getFromName(String.valueOf(sourceName)));
+ sb.append(",");
+ }
+ }
+ if (sb.lastIndexOf(",") >= 0) sb.deleteCharAt(sb.lastIndexOf(","));
+ etRuleSource.setText(sb.toString());
+ }, null, new DialogCreator.OnMultiDialogListener() {
+ @Override
+ public void onItemClick(DialogInterface dialog, int which, boolean isChecked) {
+ mSelectSources.put(mSourcesName[which], isChecked);
+ }
+
+ @Override
+ public void onSelectAll(boolean isSelectAll) {
+ for (CharSequence sourceName : mSelectSources.keySet()) {
+ mSelectSources.put(sourceName, isSelectAll);
+ }
+ }
+ });
+ }
+
+ /**
+ * 选择书籍
+ */
+ private void selectBook(){
+ List mBooks = BookService.getInstance().getAllBooksNoHide();
+ HashMap mSelectBooks = new LinkedHashMap<>();
+
+ if (mBooks == null || mBooks.size() == 0){
+ ToastUtils.showWarring("当前没有任何书籍!");
+ return;
+ }
+ String isSelect = etRuleBook.getText().toString();
+
+ CharSequence[] mBooksName = new CharSequence[mBooks.size()];
+ boolean[] isSelects = new boolean[mBooks.size()];
+ int sBookCount = 0;
+
+ for (int i = 0; i < mBooks.size(); i++) {
+ Book book = mBooks.get(i);
+ mBooksName[i] = book.getName() + "-" + book.getAuthor();
+ isSelects[i] = isSelect.contains(book.getName());
+ if (isSelects[i]) {
+ mSelectBooks.put(mBooksName[i], true);
+ sBookCount++;
+ }else {
+ mSelectBooks.put(mBooksName[i], false);
+ }
+ }
+
+ new MultiChoiceDialog().create(activity, "选择书籍",
+ mBooksName, isSelects, sBookCount, (dialog, which) -> {
+ StringBuilder sb = new StringBuilder();
+ for (CharSequence bookName : mSelectBooks.keySet()) {
+ if (mSelectBooks.get(bookName)) {
+ sb.append(bookName);
+ sb.append(",");
+ }
+ }
+ if (sb.lastIndexOf(",") >= 0) sb.deleteCharAt(sb.lastIndexOf(","));
+ etRuleBook.setText(sb.toString());
+ }, null, new DialogCreator.OnMultiDialogListener() {
+ @Override
+ public void onItemClick(DialogInterface dialog, int which, boolean isChecked) {
+ mSelectBooks.put(mBooksName[which], isChecked);
+ }
+
+ @Override
+ public void onSelectAll(boolean isSelectAll) {
+ for (CharSequence bookName : mSelectBooks.keySet()) {
+ mSelectBooks.put(bookName, isSelectAll);
+ }
+ }
+ });
+ }
+ public interface OnSaveReplaceRule{
+ void success();
+ }
+}
diff --git a/app/src/main/java/xyz/fycz/myreader/util/ShareUtils.java b/app/src/main/java/xyz/fycz/myreader/util/ShareUtils.java
index aea07de..eaa01cb 100644
--- a/app/src/main/java/xyz/fycz/myreader/util/ShareUtils.java
+++ b/app/src/main/java/xyz/fycz/myreader/util/ShareUtils.java
@@ -2,12 +2,15 @@ package xyz.fycz.myreader.util;
import android.content.Context;
import android.content.Intent;
+import android.net.Uri;
+
+import androidx.core.content.FileProvider;
+
+import java.io.File;
+
+import xyz.fycz.myreader.BuildConfig;
import xyz.fycz.myreader.R;
-/**
- * Created by Zhouas666 on 2019-04-12
- * Github: https://github.com/zas023
- */
public class ShareUtils {
public static void share(Context context, int stringRes) {
share(context, context.getString(stringRes));
@@ -21,4 +24,15 @@ public class ShareUtils {
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(Intent.createChooser(intent, context.getString(R.string.share)));
}
+
+ public static void share(Context context, File share, String title, String mimeType){
+ //noinspection ResultOfMethodCallIgnored
+ share.setReadable(true, false);
+ Uri contentUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", share);
+ final Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.putExtra(Intent.EXTRA_STREAM, contentUri);
+ intent.setType(mimeType);
+ context.startActivity(Intent.createChooser(intent, title));
+ }
}
diff --git a/app/src/main/java/xyz/fycz/myreader/util/help/ChapterContentHelp.java b/app/src/main/java/xyz/fycz/myreader/util/help/ChapterContentHelp.java
index 0d76240..f54cb5e 100644
--- a/app/src/main/java/xyz/fycz/myreader/util/help/ChapterContentHelp.java
+++ b/app/src/main/java/xyz/fycz/myreader/util/help/ChapterContentHelp.java
@@ -1,9 +1,19 @@
package xyz.fycz.myreader.util.help;
import android.text.TextUtils;
+import android.util.Log;
import com.luhuiguo.chinese.ChineseUtils;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
import xyz.fycz.myreader.application.SysManager;
import xyz.fycz.myreader.enums.Language;
import xyz.fycz.myreader.greendao.entity.ReplaceRuleBean;
@@ -40,25 +50,1251 @@ public class ChapterContentHelp {
/**
* 替换净化
*/
- public String replaceContent(String bookName, String bookTag, String content, Boolean replaceEnable) {
+ public String replaceContent(String bookTag, String bookSource, String content, Boolean replaceEnable) {
if (!replaceEnable) return toTraditional(content);
if (ReplaceRuleManager.getEnabled().size() == 0) return toTraditional(content);
//替换
for (ReplaceRuleBean replaceRule : ReplaceRuleManager.getEnabled()) {
- if (isUseTo(replaceRule.getUseTo(), bookTag, bookName)) {
- try {
- content = content.replaceAll(replaceRule.getFixedRegex(), replaceRule.getReplacement());
- } catch (Exception ignored) {
+ if (isUseTo(replaceRule.getUseTo(), bookTag, bookSource)) {
+ {
+ try {
+ // 因为这里获取不到context,就不使用getString(R.string.replace_ad)了
+ if (replaceRule.getReplaceSummary().matches("^广告话术(-.*|$)")) {
+ // 跳过太短的文本
+ if (content.length() > 100)
+ content = replaceAd2(content, replaceRule.getRegex());
+ } else
+ content = content.replaceAll(replaceRule.getFixedRegex(), replaceRule.getReplacement());
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
}
}
}
return toTraditional(content);
}
- private boolean isUseTo(String useTo, String bookTag, String bookName) {
- return TextUtils.isEmpty(useTo)
+ // 緩存生成的廣告規則正則表達式
+// private Map adMap = new HashMap<>();
+ private Map adMap = new HashMap<>();
+ // 缓存长表达式,使用普通方式替换
+ private Map adMapL = new HashMap<>();
+
+ // 使用广告话术规则对正文进行替换,此方法为正则算法,效率较高,但是有漏失,故暂时放弃使用
+ private String replaceAd(String content, String replaceRule) {
+ // replaceRule只对选择的内容进行了切片,不包含正则
+
+ if (replaceRule == null)
+ return content;
+
+ replaceRule = replaceRule.substring(2, replaceRule.length() - 2).trim();
+
+// Pattern rule = adMap.get(replaceRule);
+ String rule = adMap.get(replaceRule);
+ StringBuffer buffer = new StringBuffer(replaceRule.length() * 2);
+
+ if (rule == null) {
+ String rules[] = replaceRule.split("\n");
+
+ for (String s : rules) {
+ s = s.trim();
+ if (s.length() < 1)
+ continue;
+
+ // 如果规则只包含特殊字符,且长度大于2,直接替换。如果长度不大于2,会在自动扩大范围的过程中包含字符
+ if (s.matches("\\p{P}*")) {
+ if (s.length() > 2) {
+ if (buffer.length() > 0)
+ buffer.append('|');
+ buffer.append(Pattern.quote(s));
+ }
+ } else {
+ // 如果规则不止包含特殊字符,需要移除首尾的特殊字符,把中间的空字符转换为\s+,把其他特殊字符转换为转义符
+ if (buffer.length() > 0)
+ buffer.append('|');
+ buffer.append(s
+ .replaceFirst("^\\p{P}+", "")
+ .replaceFirst("\\p{P}$", "")
+ .replaceAll("\\s+", "xxsp")
+ .replaceAll("(\\p{P})", "(\\\\p{P}?)")
+ .replaceAll("xxsp", "\\s+")
+ );
+ }
+ }
+ // 广告话术至少出现两次
+// rule = Pattern.compile("((" + buffer + ")(\\p{P}{0,2})){1,10}(" + buffer + ")");
+ rule = ("((" + buffer.toString() + ")(\\p{P}{0,2})){1,20}(" + buffer.toString() + ")((\\p{P}{0,12})(?=\\p{P}{2}))?");
+ adMap.put(replaceRule, rule);
+ }
+
+ content = content.replaceAll(rule, "");
+
+ rule = adMapL.get(replaceRule);
+ if (rule == null) {
+ String rules[] = replaceRule.split("\n");
+ buffer = new StringBuffer(replaceRule.length() * 2);
+
+ for (String s : rules) {
+ s = s.trim();
+ if (s.length() < 1)
+ continue;
+ if (s.length() > 6) {
+ if (buffer.length() > 0)
+ buffer.append('|');
+ buffer.append(Pattern.quote(s));
+ }
+ }
+ rule = "(" + buffer.toString() + ")";
+ adMapL.put(replaceRule, rule);
+ }
+// Pattern p=Pattern.compile(rule);
+ content = content.replaceAll(rule, "");
+ return content;
+ }
+
+
+ // 緩存生成的廣告 原文規則+正则扩展
+ // 原文与正则的最大区别,在于正则匹配规则对特殊符号的处理是保守的
+ private Map AdPatternMap = new HashMap<>();
+ // 不包含符号的文本形式的规则缓存。用于广告规则的第二次替换,以解决如下问题: 规则有 abc def,而实际出现了adefbc
+ private Map AdStringDict = new HashMap<>();
+
+ // 使用广告话术规则对正文进行替换,此方法 使用Matcher匹配,合并相邻区域,再StringBlock.getResult()的算法取回没有被替换的部分
+ // 广告话术规则的相关代码可能存在以下问题: 零宽断言书写错误, \p{P}的使用(比如我最开始不知道\p{P}是不包含\\s的), getResult.remove()的算法(为了方便调试专门写了verify方法)
+ private String replaceAd2(String content, String replaceRule) {
+
+ if (replaceRule == null)
+ return content;
+
+ StringBlock block = new StringBlock(content);
+
+ Pattern rule = AdPatternMap.get(replaceRule);
+ String stringDict = AdStringDict.get(replaceRule);
+
+
+ if (rule == null) {
+ StringBuffer bufferRegex = new StringBuffer(replaceRule.length() * 3);
+ StringBuffer bufferDict = new StringBuffer();
+
+ String rules[] = replaceRule.split("\n");
+
+ for (String s : rules) {
+ s = s.trim();
+ if (s.length() < 1)
+ continue;
+
+ s = Pattern.quote(s);
+
+ if (bufferRegex.length() > 0)
+ bufferRegex.append('|');
+ else
+ bufferRegex.append("(?=(");
+ bufferRegex.append(s);
+
+ }
+
+ for (String s : rules) {
+ s = s.trim();
+ if (s.length() < 1)
+ continue;
+
+ // 如果规则不止包含特殊字符,需要移除首尾的特殊字符,把中间的空字符转换为\s+,把其他特殊字符转换为转义符
+ if (!s.matches("[\\p{P}\\s]*")) {
+ if (bufferRegex.length() > 0)
+ bufferRegex.append('|');
+ else
+ bufferRegex.append("(?=(");
+ bufferRegex.append(s
+ .replaceFirst("^\\p{P}+", "")
+ .replaceFirst("\\p{P}$", "")
+ .replaceAll("\\s+", "xxsp")
+ .replaceAll("(\\p{P})", "(\\\\p{P}?)")
+ .replaceAll("xxsp", "\\s+")
+ );
+ }
+ if (s.matches("[\\p{P}\\s]*[^\\p{P}]{4,}[\\p{P}\\s]*")) {
+ bufferDict.append('\n');
+ bufferDict.append(s);
+ }
+ }
+ bufferRegex.append("))((\\p{P}{0,12})(?=\\p{P}{2}))?");
+ rule = Pattern.compile(bufferRegex.toString());
+ AdPatternMap.put(replaceRule, rule);
+ stringDict = bufferDict.toString();
+ AdStringDict.put(replaceRule, bufferDict.toString());
+ }
+
+ Matcher matcher0 = rule.matcher(content);
+ if (matcher0.groupCount() < 2) {
+// 构造的正则表达式分2个部分,第一部分匹配文字,第二部分匹配符号。完成匹配后实际已经不需要拆墙了
+ Log.w("replaceAd2", "2 > matcher0.group()==" + matcher0.groupCount());
+ return content;
+ }
+
+ while (matcher0.find()) {
+
+ if (matcher0.group(2) != null)
+ block.remove(matcher0.start(), matcher0.start() + matcher0.group(1).length() + matcher0.group(2).length());
+ else
+ block.remove(matcher0.start(), matcher0.start() + matcher0.group(1).length());
+
+ Log.d("replaceAd2()", "Remove=" + block.verify());
+ }
+ block.remove("(\\p{P}|\\s){1,6}([^\\p{P}]?(\\p{P}|\\s){1,6})?");
+ block.removeDict(stringDict);
+ block.increase(5);
+ return block.getResult();
+ }
+
+
+ class StringBlock {
+ // 保存字符串本体
+ private String string = "";
+ // 保存可以复制的区域,奇数为start,偶数为end。
+ private ArrayList list;
+ // 保存删除的区域,用于校验
+ private ArrayList removed;
+
+ public StringBlock(String string) {
+ this.string = string;
+ list = new ArrayList<>();
+ list.add(0);
+ list.add(string.length());
+ removed = new ArrayList<>();
+ }
+
+ // 验证删除操作是否有bug 验证OK输出正数,异常输出负数
+ public int verify() {
+ // 验证list数列是否有异常
+ if (list.size() % 2 != 0)
+ return -1;
+
+ int p = list.get(0);
+ if (p < 0)
+ return -2;
+ for (int i = 1; i < list.size(); i++) {
+ int q = list.get(i);
+ if (q <= p)
+ return -3;
+ p = q;
+ }
+ // 验证删除的区域是否还在list构成的区域内
+ for (int j = 0; j < removed.size() / 2; j++) {
+ int j2 = removed.get(j * 2);
+ int j2_1 = removed.get(j * 2 + 1);
+ for (int i = 0; i < list.size() / 2; i++) {
+ int i2_1 = list.get(i * 2 + 1);
+ int i2 = list.get(i * 2);
+ if (i2 > j2) {
+ break;
+ }
+ if (i2_1 < j2) {
+ continue;
+ }
+
+ if (i2_1 == j2) {
+ if (i * 2 + 2 < list.size()) {
+ if (list.get(i * 2 + 2) < j2_1)
+ return -4;
+ }
+ } else {
+ return -5;
+ }
+
+ }
+ }
+
+ return 0;
+ }
+
+ // 增加字符串的文本,避免被误删除
+ public void increase(int size) {
+ ArrayList cache = new ArrayList<>();
+ if (list.get(0) > size)
+ cache.add(list.get(0));
+ else
+ cache.add(0);
+ for (int i = 1; i < list.size() - 1; i = i + 2) {
+ if (list.get(i + 1) - list.get(i) > size) {
+ cache.add(list.get(i));
+ cache.add(list.get(i + 1));
+ }
+ }
+ if (string.length() - list.get(list.size() - 1) > size)
+ cache.add(list.get(list.size() - 1));
+ else
+ cache.add(string.length());
+ list = cache;
+ }
+
+ // 去除长度小于等于墙厚的区域
+ public void remove(int wallThick) {
+ int j = list.size() / 2;
+ ArrayList cache = new ArrayList<>();
+ for (int i = 0; i < j; i++) {
+ int i2_1 = list.get(i * 2 + 1);
+ int i2 = list.get(i * 2);
+ if ((i2_1 - i2) > wallThick) {
+ cache.add(i2);
+ cache.add(i2_1);
+ }
+ }
+ list = cache;
+ }
+
+ // 去除完全与正则匹配的区域
+ public void remove(String wall) {
+ int j = list.size() / 2;
+ ArrayList cache = new ArrayList<>();
+ for (int i = 0; i < j; i++) {
+ int i2_1 = list.get(i * 2 + 1);
+ int i2 = list.get(i * 2);
+ if (!string.substring(i2, i2_1).matches(wall)) {
+ cache.add(i2);
+ cache.add(i2_1);
+ }
+ }
+ list = cache;
+ }
+
+ public void removeDict(String dict) {
+// 如果孔穴的两端刚好匹配到同一词条,说明这是嵌套的广告话术
+ int j = list.size() / 2;
+ // 缓存需要操作的参数
+ ArrayList cache = new ArrayList<>();
+ for (int i = 1; i < j; i++) {
+
+ String str_s0 = getSubString(2 * i - 2).replaceFirst("[\\p{P}\\s]+$", "");
+
+ String str_s1 = str_s0.replaceFirst("^.*[\\p{P}\\s][^$]", "");
+ if (str_s1.length() < 1)
+ continue;
+
+ String str_e0 = getSubString(2 * i).replaceFirst("^[\\p{P}\\s]+", "");
+ String str_e1 = str_e0.replaceFirst("[\\p{P}\\s].*$", "");
+ if (str_e1.length() < 1)
+ continue;
+
+
+ // m 第一部分开始的位置
+ int m = list.get(i * 2 - 2) + str_s0.length() - str_s1.length();
+ // 第二部分结尾
+ int n = list.get(i * 2 + 1) - str_e0.length() + str_e1.length();
+
+ if (dict.matches("[\\s\\S]*(" + str_s1 + ")([^\\p{P}]*)(" + str_e1 + ")[\\s\\S]*")) {
+ cache.add(m);
+ cache.add(n);
+ } else if (dict.matches("[\\s\\S]*(\n|^).*" + str_s1 + ".*(\n|\\s*$)[\\s\\S]*")) {
+ cache.add(m);
+ cache.add(list.get(i * 2));
+ } else if (dict.matches("[\\s\\S]*(\n|^).*" + str_e1 + ".*(\n|\\s*$)[\\s\\S]*")) {
+ // 因为java.*不匹配\n
+ cache.add(list.get(i * 2));
+ cache.add(n);
+ }
+ }
+
+ for (int i = 0; i < cache.size() / 2; i++) {
+ Log.d("removeDict", string.substring(cache.get(i * 2), cache.get((i * 2 + 1))));
+ remove(cache.get(i * 2), cache.get((i * 2 + 1)));
+ }
+
+ }
+
+ public boolean remove(int start, int end) {
+ if (start < 0 || end < 0 || start > string.length() || end > string.length() || start >= end)
+ return false;
+
+ removed.add(start);
+ removed.add(end);
+
+ int j = list.size() / 2;
+ for (int i = 0; i < j; i++) {
+// start在有效区间中间和在区间的两个边缘,是不同的算法。
+ int i2_1 = list.get(i * 2 + 1);
+ int i2 = list.get(i * 2);
+
+ if (start < i2)
+ return true;
+
+ if (start == i2) {
+ if (i2_1 > end) {
+ list.set(i * 2, end);
+ return true;
+ } else {
+ for (int k = 0; 2 * i + k < list.size(); k++) {
+ if (list.get(k + 2 * i) > end) {
+ if (k % 2 == 1) {
+ list.set(2 * i + k - 1, end);
+ } else {
+ list.remove(i * 2);
+ }
+ for (int m = 0; m < k - 1; m++)
+ list.remove(i * 2);
+ return true;
+ }
+ }
+ }
+ } else if (i2 < start && i2_1 > start) {
+ if (i2_1 > end) {
+ list.add(i * 2 + 1, end);
+ list.add(i * 2 + 1, start);
+ return true;
+ } else {
+ list.set(i * 2 + 1, start);
+ // i*2+2开始的元素可能需要被删除
+ for (int k = 2; 2 * i + k < list.size(); k++) {
+ if (list.get(k + 2 * i) < end)
+ continue;
+
+ if (k % 2 == 1) {
+ if (list.get(k + 2 * i) > end) {
+ list.set(2 * i + k - 1, end);
+ }
+ } else {
+ list.remove(i * 2 + 2);
+ }
+
+ for (int m = 0; m < k - 1; m++)
+ list.remove(i * 2 + 2);
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+
+ }
+
+ public String getResult() {
+ StringBuffer buffer = new StringBuffer(string.length());
+ int j = list.size() / 2;
+ if (j * 2 > list.size())
+ Log.e("StringBlock", "list.size=" + list.size());
+ for (int i = 0; i < j; i++) {
+ buffer.append(string, list.get(i * 2), list.get(i * 2 + 1));
+ }
+ return buffer.toString();
+ }
+
+ public String getSubString(int start) {
+ if (start >= 0 && start < list.size() - 1)
+ return string.substring(list.get(start), list.get(start + 1));
+ return null;
+ }
+ }
+
+ /**
+ * 段落重排算法入口。把整篇内容输入,连接错误的分段,再把每个段落调用其他方法重新切分
+ *
+ * @param content 正文
+ * @param chapterName 标题
+ * @return
+ */
+ public static String LightNovelParagraph2(String content, String chapterName) {
+ if (SysManager.getSetting().isLightNovelParagraph()) {
+ String _content;
+ int chapterNameLength = chapterName.trim().length();
+ if (chapterNameLength > 1) {
+ String regexp = chapterName.trim().replaceAll("\\s+", "(\\\\s*)");
+// 质量较低的页面,章节内可能重复出现章节标题
+ if (chapterNameLength > 5)
+ _content = content.replaceAll(regexp, "").trim();
+ else
+ _content = content.replaceFirst("^\\s*" + regexp, "").trim();
+ } else {
+ _content = content;
+ }
+
+ List dict = makeDict(_content);
+
+ String[] p = _content
+ .replaceAll(""", "“")
+ .replaceAll("[::]['\"‘”“]+", ":“")
+ .replaceAll("[\"”“]+[\\s]*[\"”“][\\s\"”“]*", "”\n“")
+ .split("\n(\\s*)");
+
+// 初始化StringBuffer的长度,在原content的长度基础上做冗余
+ StringBuffer buffer = new StringBuffer((int) (content.length() * 1.15));
+// 章节的文本格式为章节标题-空行-首段,所以处理段落时需要略过第一行文本。
+ buffer.append(" ");
+
+ if (!chapterName.trim().equals(p[0].trim())) {
+ // 去除段落内空格。unicode 3000 象形字间隔(中日韩符号和标点),不包含在\s内
+ buffer.append(p[0].replaceAll("[\u3000\\s]+", ""));
+ }
+
+// 如果原文存在分段错误,需要把段落重新黏合
+ for (int i = 1; i < p.length; i++) {
+ if (match(MARK_SENTENCES_END, buffer.charAt(buffer.length() - 1)))
+ buffer.append("\n");
+// 段落开头以外的地方不应该有空格
+ // 去除段落内空格。unicode 3000 象形字间隔(中日韩符号和标点),不包含在\s内
+ buffer.append(p[i].replaceAll("[\u3000\\s]", ""));
+
+ }
+ // 预分段预处理
+ // ”“处理为”\n“。
+ // ”。“处理为”。\n“。不考虑“?” “!”的情况。
+// ”。xxx处理为 ”。\n xxx
+ p = buffer.toString()
+ .replaceAll("[\"”“]+[\\s]*[\"”“]+", "”\n“")
+ .replaceAll("[\"”“]+(?。!?!~)[\"”“]+", "”$1\n“")
+ .replaceAll("[\"”“]+(?。!?!~)([^\"”“])", "”$1\n$2")
+ .replaceAll("([问说喊唱叫骂道着答])[\\.。]", "$1。\n")
+// .replaceAll("([\\.。\\!!??])([^\"”“]+)[::][\"”“]", "$1\n$2:“")
+ .split("\n");
+
+ buffer = new StringBuffer((int) (content.length() * 1.15));
+
+ for (String s : p) {
+ buffer.append("\n");
+ buffer.append(FindNewLines(s, dict)
+ );
+ }
+
+ buffer = reduceLength(buffer);
+
+ content = chapterName + "\n\n"
+ + buffer.toString()
+ //处理章节头部空格和换行
+ .replaceFirst("^\\s+", "")
+ // 此规则会造成不规范引号被误换行,暂时无法解决,我认为利大于弊
+ // 例句:“你”“我”“他”都是一样的
+ // 误处理为 “你”\n“我”\n“他”都是一样的
+ // 而规范使用的标点不会被误处理: “你”、“我”、“他”,都是一样的。
+ .replaceAll("\\s*[\"”“]+[\\s]*[\"”“][\\s\"”“]*", "”\n“")
+ // 规范 A:“B...
+ .replaceAll("[::][”“\"\\s]+", ":“")
+ // 处理奇怪的多余引号 \n”A:“B... 为 \nA:“B...
+ .replaceAll("\n[\"“”]([^\n\"“”]+)([,:,:][\"”“])([^\n\"“”]+)", "\n$1:“$3")
+ .replaceAll("\n(\\s*)", "\n")
+ // 处理“……”
+// .replaceAll("\n[\"”“][.,。,…]+\\s*[.,。,…]+[\"”“]","\n“……”")
+ // 处理被错误断行的省略号。存在较高的误判,但是我认为利大于弊
+ .replaceAll("[.,。,…]+\\s*[.,。,…]+", "……")
+ .replaceAll("\n([\\s::,,]+)", "\n")
+ ;
+ }
+ return content;
+ }
+
+ /**
+ * 从字符串提取引号包围,且不止出现一次的内容为字典
+ *
+ * @param str
+ * @return 词条列表
+ */
+ private static List makeDict(String str) {
+
+ // 引号中间不包含任何标点,但是没有排除空格
+ Pattern patten = Pattern.compile("(?<=[\"'”“])([^\n\\p{P}]{1," + WORD_MAX_LENGTH + "})(?=[\"'”“])");
+ Matcher matcher = patten.matcher(str);
+
+ List cache = new ArrayList<>();
+ List dict = new ArrayList<>();
+ List groups = new ArrayList<>();
+
+ while (matcher.find()) {
+ String word = matcher.group();
+ String w = word.replaceAll("\\s+", "");
+ if (!groups.contains(word))
+ groups.add(word);
+ if (!groups.contains(w))
+ groups.add(w);
+ }
+
+ for (String word : groups) {
+ String w = word.replaceAll("\\s+", "");
+ if (cache.contains(w)) {
+ if (!dict.contains(w)) {
+ dict.add(w);
+ if (!dict.contains(word))
+ dict.add(word);
+ }
+ } else {
+ cache.add(w);
+ cache.add(word);
+ }
+ }
+/*
+ System.out.print("makeDict:");
+ for (String s : dict)
+ System.out.print("\t" + s);
+ System.out.print("\n");
+ */
+ return dict;
+ }
+
+ /**
+ * 强制切分,减少段落内的句子
+ * 如果连续2对引号的段落没有提示语,进入对话模式。最后一对引号后强制切分段落
+ * 如果引号内的内容长于5句,可能引号状态有误,随机分段
+ * 如果引号外的内容长于3句,随机分段
+ *
+ * @param str
+ * @return
+ */
+ private static StringBuffer reduceLength(StringBuffer str) {
+ String[] p = str.toString().split("\n");
+ int l = p.length;
+ boolean[] b = new boolean[l];
+
+ for (int i = 0; i < l; i++) {
+ if (p[i].matches(PARAGRAPH_DIAGLOG))
+ b[i] = true;
+ else
+ b[i] = false;
+ }
+
+ int dialogue = 0;
+
+ for (int i = 0; i < l; i++) {
+ if (b[i]) {
+ if (dialogue < 0)
+ dialogue = 1;
+ else if (dialogue < 2)
+ dialogue++;
+ } else {
+ if (dialogue > 1) {
+ p[i] = splitQuote(p[i]);
+ dialogue--;
+ } else if (dialogue > 0 && i < l - 2) {
+ if (b[i + 1])
+ p[i] = splitQuote(p[i]);
+ }
+ }
+ }
+
+ StringBuffer string = new StringBuffer();
+ for (int i = 0; i < l; i++) {
+ string.append('\n');
+ string.append(p[i]);
+// System.out.print(" "+b[i]);
+ }
+// System.out.println(" " + str);
+ return string;
+ }
+
+ // 强制切分进入对话模式后,未构成 “xxx” 形式的段落
+ private static String splitQuote(String str) {
+// System.out.println("splitQuote() " + str);
+ int length = str.length();
+ if (length < 3)
+ return str;
+ if (match(MARK_QUOTATION, str.charAt(0))) {
+ int i = seekIndex(str, MARK_QUOTATION, 1, length - 2, true) + 1;
+ if (i > 1)
+ if (!match(MARK_QUOTATION_BEFORE, str.charAt(i - 1)))
+ return str.substring(0, i) + "\n" + str.substring(i);
+ } else if (match(MARK_QUOTATION, str.charAt(length - 1))) {
+ int i = length - 1 - seekIndex(str, MARK_QUOTATION, 1, length - 2, false);
+ if (i > 1)
+ if (!match(MARK_QUOTATION_BEFORE, str.charAt(i - 1)))
+ return str.substring(0, i) + "\n" + str.substring(i);
+ }
+ return str;
+ }
+
+ /**
+ * 计算随机插入换行符的位置。
+ *
+ * @param str 字符串
+ * @param offset 传回的结果需要叠加的偏移量
+ * @param min 最低几个句子,随机插入换行
+ * @param gain 倍率。每个句子插入换行的数学期望 = 1 / gain , gain越大越不容易插入换行
+ * @return
+ */
+ private static ArrayList forceSplit(String str, int offset, int min, int gain, int tigger) {
+ ArrayList result = new ArrayList<>();
+ ArrayList array_end = seekIndexs(str, MARK_SENTENCES_END_P, 0, str.length() - 2, true);
+ ArrayList array_mid = seekIndexs(str, MARK_SENTENCES_MID, 0, str.length() - 2, true);
+ if (array_end.size() < tigger && array_mid.size() < tigger * 3)
+ return result;
+ int j = 0;
+ for (int i = min; i < array_end.size(); i++) {
+ int k = 0;
+ for (; j < array_mid.size(); j++) {
+ if (array_mid.get(j) < array_end.get(i))
+ k++;
+ }
+ if (Math.random() * gain < (0.8 + k / 2.5)) {
+ result.add(array_end.get(i) + offset);
+ i = Math.max(i + min, i);
+ }
+ }
+ return result;
+ }
+
+ // 对内容重新划分段落.输入参数str已经使用换行符预分割
+ private static String FindNewLines(String str, List dict) {
+ StringBuffer string = new StringBuffer(str);
+ // 标记string中每个引号的位置.特别的,用引号进行列举时视为只有一对引号。 如:“锅”、“碗”视为“锅、碗”,从而避免误断句。
+ List array_quote = new ArrayList<>();
+ // 标记忽略的引号
+ List array_ignore_quote = new ArrayList<>();
+ // 标记插入换行符的位置,int为插入位置(str的char下标)
+ ArrayList ins_n = new ArrayList<>();
+ // 标记不需要插入换行符的位置。功能暂未实现。
+ ArrayList remove_n = new ArrayList<>();
+
+// mod[i]标记str的每一段处于引号内还是引号外。范围: str.substring( array_quote.get(i), array_quote.get(i+1) )的状态。
+// 长度:array_quote.size(),但是初始化时未预估占用的长度,用空间换时间
+// 0未知,正数引号内,负数引号外。
+// 如果相邻的两个标记都为+1,那么需要增加1个引号。
+// 引号内不进行断句
+ int[] mod = new int[str.length()];
+ boolean wait_close = false;
+
+ for (int i = 0; i < str.length(); i++) {
+ char c = str.charAt(i);
+ if (match(MARK_QUOTATION, c)) {
+ int size = array_quote.size();
+
+ // 把“xxx”、“yy”和“z”合并为“xxx_yy_z”进行处理
+ if (size > 0) {
+ int quote_pre = array_quote.get(size - 1);
+ if (i - quote_pre == 2) {
+ boolean remove = false;
+ if (wait_close) {
+ if (match(",,、/", str.charAt(i - 1))) {
+ // 考虑出现“和”这种特殊情况
+ remove = true;
+ }
+ } else if (match(",,、/和与或", str.charAt(i - 1))) {
+ remove = true;
+ }
+ if (remove) {
+ string.setCharAt(i, '“');
+ string.setCharAt(i - 2, '”');
+ array_quote.remove(size - 1);
+ mod[size - 1] = 1;
+ mod[size] = -1;
+ continue;
+ }
+ }
+ }
+ array_quote.add(i);
+
+ // 为xxx:“xxx”做标记
+ if (i > 1) {
+ // 当前发言的正引号的前一个字符
+ char char_b1 = str.charAt(i - 1);
+ // 上次发言的正引号的前一个字符
+ char char_b2 = 0;
+ if (match(MARK_QUOTATION_BEFORE, char_b1)) {
+ // 如果不是第一处引号,寻找上一处断句,进行分段
+ if (array_quote.size() > 1) {
+ int last_quote = array_quote.get(array_quote.size() - 2);
+ int p = 0;
+ if (char_b1 == ',' || char_b1 == ',') {
+ if (array_quote.size() > 2) {
+ p = array_quote.get(array_quote.size() - 3);
+ if (p > 0) {
+ char_b2 = str.charAt(p - 1);
+ }
+ }
+ }
+// if(char_b2=='.' || char_b2=='。')
+ if (match(MARK_SENTENCES_END_P, char_b2))
+ ins_n.add(p - 1);
+ else if (match("的", char_b2)) {
+ //剔除引号标记aaa的"xxs",bbb的“yyy”
+
+ } else {
+ int last_end = seekLast(str, MARK_SENTENCES_END, i, last_quote);
+ if (last_end > 0)
+ ins_n.add(last_end);
+ else
+ ins_n.add(last_quote);
+ }
+ }
+
+ wait_close = true;
+ mod[size] = 1;
+ if (size > 0) {
+ mod[size - 1] = -1;
+ if (size > 1) {
+ mod[size - 2] = 1;
+ }
+
+/*
+ int quote_pre = array_quote.get(array_quote.size() - 2);
+ boolean flag_ins_n = false;
+ for (int j = i; j > quote_pre; j--) {
+ if (match(MARK_SENTENCES_END, string.charAt(j))) {
+ ins_n.add(j);
+ flag_ins_n = true;
+ }
+ }
+ if (!flag_ins_n)
+ ins_n.add(quote_pre);
+ */
+ }
+ } else if (wait_close) {
+ {
+ wait_close = false;
+ ins_n.add(i);
+ }
+ }
+ }
+
+ }
+ }
+
+ int size = array_quote.size();
+
+
+// 标记循环状态,此位置前的引号是否已经配对
+ boolean opend = false;
+ if (size > 0) {
+
+// 第1次遍历array_quote,令其元素的值不为0
+ for (int i = 0; i < size; i++) {
+ if (mod[i] > 0) {
+ opend = true;
+ } else if (mod[i] < 0) {
+// 连续2个反引号表明存在冲突,强制把前一个设为正引号
+ if (!opend) {
+ if (i > 0)
+ mod[i] = 3;
+ }
+ opend = false;
+ } else {
+ opend = !opend;
+ if (opend)
+ mod[i] = 2;
+ else
+ mod[i] = -2;
+ }
+ }
+// 修正,断尾必须封闭引号
+ if (opend) {
+ if (array_quote.get(size - 1) - string.length() > -3) {
+// if((match(MARK_QUOTATION,string.charAt(string.length()-1)) || match(MARK_QUOTATION,string.charAt(string.length()-2)))){
+ if (size > 1)
+ mod[size - 2] = 4;
+ // 0<=i=1
+ mod[size - 1] = -4;
+ } else if (!match(MARK_SENTENCES_SAY, string.charAt(string.length() - 2)))
+ string.append("”");
+ }
+
+
+// 第2次循环,mod[i]由负变正时,前1字符如果是句末,需要插入换行
+ int loop2_mod_1 = -1; //上一个引号跟随内容的状态
+ int loop2_mod_2; //当前引号跟随内容的状态
+ int i = 0;
+ int j = array_quote.get(0) - 1; //当前引号前一字符的序号
+ if (j < 0) {
+ i = 1;
+ loop2_mod_1 = 0;
+ }
+
+ for (; i < size; i++) {
+ j = array_quote.get(i) - 1;
+ loop2_mod_2 = mod[i];
+ if (loop2_mod_1 < 0 && loop2_mod_2 > 0) {
+ if (match(MARK_SENTENCES_END, string.charAt(j)))
+ ins_n.add(j);
+ }
+/* else if (mod[i - 1] > 0 && mod[i] < 0) {
+ if (j > 0) {
+ if (match(MARK_SENTENCES_END, string.charAt(j)))
+ ins_n.add(j);
+ }
+ }
+*/
+ loop2_mod_1 = loop2_mod_2;
+ }
+ }
+
+// 第3次循环,匹配并插入换行。
+// "xxxx" xxxx。\n xxx“xxxx”
+// 未实现
+
+
+ // 使用字典验证ins_n , 避免插入不必要的换行。
+ // 由于目前没有插入、的列表,无法解决 “xx”、“xx”“xx” 被插入换行的问题
+ ArrayList _ins_n = new ArrayList<>();
+ for (int i : ins_n) {
+ if (match("\"'”“", string.charAt(i))) {
+ int start = seekLast(str, "\"'”“", i - 1, i - WORD_MAX_LENGTH);
+ if (start > 0) {
+ String word = str.substring(start + 1, i);
+
+ if (dict.contains(word)) {
+// System.out.println("使用字典验证 跳过\tins_n=" + i + " word=" + word);
+// 引号内如果是字典词条,后方不插入换行符(前方不需要优化)
+ remove_n.add(i);
+ continue;
+ } else {
+ System.out.println("使用字典验证 插入\tins_n=" + i + " word=" + word);
+ if (match("的地得和或", str.charAt(start))) {
+// xx的“xx”,后方不插入换行符(前方不需要优化)
+ continue;
+ }
+
+ }
+ }
+ } else {
+ // System.out.println("使用字典验证 else\tins_n=" + i + " substring=" + string.substring(i-5,i+5));
+
+ }
+ _ins_n.add(i);
+ }
+ ins_n = _ins_n;
+
+
+// 随机在句末插入换行符
+ ins_n = new ArrayList(new HashSet(ins_n));
+ Collections.sort(ins_n);
+
+
+ {
+ String subs = "";
+ int j = 0;
+ int progress = 0;
+
+ int next_line = -1;
+ if (ins_n.size() > 0)
+ next_line = ins_n.get(j);
+
+ int gain = 3;
+ int min = 0;
+ int trigger = 2;
+
+ for (int i = 0; i < array_quote.size(); i++) {
+ int qutoe = array_quote.get(i);
+ if (qutoe > 0) {
+ gain = 4;
+ min = 2;
+ trigger = 4;
+ } else {
+ gain = 3;
+ min = 0;
+ trigger = 2;
+ }
+
+// 把引号前的换行符与内容相间插入
+ for (; j < ins_n.size(); j++) {
+// 如果下一个换行符在当前引号前,那么需要此次处理.如果紧挨当前引号,需要考虑插入引号的情况
+ if (next_line >= qutoe)
+ break;
+ next_line = ins_n.get(j);
+ if (progress < next_line) {
+ subs = string.substring(progress, next_line);
+ ins_n.addAll(forceSplit(subs, progress, min, gain, trigger));
+ progress = next_line + 1;
+ }
+ }
+ if (progress < qutoe) {
+ subs = string.substring(progress, qutoe + 1);
+ ins_n.addAll(forceSplit(subs, progress, min, gain, trigger));
+ progress = qutoe + 1;
+ }
+ }
+
+
+ for (; j < ins_n.size(); j++) {
+ next_line = ins_n.get(j);
+ if (progress < next_line) {
+ subs = string.substring(progress, next_line);
+ ins_n.addAll(forceSplit(subs, progress, min, gain, trigger));
+ progress = next_line + 1;
+ }
+ }
+
+ if (progress < string.length()) {
+ subs = string.substring(progress, string.length());
+ ins_n.addAll(forceSplit(subs, progress, min, gain, trigger));
+ }
+
+ }
+
+// 根据段落状态修正引号方向、计算需要插入引号的位置
+// ins_quote跟随array_quote ins_quote[i]!=0,则array_quote.get(i)的引号前需要前插入'”'
+ boolean[] ins_quote = new boolean[size];
+ opend = false;
+ for (int i = 0; i < size; i++) {
+ int p = array_quote.get(i);
+ if (mod[i] > 0) {
+ string.setCharAt(p, '“');
+ if (opend)
+ ins_quote[i] = true;
+ opend = true;
+ } else if (mod[i] < 0) {
+ string.setCharAt(p, '”');
+ opend = false;
+ } else {
+ opend = !opend;
+ if (opend)
+ string.setCharAt(p, '“');
+ else
+ string.setCharAt(p, '”');
+ }
+ }
+
+ ins_n = new ArrayList(new HashSet(ins_n));
+ Collections.sort(ins_n);
+
+// 输出log进行检验
+/*
+ System.out.println("quote[i]:position/mod\t" + string);
+ for (int i = 0; i < array_quote.size(); i++) {
+ System.out.print(" [" + i + "]" + array_quote.get(i) + "/" + mod[i]);
+ }
+ System.out.print("\n");
+
+ System.out.print("ins_q:");
+ for (int i = 0; i < ins_quote.length; i++) {
+ System.out.print(" " + ins_quote[i]);
+ }
+ System.out.print("\n");
+
+ System.out.print("ins_n:");
+
+ for (int i : ins_n) {
+ System.out.print(" " + i);
+ }
+ System.out.print("\n");
+*/
+
+// 完成字符串拼接(从string复制、插入引号和换行
+// ins_quote 在引号前插入一个引号。 ins_quote[i]!=0,则array_quote.get(i)的引号前需要前插入'”'
+// ins_n 插入换行。数组的值表示插入换行符的位置
+ StringBuffer buffer = new StringBuffer((int) (str.length() * 1.15));
+
+ int j = 0;
+ int progress = 0;
+
+ int next_line = -1;
+ if (ins_n.size() > 0)
+ next_line = ins_n.get(j);
+
+ for (int i = 0; i < array_quote.size(); i++) {
+ int qutoe = array_quote.get(i);
+
+// 把引号前的换行符与内容相间插入
+ for (; j < ins_n.size(); j++) {
+// 如果下一个换行符在当前引号前,那么需要此次处理.如果紧挨当前引号,需要考虑插入引号的情况
+ if (next_line >= qutoe)
+ break;
+ next_line = ins_n.get(j);
+ buffer.append(string, progress, next_line + 1);
+ buffer.append('\n');
+ progress = next_line + 1;
+ }
+ if (progress < qutoe) {
+ buffer.append(string, progress, qutoe + 1);
+ progress = qutoe + 1;
+ }
+ if (ins_quote[i] && buffer.length() > 2) {
+ if (buffer.charAt(buffer.length() - 1) == '\n')
+ buffer.append('“');
+ else
+ buffer.insert(buffer.length() - 1, "”\n");
+ }
+ }
+
+ for (; j < ins_n.size(); j++) {
+ next_line = ins_n.get(j);
+ if (progress <= next_line) {
+ buffer.append(string, progress, next_line + 1);
+ buffer.append('\n');
+ progress = next_line + 1;
+ }
+ }
+
+ if (progress < string.length()) {
+ buffer.append(string, progress, string.length());
+ }
+
+ return buffer.toString();
+ }
+
+ /**
+ * 计算匹配到字典的每个字符的位置
+ *
+ * @param str 待匹配的字符串
+ * @param key 字典
+ * @param from 从字符串的第几个字符开始匹配
+ * @param to 匹配到第几个字符结束
+ * @param inOrder 是否按照从前向后的顺序匹配
+ * @return 返回距离构成的ArrayList
+ */
+ private static ArrayList seekIndexs(String str, String key, int from, int to, boolean inOrder) {
+ ArrayList list = new ArrayList<>();
+
+ if (str.length() - from < 1)
+ return list;
+ int i = 0;
+ if (from > i)
+ i = from;
+ int t = str.length();
+ if (to > 0)
+ t = Math.min(t, to);
+ char c;
+ for (; i < t; i++) {
+ if (inOrder)
+ c = str.charAt(i);
+ else
+ c = str.charAt(str.length() - i - 1);
+ if (key.indexOf(c) != -1) {
+ list.add(i);
+ }
+ }
+ return list;
+ }
+
+
+ /**
+ * 计算字符串最后出现与字典中字符匹配的位置
+ *
+ * @param str 数据字符串
+ * @param key 字典字符串
+ * @param from 从哪个字符开始匹配,默认最末位
+ * @param to 匹配到哪个字符(不包含此字符)默认0
+ * @return 位置(正向计算)
+ */
+ private static int seekLast(String str, String key, int from, int to) {
+ if (str.length() - from < 1)
+ return -1;
+ int i = str.length() - 1;
+ if (from < i && i > 0)
+ i = from;
+ int t = 0;
+ if (to > 0)
+ t = to;
+ char c;
+ for (; i > t; i--) {
+ c = str.charAt(i);
+ if (key.indexOf(c) != -1) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * 计算字符串与字典中字符的最短距离
+ *
+ * @param str 数据字符串
+ * @param key 字典字符串
+ * @param from 从哪个字符开始匹配,默认0
+ * @param to 匹配到哪个字符(不包含此字符)默认匹配到最末位
+ * @param inOrder 是否从正向开始匹配
+ * @return 返回最短距离, 注意不是str的char的下标
+ */
+ private static int seekIndex(String str, String key, int from, int to, boolean inOrder) {
+ if (str.length() - from < 1)
+ return -1;
+ int i = 0;
+ if (from > i)
+ i = from;
+ int t = str.length();
+ if (to > 0)
+ t = Math.min(t, to);
+ char c;
+ for (; i < t; i++) {
+ if (inOrder)
+ c = str.charAt(i);
+ else
+ c = str.charAt(str.length() - i - 1);
+ if (key.indexOf(c) != -1) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * 计算字符串与字典的距离。
+ *
+ * @param str 数据字符串
+ * @param form 从第几个字符开始匹配
+ * @param to 匹配到第几个字符串结束
+ * @param inOrder 是否从前向后匹配。
+ * @param words 可变长参数构成的字典。每个字符串代表一个字符
+ * @return 匹配结果。注意这个距离是使用第一个字符进行计算的
+ */
+ private static int seekWordsIndex(String str, int form, int to, boolean inOrder, String... words) {
+
+ if (words.length < 1)
+ return -2;
+
+ int i = seekIndex(str, words[0], form, to, inOrder);
+ if (i < 0)
+ return i;
+
+ for (int j = 1; j < words.length; j++) {
+ int k = seekIndex(str, words[j], form, to, inOrder);
+ if (inOrder) {
+ if (i + j != k)
+ return -3;
+ } else {
+ if (i - j != k)
+ return -3;
+ }
+ }
+ return i;
+ }
+
+ /* 搜寻引号并进行分段。处理了一、二、五三类常见情况
+ 参照百科词条[引号#应用示例](https://baike.baidu.com/item/%E5%BC%95%E5%8F%B7/998963?#5)对引号内容进行矫正并分句。
+ 一、完整引用说话内容,在反引号内侧有断句标点。例如:
+ 1) 丫姑折断几枝扔下来,边叫我的小名儿边说:“先喂饱你!”
+ 2)“哎呀,真是美极了!”皇帝说,“我十分满意!”
+ 3)“怕什么!海的美就在这里!”我说道。
+ 二、部分引用,在反引号外侧有断句标点:
+ 4)适当地改善自己的生活,岂但“你管得着吗”,而且是顺乎天理,合乎人情的。
+ 5)现代画家徐悲鸿笔下的马,正如有的评论家所说的那样,“形神兼备,充满生机”。
+ 6)唐朝的张嘉贞说它“制造奇特,人不知其所为”。
+ 三、一段接着一段地直接引用时,中间段落只在段首用起引号,该段段尾却不用引回号。但是正统文学不在考虑范围内。
+ 四、引号里面又要用引号时,外面一层用双引号,里面一层用单引号。暂时不需要考虑
+ 五、反语和强调,周围没有断句符号。
+*/
+
+ // 段落换行符
+ private static String SPACE_BEFORE_PARAGRAPH = "\n ";
+ // 段落末位的标点
+ private static String MARK_SENTENCES = "?。!?!~”\"";
+ // 句子结尾的标点。因为引号可能存在误判,不包含引号。
+ private static String MARK_SENTENCES_END = "?。!?!~";
+ private static String MARK_SENTENCES_END_P = ".?。!?!~";
+ // 句中标点,由于某些网站常把“,”写为".",故英文句点按照句中标点判断
+ private static String MARK_SENTENCES_MID = ".,、,—…";
+ private static String MARK_SENTENCES_F = "啊嘛吧吗噢哦了呢呐";
+ private static String MARK_SENTENCES_SAY = "问说喊唱叫骂道着答";
+ // XXX说:“”的冒号
+ private static String MARK_QUOTATION_BEFORE = ",:,:";
+ // 引号
+ private static String MARK_QUOTATION = "\"“”";
+
+ private static String PARAGRAPH_DIAGLOG = "^[\"”“][^\"”“]+[\"”“]$";
+ // 限制字典的长度
+ private static int WORD_MAX_LENGTH = 16;
+
+ private static boolean isFullSentences(String s) {
+ if (s.length() < 2)
+ return false;
+ char c = s.charAt(s.length() - 1);
+ return MARK_SENTENCES.indexOf(c) != -1;
+ }
+
+ private static boolean match(String rule, char chr) {
+ return rule.indexOf(chr) != -1;
+ }
+
+
+ private boolean isUseTo(String useTo, String bookTag, String bookSource) {
+ String[] useTos = useTo.split(";");
+ return useTo.length() <= 1
+ || TextUtils.isEmpty(useTos[0])
+ || TextUtils.isEmpty(useTos[1])
|| useTo.contains(bookTag)
- || useTo.contains(bookName);
+ || useTo.contains(bookSource);
}
}
diff --git a/app/src/main/java/xyz/fycz/myreader/util/utils/ClipBoardUtil.java b/app/src/main/java/xyz/fycz/myreader/util/utils/ClipBoardUtil.java
new file mode 100644
index 0000000..0557ddc
--- /dev/null
+++ b/app/src/main/java/xyz/fycz/myreader/util/utils/ClipBoardUtil.java
@@ -0,0 +1,68 @@
+package xyz.fycz.myreader.util.utils;
+
+/**
+ * @author fengyue
+ * @date 2021/1/19 14:30
+ */
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.text.TextUtils;
+
+import xyz.fycz.myreader.application.MyApplication;
+import xyz.fycz.myreader.util.ToastUtils;
+
+/**
+ * 剪切板读写工具
+ */
+public class ClipBoardUtil {
+ /**
+ * 获取剪切板内容
+ *
+ * @return
+ */
+ public static String paste(Context context) {
+ ClipboardManager manager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ if (manager != null) {
+ if (manager.hasPrimaryClip() && manager.getPrimaryClip().getItemCount() > 0) {
+ CharSequence addedText = manager.getPrimaryClip().getItemAt(0).getText();
+ String addedTextString = String.valueOf(addedText);
+ if (!TextUtils.isEmpty(addedTextString)) {
+ return addedTextString;
+ }
+ }
+ }
+ return "";
+ }
+
+ /**
+ * 写入剪切板
+ * @param text
+ * @return
+ */
+ public static boolean write(Context context, String text){
+ ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clipData = ClipData.newPlainText(null, text);
+ if (clipboard != null) {
+ clipboard.setPrimaryClip(clipData);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * 清空剪切板
+ */
+ public static void clear(Context context) {
+ ClipboardManager manager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ if (manager != null) {
+ try {
+ manager.setPrimaryClip(manager.getPrimaryClip());
+ manager.setPrimaryClip(ClipData.newPlainText("", ""));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/fycz/myreader/util/utils/FileUtils.java b/app/src/main/java/xyz/fycz/myreader/util/utils/FileUtils.java
index b5516ad..19d8981 100644
--- a/app/src/main/java/xyz/fycz/myreader/util/utils/FileUtils.java
+++ b/app/src/main/java/xyz/fycz/myreader/util/utils/FileUtils.java
@@ -22,6 +22,7 @@ import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
+import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
@@ -424,6 +425,21 @@ public class FileUtils {
return writeSucc;
}
+ public static boolean writeText(String text, File file){
+ FileWriter fw = null;
+ try {
+ fw = new FileWriter(file);
+ fw.write(text);
+ fw.flush();
+ return true;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ }finally {
+ IOUtils.close(fw);
+ }
+ }
+
public static boolean copy(String src, String dest){
byte[] buffer = FileUtils.getBytes(new File(src));
return buffer != null && FileUtils.writeFile(buffer,
diff --git a/app/src/main/java/xyz/fycz/myreader/util/utils/StringUtils.java b/app/src/main/java/xyz/fycz/myreader/util/utils/StringUtils.java
index de527dc..0dbbade 100644
--- a/app/src/main/java/xyz/fycz/myreader/util/utils/StringUtils.java
+++ b/app/src/main/java/xyz/fycz/myreader/util/utils/StringUtils.java
@@ -324,6 +324,16 @@ public class StringUtils {
}
return result;
}
+ public static boolean isJsonArray(String str) {
+ boolean result = false;
+ if (!TextUtils.isEmpty(str)) {
+ str = str.trim();
+ if (str.startsWith("[") && str.endsWith("]")) {
+ result = true;
+ }
+ }
+ return result;
+ }
public static boolean isContainEachOther(String s1, String s2){
if (s1 == null || s2 == null) return true;
diff --git a/app/src/main/java/xyz/fycz/myreader/webapi/crawler/ReadCrawlerUtil.java b/app/src/main/java/xyz/fycz/myreader/webapi/crawler/ReadCrawlerUtil.java
index 400d305..4bee829 100644
--- a/app/src/main/java/xyz/fycz/myreader/webapi/crawler/ReadCrawlerUtil.java
+++ b/app/src/main/java/xyz/fycz/myreader/webapi/crawler/ReadCrawlerUtil.java
@@ -14,6 +14,7 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.ResourceBundle;
/**
@@ -49,6 +50,16 @@ public class ReadCrawlerUtil {
return readCrawlers;
}
+ public static List getAllSources(){
+ List sources = new ArrayList<>();
+ for (BookSource bookSource : BookSource.values()) {
+ if (bookSource.equals(BookSource.fynovel))
+ continue;
+ sources.add(bookSource.text);
+ }
+ return sources;
+ }
+
public static HashMap getDisableSources() {
SharedPreUtils spu = SharedPreUtils.getInstance();
String searchSource = spu.getString(MyApplication.getmContext().getString(R.string.searchSource), null);
@@ -129,8 +140,8 @@ public class ReadCrawlerUtil {
}
public static ReadCrawler getReadCrawler(String bookSource) {
- ResourceBundle rb = ResourceBundle.getBundle("crawler");
try {
+ ResourceBundle rb = ResourceBundle.getBundle("crawler");
String readCrawlerPath = rb.getString(bookSource);
Class clz = Class.forName(readCrawlerPath);
return (ReadCrawler) clz.newInstance();
diff --git a/app/src/main/java/xyz/fycz/myreader/webapi/crawler/read/TianLaiReadCrawler.java b/app/src/main/java/xyz/fycz/myreader/webapi/crawler/read/TianLaiReadCrawler.java
index 8af2beb..326021e 100644
--- a/app/src/main/java/xyz/fycz/myreader/webapi/crawler/read/TianLaiReadCrawler.java
+++ b/app/src/main/java/xyz/fycz/myreader/webapi/crawler/read/TianLaiReadCrawler.java
@@ -62,7 +62,7 @@ public class TianLaiReadCrawler implements ReadCrawler {
char c = 160;
String spaec = "" + c;
content = content.replace(spaec, " ");
- content = content.replace("一秒记住【笔趣阁 www.52bqg.net】,精彩小说无弹窗免费阅读!", "");
+ content = content.replaceAll("笔趣阁.*最新章节!", "");
return content;
} else {
return "";
diff --git a/app/src/main/java/xyz/fycz/myreader/widget/BubblePopupView.java b/app/src/main/java/xyz/fycz/myreader/widget/BubblePopupView.java
new file mode 100644
index 0000000..f8f366a
--- /dev/null
+++ b/app/src/main/java/xyz/fycz/myreader/widget/BubblePopupView.java
@@ -0,0 +1,642 @@
+package xyz.fycz.myreader.widget;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.StateListDrawable;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.PopupWindow;
+import android.widget.TextView;
+
+
+import java.util.List;
+
+/**
+ * 仿QQ长按气泡弹窗
+ * 作者:pixiaozhi
+ * 时间:19/05/31.
+ */
+public class BubblePopupView {
+
+ private static final boolean DEFAULT_SHOW_BOTTOM = false;
+ private static final boolean DEFAULT_SHOW_TOUCH_LOCATION = false;
+ private static final boolean DEFAULT_FOCUSABLE = true;
+ private static final int DEFAULT_NORMAL_TEXT_COLOR = Color.WHITE;
+ private static final int DEFAULT_PRESSED_TEXT_COLOR = Color.WHITE;
+ private static final float DEFAULT_TEXT_SIZE_DP = 14;
+ private static final float DEFAULT_TEXT_PADDING_LEFT_DP = 10.0f;
+ private static final float DEFAULT_TEXT_PADDING_TOP_DP = 5.0f;
+ private static final float DEFAULT_TEXT_PADDING_RIGHT_DP = 10.0f;
+ private static final float DEFAULT_TEXT_PADDING_BOTTOM_DP = 5.0f;
+ private static final int DEFAULT_NORMAL_BACKGROUND_COLOR = 0xCC000000;
+ private static final int DEFAULT_PRESSED_BACKGROUND_COLOR = 0xE7777777;
+ private static final int DEFAULT_BACKGROUND_RADIUS_DP = 8;
+ private static final int DEFAULT_DIVIDER_COLOR = 0x9AFFFFFF;
+ private static final float DEFAULT_DIVIDER_WIDTH_DP = 0.5f;
+ private static final float DEFAULT_DIVIDER_HEIGHT_DP = 16.0f;
+ private static final float DEFAULT_NAVIGATION_BAR_HEIGHT = 0f;
+
+ private Context mContext;
+ private PopupWindow mPopupWindow;
+ private View mAnchorView;
+ private View mContextView;
+ private View mIndicatorView;
+ private List mPopupItemList;
+ private PopupListListener mPopupListListener;
+ private int mContextPosition;
+ private StateListDrawable mLeftItemBackground;
+ private StateListDrawable mRightItemBackground;
+ private StateListDrawable mCornerItemBackground;
+ private ColorStateList mTextColorStateList;
+ private GradientDrawable mCornerBackground;
+ //指示器属性
+ private int mIndicatorWidth;
+ private int mIndicatorHeight;
+ //PopupWindow属性
+ private int mPopupWindowWidth;
+ private int mPopupWindowHeight;
+ //文本属性
+ private int mNormalTextColor;
+ private int mPressedTextColor;
+ private float mTextSize;
+ private int mTextPaddingLeft;
+ private int mTextPaddingTop;
+ private int mTextPaddingRight;
+ private int mTextPaddingBottom;
+ private int mNormalBackgroundColor;
+ private int mPressedBackgroundColor;
+ private int mBackgroundCornerRadius;
+ //分割线属性
+ private int mDividerColor;
+ private int mDividerWidth;
+ private int mDividerHeight;
+ //是否显示在下方
+ private boolean mIsShowBottom;
+ //是否跟随手指显示
+ private boolean mIsShowTouchLocation;
+ //倒转高度,当落下位置比这个值小时,气泡显示在下方
+ private float mReversalHeight;
+ //popWindow是否聚焦,默认是
+ private boolean mIsFocusable;
+
+ public BubblePopupView(Context context) {
+ this.mContext = context;
+ this.mIsShowBottom = DEFAULT_SHOW_BOTTOM;
+ this.mIsShowTouchLocation = DEFAULT_SHOW_TOUCH_LOCATION;
+ this.mIsFocusable = DEFAULT_FOCUSABLE;
+ this.mReversalHeight = dp2px(DEFAULT_NAVIGATION_BAR_HEIGHT);
+ this.mNormalTextColor = DEFAULT_NORMAL_TEXT_COLOR;
+ this.mPressedTextColor = DEFAULT_PRESSED_TEXT_COLOR;
+ this.mTextSize = dp2px(DEFAULT_TEXT_SIZE_DP);
+ this.mTextPaddingLeft = dp2px(DEFAULT_TEXT_PADDING_LEFT_DP);
+ this.mTextPaddingTop = dp2px(DEFAULT_TEXT_PADDING_TOP_DP);
+ this.mTextPaddingRight = dp2px(DEFAULT_TEXT_PADDING_RIGHT_DP);
+ this.mTextPaddingBottom = dp2px(DEFAULT_TEXT_PADDING_BOTTOM_DP);
+ this.mNormalBackgroundColor = DEFAULT_NORMAL_BACKGROUND_COLOR;
+ this.mPressedBackgroundColor = DEFAULT_PRESSED_BACKGROUND_COLOR;
+ this.mBackgroundCornerRadius = dp2px(DEFAULT_BACKGROUND_RADIUS_DP);
+ this.mDividerColor = DEFAULT_DIVIDER_COLOR;
+ this.mDividerWidth = dp2px(DEFAULT_DIVIDER_WIDTH_DP);
+ this.mDividerHeight = dp2px(DEFAULT_DIVIDER_HEIGHT_DP);
+ this.mIndicatorView = getDefaultIndicatorView(mContext);
+ refreshBackgroundOrRadiusStateList();
+ refreshTextColorStateList(mPressedTextColor, mNormalTextColor);
+ }
+
+ /**
+ * 以气泡样式显示弹出窗口
+ *
+ * @param anchorView 要固定弹出窗口的视图
+ * @param contextPosition 上下文位置,当是列表时用于记录Position
+ * @param rawX 原始X坐标
+ * @param rawY 原始Y坐标
+ * @param popupItemList 弹出菜单列表
+ * @param popupListListener 监听器
+ */
+ public void showPopupListWindow(View anchorView, int contextPosition, float rawX, float rawY,
+ List popupItemList, PopupListListener popupListListener) {
+ mAnchorView = anchorView;
+ mContextPosition = contextPosition;
+ mPopupItemList = popupItemList;
+ mPopupListListener = popupListListener;
+ mPopupWindow = null;
+ mContextView = anchorView;
+ if (mPopupListListener != null
+ && !mPopupListListener.showPopupList(mContextView, mContextView, contextPosition)) {
+ return;
+ }
+ int[] location = new int[2];
+ mAnchorView.getLocationOnScreen(location);
+// LogUtil.e("rawX:" + rawX + ",rawY:" + rawY + ",location[0]:" + location[0] + ",location[1]" + location[1]);
+ if (mIsShowTouchLocation) {
+ showPopupListWindow(rawX - location[0], rawY - location[1]);
+ } else {
+ Log.e("navigationBarHeight:", mReversalHeight + rawY + ",rawY:" + rawY);
+ if (mReversalHeight > rawY) {
+ mIsShowBottom = true;
+ showPopupListWindow(mAnchorView.getWidth() / 2f, mAnchorView.getHeight());
+ } else {
+ mIsShowBottom = false;
+ showPopupListWindow(mAnchorView.getWidth() / 2f, 0);
+ }
+ }
+ }
+
+ /**
+ * 创建布局和显示
+ */
+ private void showPopupListWindow(float offsetX, float offsetY) {
+ if (mContext instanceof Activity && ((Activity) mContext).isFinishing()) {
+ return;
+ }
+ if (mPopupWindow == null) {
+ //创建根布局
+ LinearLayout contentView = new LinearLayout(mContext);
+ contentView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
+ contentView.setOrientation(LinearLayout.VERTICAL);
+ //创建list布局
+ LinearLayout popupListContainer = new LinearLayout(mContext);
+ popupListContainer.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
+ popupListContainer.setOrientation(LinearLayout.HORIZONTAL);
+ popupListContainer.setBackgroundDrawable(mCornerBackground);
+
+ //创建指示器
+ if (mIndicatorView != null) {
+ LinearLayout.LayoutParams layoutParams;
+ if (mIndicatorView.getLayoutParams() == null) {
+ layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
+ } else {
+ layoutParams = (LinearLayout.LayoutParams) mIndicatorView.getLayoutParams();
+ }
+ layoutParams.gravity = Gravity.CENTER;
+ mIndicatorView.setLayoutParams(layoutParams);
+ ViewParent viewParent = mIndicatorView.getParent();
+ if (viewParent instanceof ViewGroup) {
+ ((ViewGroup) viewParent).removeView(mIndicatorView);
+ }
+
+ if (!mIsShowBottom) {
+ contentView.addView(popupListContainer);
+ contentView.addView(mIndicatorView);
+ } else {
+ contentView.addView(mIndicatorView);
+ contentView.addView(popupListContainer);
+ }
+ }
+
+ //添加list的item
+ for (int i = 0; i < mPopupItemList.size(); i++) {
+ TextView textView = new TextView(mContext);
+ textView.setTextColor(mTextColorStateList);
+ textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
+ textView.setPadding(mTextPaddingLeft, mTextPaddingTop, mTextPaddingRight, mTextPaddingBottom);
+ textView.setClickable(true);
+ final int finalI = i;
+ //设置点击回调
+ textView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mPopupListListener != null) {
+ mPopupListListener.onPopupListClick(mContextView, mContextPosition, finalI);
+ hidePopupListWindow();
+ }
+ }
+ });
+
+ textView.setText(mPopupItemList.get(i));
+
+ //设置item的背景
+ if (mPopupItemList.size() > 1 && i == 0) {
+ textView.setBackgroundDrawable(mLeftItemBackground);
+ } else if (mPopupItemList.size() > 1 && i == mPopupItemList.size() - 1) {
+ textView.setBackgroundDrawable(mRightItemBackground);
+ } else if (mPopupItemList.size() == 1) {
+ textView.setBackgroundDrawable(mCornerItemBackground);
+ } else {
+ textView.setBackgroundDrawable(getCenterItemBackground());
+ }
+ popupListContainer.addView(textView);
+ //设置2个item中的分割线
+ if (mPopupItemList.size() > 1 && i != mPopupItemList.size() - 1) {
+ View divider = new View(mContext);
+ LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(mDividerWidth, mDividerHeight);
+ layoutParams.gravity = Gravity.CENTER;
+ divider.setLayoutParams(layoutParams);
+ divider.setBackgroundColor(mDividerColor);
+ popupListContainer.addView(divider);
+ }
+ }
+ if (mPopupWindowWidth == 0) {
+ mPopupWindowWidth = getViewWidth(popupListContainer);
+ }
+ //获取指示器宽高
+ if (mIndicatorView != null && mIndicatorWidth == 0) {
+ if (mIndicatorView.getLayoutParams().width > 0) {
+ mIndicatorWidth = mIndicatorView.getLayoutParams().width;
+ } else {
+ mIndicatorWidth = getViewWidth(mIndicatorView);
+ }
+ }
+ if (mIndicatorView != null && mIndicatorHeight == 0) {
+ if (mIndicatorView.getLayoutParams().height > 0) {
+ mIndicatorHeight = mIndicatorView.getLayoutParams().height;
+ } else {
+ mIndicatorHeight = getViewHeight(mIndicatorView);
+ }
+ }
+ if (mPopupWindowHeight == 0) {
+ mPopupWindowHeight = getViewHeight(popupListContainer) + mIndicatorHeight;
+ }
+ mPopupWindow = new PopupWindow(contentView, mPopupWindowWidth, mPopupWindowHeight, true);
+ mPopupWindow.setTouchable(true);
+ mPopupWindow.setFocusable(mIsFocusable);
+ mPopupWindow.setBackgroundDrawable(new BitmapDrawable());
+ }
+ int[] location = new int[2];
+ mAnchorView.getLocationOnScreen(location);
+ if (mIndicatorView != null) {
+ float leftTranslationLimit = mIndicatorWidth / 2f + mBackgroundCornerRadius - mPopupWindowWidth / 2f;
+ float rightTranslationLimit = mPopupWindowWidth / 2f - mIndicatorWidth / 2f - mBackgroundCornerRadius;
+ //获取最大绝对宽度,单位是px
+ float maxWidth = mContext.getResources().getDisplayMetrics().widthPixels;
+ //通过setTranslationX改变view的位置,是不改变view的LayoutParams的,也即不改变getLeft等view的信息
+ if (location[0] + offsetX < mPopupWindowWidth / 2f) {
+ mIndicatorView.setTranslationX(Math.max(location[0] + offsetX - mPopupWindowWidth / 2f, leftTranslationLimit));
+ } else if (location[0] + offsetX + mPopupWindowWidth / 2f > maxWidth) {
+ mIndicatorView.setTranslationX(Math.min(location[0] + offsetX + mPopupWindowWidth / 2f - maxWidth, rightTranslationLimit));
+ } else {
+ mIndicatorView.setTranslationX(0);
+ }
+ }
+ if (!mPopupWindow.isShowing()) {
+ int x = (int) (location[0] + offsetX - mPopupWindowWidth / 2f + 0.5f);
+ int y = mIsShowBottom ? (int) (location[1] + offsetY + 0.5f) : (int) (location[1] + offsetY - mPopupWindowHeight + 0.5f);
+ mPopupWindow.showAtLocation(mAnchorView, Gravity.NO_GRAVITY, x, y);
+ }
+ }
+
+ /**
+ * 刷新背景或附加状态列表
+ */
+ private void refreshBackgroundOrRadiusStateList() {
+ // left
+ GradientDrawable leftItemPressedDrawable = new GradientDrawable();
+ leftItemPressedDrawable.setColor(mPressedBackgroundColor);
+ leftItemPressedDrawable.setCornerRadii(new float[]{
+ mBackgroundCornerRadius, mBackgroundCornerRadius,
+ 0, 0,
+ 0, 0,
+ mBackgroundCornerRadius, mBackgroundCornerRadius});
+ GradientDrawable leftItemNormalDrawable = new GradientDrawable();
+ leftItemNormalDrawable.setColor(Color.TRANSPARENT);
+ leftItemNormalDrawable.setCornerRadii(new float[]{
+ mBackgroundCornerRadius, mBackgroundCornerRadius,
+ 0, 0,
+ 0, 0,
+ mBackgroundCornerRadius, mBackgroundCornerRadius});
+ mLeftItemBackground = new StateListDrawable();
+ mLeftItemBackground.addState(new int[]{android.R.attr.state_pressed}, leftItemPressedDrawable);
+ mLeftItemBackground.addState(new int[]{}, leftItemNormalDrawable);
+ // right
+ GradientDrawable rightItemPressedDrawable = new GradientDrawable();
+ rightItemPressedDrawable.setColor(mPressedBackgroundColor);
+ rightItemPressedDrawable.setCornerRadii(new float[]{
+ 0, 0,
+ mBackgroundCornerRadius, mBackgroundCornerRadius,
+ mBackgroundCornerRadius, mBackgroundCornerRadius,
+ 0, 0});
+ GradientDrawable rightItemNormalDrawable = new GradientDrawable();
+ rightItemNormalDrawable.setColor(Color.TRANSPARENT);
+ rightItemNormalDrawable.setCornerRadii(new float[]{
+ 0, 0,
+ mBackgroundCornerRadius, mBackgroundCornerRadius,
+ mBackgroundCornerRadius, mBackgroundCornerRadius,
+ 0, 0});
+ mRightItemBackground = new StateListDrawable();
+ mRightItemBackground.addState(new int[]{android.R.attr.state_pressed}, rightItemPressedDrawable);
+ mRightItemBackground.addState(new int[]{}, rightItemNormalDrawable);
+ // corner
+ GradientDrawable cornerItemPressedDrawable = new GradientDrawable();
+ cornerItemPressedDrawable.setColor(mPressedBackgroundColor);
+ cornerItemPressedDrawable.setCornerRadius(mBackgroundCornerRadius);
+ GradientDrawable cornerItemNormalDrawable = new GradientDrawable();
+ cornerItemNormalDrawable.setColor(Color.TRANSPARENT);
+ cornerItemNormalDrawable.setCornerRadius(mBackgroundCornerRadius);
+ mCornerItemBackground = new StateListDrawable();
+ mCornerItemBackground.addState(new int[]{android.R.attr.state_pressed}, cornerItemPressedDrawable);
+ mCornerItemBackground.addState(new int[]{}, cornerItemNormalDrawable);
+ mCornerBackground = new GradientDrawable();
+ mCornerBackground.setColor(mNormalBackgroundColor);
+ mCornerBackground.setCornerRadius(mBackgroundCornerRadius);
+ }
+
+ /**
+ * 获取中心item背景
+ */
+ private StateListDrawable getCenterItemBackground() {
+ StateListDrawable centerItemBackground = new StateListDrawable();
+ GradientDrawable centerItemPressedDrawable = new GradientDrawable();
+ centerItemPressedDrawable.setColor(mPressedBackgroundColor);
+ GradientDrawable centerItemNormalDrawable = new GradientDrawable();
+ centerItemNormalDrawable.setColor(Color.TRANSPARENT);
+ centerItemBackground.addState(new int[]{android.R.attr.state_pressed}, centerItemPressedDrawable);
+ centerItemBackground.addState(new int[]{}, centerItemNormalDrawable);
+ return centerItemBackground;
+ }
+
+ /**
+ * 刷新文本颜色状态列表
+ *
+ * @param pressedTextColor 按下文本颜色
+ * @param normalTextColor 正常状态下文本颜色
+ */
+ private void refreshTextColorStateList(int pressedTextColor, int normalTextColor) {
+ int[][] states = new int[2][];
+ states[0] = new int[]{android.R.attr.state_pressed};
+ states[1] = new int[]{};
+ int[] colors = new int[]{pressedTextColor, normalTextColor};
+ mTextColorStateList = new ColorStateList(states, colors);
+ }
+
+ public void hidePopupListWindow() {
+ if (mContext instanceof Activity && ((Activity) mContext).isFinishing()) {
+ return;
+ }
+ if (mPopupWindow != null && mPopupWindow.isShowing()) {
+ mPopupWindow.dismiss();
+ }
+ }
+
+ public View getIndicatorView() {
+ return mIndicatorView;
+ }
+
+ public View getDefaultIndicatorView(Context context) {
+ return getTriangleIndicatorView(context, dp2px(16), dp2px(8), DEFAULT_NORMAL_BACKGROUND_COLOR);
+ }
+
+ public View getTriangleIndicatorView(Context context, final float widthPixel, final float heightPixel,
+ final int color) {
+ ImageView indicator = new ImageView(context);
+ Drawable drawable = new Drawable() {
+ @Override
+ public void draw(Canvas canvas) {
+ Path path = new Path();
+ Paint paint = new Paint();
+ paint.setColor(color);
+ paint.setStyle(Paint.Style.FILL);
+ if (!mIsShowBottom) {
+ //这里画的倒三角
+ path.moveTo(0f, 0f);
+ path.lineTo(widthPixel, 0f);
+ path.lineTo(widthPixel / 2, heightPixel);
+ //将图像封闭,这里path.close()等同于 path.moveTo(widthPixel / 2, heightPixel);path.lineTo(widthPixel, 0);
+ path.close();
+ } else {
+ //正三角
+ path.moveTo(0f, heightPixel);
+ path.lineTo(widthPixel, heightPixel);
+ path.lineTo(widthPixel / 2, 0);
+ path.close();
+ }
+ canvas.drawPath(path, paint);
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {
+
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return (int) widthPixel;
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return (int) heightPixel;
+ }
+ };
+ indicator.setImageDrawable(drawable);
+ return indicator;
+ }
+
+ public void setIndicatorView(View indicatorView) {
+ this.mIndicatorView = indicatorView;
+ }
+
+ public void setIndicatorSize(int widthPixel, int heightPixel) {
+ this.mIndicatorWidth = widthPixel;
+ this.mIndicatorHeight = heightPixel;
+ LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(mIndicatorWidth, mIndicatorHeight);
+ layoutParams.gravity = Gravity.CENTER;
+ if (mIndicatorView != null) {
+ mIndicatorView.setLayoutParams(layoutParams);
+ }
+ }
+
+ public int getNormalTextColor() {
+ return mNormalTextColor;
+ }
+
+ public void setNormalTextColor(int normalTextColor) {
+ this.mNormalTextColor = normalTextColor;
+ refreshTextColorStateList(mPressedTextColor, mNormalTextColor);
+ }
+
+ public int getPressedTextColor() {
+ return mPressedTextColor;
+ }
+
+ public void setPressedTextColor(int pressedTextColor) {
+ this.mPressedTextColor = pressedTextColor;
+ refreshTextColorStateList(mPressedTextColor, mNormalTextColor);
+ }
+
+ public float getTextSize() {
+ return mTextSize;
+ }
+
+ public void setTextSize(float textSizePixel) {
+ this.mTextSize = textSizePixel;
+ }
+
+ public int getTextPaddingLeft() {
+ return mTextPaddingLeft;
+ }
+
+ public void setTextPaddingLeft(int textPaddingLeft) {
+ this.mTextPaddingLeft = textPaddingLeft;
+ }
+
+ public int getTextPaddingTop() {
+ return mTextPaddingTop;
+ }
+
+ public void setTextPaddingTop(int textPaddingTop) {
+ this.mTextPaddingTop = textPaddingTop;
+ }
+
+ public int getTextPaddingRight() {
+ return mTextPaddingRight;
+ }
+
+ public void setTextPaddingRight(int textPaddingRight) {
+ this.mTextPaddingRight = textPaddingRight;
+ }
+
+ public int getTextPaddingBottom() {
+ return mTextPaddingBottom;
+ }
+
+ public void setTextPaddingBottom(int textPaddingBottom) {
+ this.mTextPaddingBottom = textPaddingBottom;
+ }
+
+ public void setTextPadding(int left, int top, int right, int bottom) {
+ this.mTextPaddingLeft = left;
+ this.mTextPaddingTop = top;
+ this.mTextPaddingRight = right;
+ this.mTextPaddingBottom = bottom;
+ }
+
+ public int getNormalBackgroundColor() {
+ return mNormalBackgroundColor;
+ }
+
+ public void setNormalBackgroundColor(int normalBackgroundColor) {
+ this.mNormalBackgroundColor = normalBackgroundColor;
+ refreshBackgroundOrRadiusStateList();
+ }
+
+ public int getPressedBackgroundColor() {
+ return mPressedBackgroundColor;
+ }
+
+ public void setPressedBackgroundColor(int pressedBackgroundColor) {
+ this.mPressedBackgroundColor = pressedBackgroundColor;
+ refreshBackgroundOrRadiusStateList();
+ }
+
+ public void setShowBottom(boolean isShowBottom) {
+ this.mIsShowBottom = isShowBottom;
+ }
+
+ public void setShowTouchLocation(boolean showTouchLocation) {
+ mIsShowTouchLocation = showTouchLocation;
+ }
+
+ public void setFocusable(boolean mIsFocusable) {
+ this.mIsFocusable = mIsFocusable;
+ }
+
+ public int getBackgroundCornerRadius() {
+ return mBackgroundCornerRadius;
+ }
+
+ public void setBackgroundCornerRadius(int backgroundCornerRadiusPixel) {
+ this.mBackgroundCornerRadius = backgroundCornerRadiusPixel;
+ refreshBackgroundOrRadiusStateList();
+ }
+
+ public int getDividerColor() {
+ return mDividerColor;
+ }
+
+ public void setDividerColor(int dividerColor) {
+ this.mDividerColor = dividerColor;
+ }
+
+ public int getDividerWidth() {
+ return mDividerWidth;
+ }
+
+ public void setDividerWidth(int dividerWidthPixel) {
+ this.mDividerWidth = dividerWidthPixel;
+ }
+
+ public int getDividerHeight() {
+ return mDividerHeight;
+ }
+
+ public void setDividerHeight(int dividerHeightPixel) {
+ this.mDividerHeight = dividerHeightPixel;
+ }
+
+ public void setmReversalHeight(float mReversalHeight) {
+ this.mReversalHeight = mReversalHeight;
+ }
+
+ public Resources getResources() {
+ if (mContext == null) {
+ return Resources.getSystem();
+ } else {
+ return mContext.getResources();
+ }
+ }
+
+ private int getViewWidth(View view) {
+ // 1、UNSPECIFIED,不限定。意思就是,子View想要多大,我就可以给你多大,你放心大胆的measure吧,不用管其他的。也不用管我传递给你的尺寸值。(其实Android高版本中推荐,只要是这个模式,尺寸设置为0)
+ //
+ // 2、EXACTLY,精确的。意思就是,根据我当前的状况,结合你指定的尺寸参数来考虑,你就应该是这个尺寸,具体大小在MeasureSpec的尺寸属性中,自己去查看吧,你也不要管你的content有多大了,就用这个尺寸吧。
+ //
+ // 3、AT_MOST,最多的。意思就是,根据我当前的情况,结合你指定的尺寸参数来考虑,在不超过我给你限定的尺寸的前提下,你测量一个恰好能包裹你内容的尺寸就可以了。
+ view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
+ return view.getMeasuredWidth();
+ }
+
+ private int getViewHeight(View view) {
+ view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
+ return view.getMeasuredHeight();
+ }
+
+ public int dp2px(float value) {
+ return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+ value, getResources().getDisplayMetrics());
+ }
+
+ public int sp2px(float value) {
+ return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
+ value, getResources().getDisplayMetrics());
+ }
+
+ /**
+ * 回调监听器
+ */
+ public interface PopupListListener {
+ boolean showPopupList(View adapterView, View contextView, int contextPosition);
+
+ void onPopupListClick(View contextView, int contextPosition, int position);
+ }
+
+}
+
+
+
diff --git a/app/src/main/java/xyz/fycz/myreader/widget/page/PageLoader.java b/app/src/main/java/xyz/fycz/myreader/widget/page/PageLoader.java
index a9097f2..88fa917 100644
--- a/app/src/main/java/xyz/fycz/myreader/widget/page/PageLoader.java
+++ b/app/src/main/java/xyz/fycz/myreader/widget/page/PageLoader.java
@@ -9,15 +9,10 @@ import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextUtils;
-import androidx.annotation.NonNull;
-import androidx.core.content.ContextCompat;
-
import android.text.TextPaint;
import android.util.DisplayMetrics;
-import android.util.Log;
import com.gyf.immersionbar.ImmersionBar;
-import com.luhuiguo.chinese.ChineseUtils;
import io.reactivex.Single;
import io.reactivex.SingleObserver;
@@ -34,14 +29,13 @@ import xyz.fycz.myreader.entity.Setting;
import xyz.fycz.myreader.greendao.entity.Chapter;
import xyz.fycz.myreader.model.audio.ReadAloudService;
import xyz.fycz.myreader.util.IOUtils;
-import xyz.fycz.myreader.util.StatusBarUtil;
import xyz.fycz.myreader.util.ToastUtils;
+import xyz.fycz.myreader.util.help.ChapterContentHelp;
import xyz.fycz.myreader.util.utils.BitmapUtil;
import xyz.fycz.myreader.util.utils.MeUtils;
import xyz.fycz.myreader.util.utils.RxUtils;
import xyz.fycz.myreader.util.utils.ScreenUtils;
import xyz.fycz.myreader.util.utils.StringUtils;
-import xyz.fycz.myreader.widget.page2.TxtChar;
import xyz.fycz.myreader.widget.page2.TxtLine;
import java.io.BufferedReader;
@@ -51,8 +45,6 @@ import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
-import static xyz.fycz.myreader.common.APPCONST.*;
-
/**
* Created by fengyue on 20-11-21
*/
@@ -103,7 +95,7 @@ public abstract class PageLoader {
// 绘制背景颜色的画笔(用来擦除需要重绘的部分)
private Paint mBgPaint;
// 绘制小说内容的画笔
- private TextPaint mTextPaint;
+ public TextPaint mTextPaint;
// 阅读器的配置选项
private Setting mSettingManager;
// 被遮盖的页,或者认为被取消显示的页
@@ -169,6 +161,7 @@ public abstract class PageLoader {
private int readAloudParagraph = -1; //正在朗读章节
private Bitmap bgBitmap;
+ private ChapterContentHelp contentHelper = new ChapterContentHelp();
public void resetReadAloudParagraph() {
readAloudParagraph = -1;
@@ -900,11 +893,13 @@ public abstract class PageLoader {
//根据状态不一样,数据不一样
if (mStatus != STATUS_FINISH) {
if (isChapterListPrepare) {
- canvas.drawText(mChapterList.get(mCurChapterPos).getTitle()
- , mMarginLeft, tipTop, mTipPaint);
+ String title = mChapterList.get(mCurChapterPos).getTitle();
+ title = contentHelper.replaceContent(mCollBook.getName() + "-" + mCollBook.getAuthor(), mCollBook.getSource(), title, true);
+ canvas.drawText(title, mMarginLeft, tipTop, mTipPaint);
}
} else {
- String title = TextUtils.ellipsize(mCurPage.title, mTipPaint, mDisplayWidth - mMarginLeft - mMarginRight - mTipPaint.measureText(progress), TextUtils.TruncateAt.END).toString();
+ String title = contentHelper.replaceContent(mCollBook.getName() + "-" + mCollBook.getAuthor(), mCollBook.getSource(), mCurPage.title, true);
+ title = TextUtils.ellipsize(title, mTipPaint, mDisplayWidth - mMarginLeft - mMarginRight - mTipPaint.measureText(progress), TextUtils.TruncateAt.END).toString();
canvas.drawText(title, mMarginLeft, tipTop, mTipPaint);
/******绘制页码********/
// 底部的字显示的位置Y
@@ -1041,14 +1036,15 @@ public abstract class PageLoader {
top += ImmersionBar.getStatusBarHeight((Activity) mContext);
}
}
-
+ Paint.FontMetrics fontMetricsForTitle = mTitlePaint.getFontMetrics();
+ Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
//设置总距离
float interval = mTextInterval + mTextPaint.getTextSize();
float para = mTextPara + mTextPaint.getTextSize();
float titleInterval = mTitleInterval + mTitlePaint.getTextSize();
float titlePara = mTitlePara + mTextPaint.getTextSize();
String str = null;
-
+ int ppp = 0;//pzl,文字位置
//对标题进行绘制
boolean isLight;
int titleLen = 0;
@@ -1062,12 +1058,46 @@ public abstract class PageLoader {
if (i == 0) {
top += mTitlePara;
}
-
//计算文字显示的起始点
int start = (int) (mDisplayWidth - mTitlePaint.measureText(str)) / 2;
//进行绘制
canvas.drawText(str, start, top, mTitlePaint);
+ //pzl
+ float leftposition = start;
+ float rightposition = 0;
+ float bottomposition = top + mTitlePaint.getFontMetrics().descent;
+ float TextHeight = Math.abs(fontMetricsForTitle.ascent) + Math.abs(fontMetricsForTitle.descent);
+
+ if (mCurPage.txtLists != null) {
+ for (TxtChar c : mCurPage.txtLists.get(i).getCharsData()) {
+ rightposition = leftposition + c.getCharWidth();
+ Point tlp = new Point();
+ c.setTopLeftPosition(tlp);
+ tlp.x = (int) leftposition;
+ tlp.y = (int) (bottomposition - TextHeight);
+
+ Point blp = new Point();
+ c.setBottomLeftPosition(blp);
+ blp.x = (int) leftposition;
+ blp.y = (int) bottomposition;
+
+ Point trp = new Point();
+ c.setTopRightPosition(trp);
+ trp.x = (int) rightposition;
+ trp.y = (int) (bottomposition - TextHeight);
+
+ Point brp = new Point();
+ c.setBottomRightPosition(brp);
+ brp.x = (int) rightposition;
+ brp.y = (int) bottomposition;
+ ppp++;
+ c.setIndex(ppp);
+
+ leftposition = rightposition;
+ }
+ }
+
//设置尾部间距
if (i == mCurPage.titleLines - 1) {
top += titlePara;
@@ -1096,6 +1126,47 @@ public abstract class PageLoader {
} else {
canvas.drawText(str, mMarginLeft, top, mTextPaint);
}
+ //记录文字位置 --开始 pzl
+ float leftposition = mMarginLeft;
+ if (isFirstLineOfParagraph(str)) {
+ //canvas.drawText(blanks, x, top, mTextPaint);
+ float bw = StaticLayout.getDesiredWidth(indent, mTextPaint);
+ leftposition += bw;
+ }
+ float rightposition = 0;
+ float bottomposition = top + mTextPaint.getFontMetrics().descent;
+ float textHeight = Math.abs(fontMetrics.ascent) + Math.abs(fontMetrics.descent);
+
+ if (mCurPage.txtLists != null) {
+ for (TxtChar c : mCurPage.txtLists.get(i).getCharsData()) {
+ rightposition = leftposition + c.getCharWidth();
+ Point tlp = new Point();
+ c.setTopLeftPosition(tlp);
+ tlp.x = (int) leftposition;
+ tlp.y = (int) (bottomposition - textHeight);
+
+ Point blp = new Point();
+ c.setBottomLeftPosition(blp);
+ blp.x = (int) leftposition;
+ blp.y = (int) bottomposition;
+
+ Point trp = new Point();
+ c.setTopRightPosition(trp);
+ trp.x = (int) rightposition;
+ trp.y = (int) (bottomposition - textHeight);
+
+ Point brp = new Point();
+ c.setBottomRightPosition(brp);
+ brp.x = (int) rightposition;
+ brp.y = (int) bottomposition;
+
+ leftposition = rightposition;
+
+ ppp++;
+ c.setIndex(ppp);
+ }
+ }
+ //记录文字位置 --结束 pzl
if (str.endsWith("\n")) {
top += para;
} else {
@@ -1507,15 +1578,11 @@ public abstract class PageLoader {
paragraph = paragraph.trim() + "\n";
try {
while (showTitle || (paragraph = br.readLine()) != null) {
- if (firstLine && !showTitle){
+ paragraph = contentHelper.replaceContent(mCollBook.getName() + "-" + mCollBook.getAuthor(), mCollBook.getSource(), paragraph, true);
+ if (firstLine && !showTitle) {
paragraph = paragraph.replace(chapter.getTitle(), "");
firstLine = false;
}
- if (mSettingManager.getLanguage() == Language.traditional) {
- paragraph = ChineseUtils.toTraditional(paragraph);
- } else if (mSettingManager.getLanguage() == Language.simplified) {
- paragraph = ChineseUtils.toSimplified(paragraph);
- }
// 重置段落
if (!showTitle) {
paragraph = paragraph.replaceAll("\\s", "").trim();
@@ -1585,9 +1652,9 @@ public abstract class PageLoader {
lines.add(subStr);
//begin pzl
//记录每个字的位置
- char[] cs = subStr.toCharArray();
+ char[] cs = subStr.replace((char) 12288, ' ').trim().toCharArray();
TxtLine txtList = new TxtLine();//每一行
- txtList.setCharsData(new ArrayList());
+ txtList.setCharsData(new ArrayList<>());
for (char c : cs) {
String mesasrustr = String.valueOf(c);
float charwidth = mTextPaint.measureText(mesasrustr);
@@ -1642,13 +1709,7 @@ public abstract class PageLoader {
} catch (IOException e) {
e.printStackTrace();
} finally {
- if (br != null) {
- try {
- br.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
+ IOUtils.close(br);
}
return txtChapter;
}
@@ -1693,6 +1754,10 @@ public abstract class PageLoader {
return mCurChapter.getPage(pos);
}
+ public TxtPage curPage() {
+ return mCurPage;
+ }
+
/**
* @return:获取上一个页面
*/
@@ -1889,6 +1954,37 @@ public abstract class PageLoader {
}
}
+ /**
+ * --------------------
+ * 检测获取按压坐标所在位置的字符,没有的话返回null
+ * --------------------
+ * author: huangwei
+ * 2017年7月4日上午10:23:19
+ */
+ TxtChar detectPressTxtChar(float down_X2, float down_Y2) {
+ TxtPage txtPage = mCurPage;
+ if (txtPage == null) return null;
+ List txtLines = txtPage.txtLists;
+ if (txtLines == null) return null;
+ for (TxtLine l : txtLines) {
+ List txtChars = l.getCharsData();
+ if (txtChars != null) {
+ for (TxtChar c : txtChars) {
+ Point leftPoint = c.getBottomLeftPosition();
+ Point rightPoint = c.getBottomRightPosition();
+ if (leftPoint != null && down_Y2 > leftPoint.y) {
+ break;// 说明是在下一行
+ }
+ if (leftPoint != null && rightPoint != null && down_X2 >= leftPoint.x && down_X2 <= rightPoint.x) {
+ return c;
+ }
+
+ }
+ }
+ }
+ return null;
+ }
+
/*****************************************interface*****************************************/
public interface OnPageChangeListener {
diff --git a/app/src/main/java/xyz/fycz/myreader/widget/page/PageView.java b/app/src/main/java/xyz/fycz/myreader/widget/page/PageView.java
index d467c73..bc56e26 100644
--- a/app/src/main/java/xyz/fycz/myreader/widget/page/PageView.java
+++ b/app/src/main/java/xyz/fycz/myreader/widget/page/PageView.java
@@ -4,23 +4,28 @@ import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
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.gyf.immersionbar.ImmersionBar;
+import java.util.ArrayList;
+import java.util.List;
+
import xyz.fycz.myreader.application.SysManager;
import xyz.fycz.myreader.entity.Setting;
import xyz.fycz.myreader.greendao.entity.Book;
import xyz.fycz.myreader.greendao.service.ChapterService;
-import xyz.fycz.myreader.ui.popmenu.AutoPageMenu;
import xyz.fycz.myreader.util.utils.SnackbarUtils;
import xyz.fycz.myreader.webapi.crawler.base.ReadCrawler;
import xyz.fycz.myreader.widget.animation.*;
+import xyz.fycz.myreader.widget.page2.TxtLine;
/**
* Created by Administrator on 2016/8/29 0029.
@@ -71,6 +76,29 @@ public class PageView extends View {
//内容加载器
private PageLoader mPageLoader;
+
+ //文字选择画笔
+ private Paint mTextSelectPaint = null;
+ //文字选择画笔颜色
+ private int TextSelectColor = Color.parseColor("#7787CEFA");
+ private Path mSelectTextPath = new Path();
+ // 是否发触了长按事件
+ private boolean isLongPress = false;
+ //第一个选择的文字
+ private TxtChar firstSelectTxtChar = null;
+ //最后选择的一个文字
+ private TxtChar lastSelectTxtChar = null;
+ //选择模式
+ private SelectMode selectMode = SelectMode.Normal;
+ //文本高度
+ private float textHeight = 0;
+ //长按的runnable
+ private Runnable mLongPressRunnable;
+ //长按时间
+ private static final int LONG_PRESS_TIMEOUT = 800;
+ //选择的列
+ private List mSelectLines = new ArrayList<>();
+
public PageView(Context context) {
this(context, null);
}
@@ -81,9 +109,29 @@ public class PageView extends View {
public PageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
+ init();
statusBarHeight = ImmersionBar.getStatusBarHeight((Activity) getContext());
}
-
+ private void init() {
+ //初始化画笔
+ mTextSelectPaint = new Paint();
+ mTextSelectPaint.setAntiAlias(true);
+ mTextSelectPaint.setTextSize(19);
+ mTextSelectPaint.setColor(TextSelectColor);
+
+ mLongPressRunnable = () -> {
+ if (mPageLoader == null) return;
+ performLongClick();
+ if (mStartX > 0 && mStartY > 0) {// 说明还没释放,是长按事件
+ isLongPress = true;//长按
+ TxtChar p = mPageLoader.detectPressTxtChar(mStartX, mStartY);//找到长按的点
+ firstSelectTxtChar = p;//设置开始位置字符
+ lastSelectTxtChar = p;//设置结束位置字符
+ selectMode = SelectMode.PressSelectText;//设置模式为长按选择
+ mTouchListener.onLongPress();//响应长按事件,供上层调用
+ }
+ };
+ }
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
@@ -218,11 +266,19 @@ public class PageView extends View {
protected void onDraw(Canvas canvas) {
//绘制动画
mPageAnim.draw(canvas);
+ if (selectMode != SelectMode.Normal && !isRunning() && !isMove) {
+ DrawSelectText(canvas);
+ }
}
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
+ if (mPageAnim == null) return true;
+ if (mPageLoader == null) return true;
+
+ Paint.FontMetrics fontMetrics = mPageLoader.mTextPaint.getFontMetrics();
+ textHeight = Math.abs(fontMetrics.ascent) + Math.abs(fontMetrics.descent);
if (!canTouch && event.getAction() != MotionEvent.ACTION_DOWN) return true;
@@ -233,8 +289,24 @@ public class PageView extends View {
mStartX = x;
mStartY = y;
isMove = false;
+ //
+ if (SysManager.getSetting().isCanSelectText() && mPageLoader.getPageStatus() == PageLoader.STATUS_FINISH) {
+ postDelayed(mLongPressRunnable, LONG_PRESS_TIMEOUT);
+ }
+
+ //
+ isLongPress = false;
+
canTouch = mTouchListener.onTouch();
+
+ if (!canTouch){
+ removeCallbacks(mLongPressRunnable);
+ }
+
mPageAnim.onTouchEvent(event);
+ selectMode = SelectMode.Normal;
+
+ mTouchListener.onTouchClearCursor();
break;
case MotionEvent.ACTION_MOVE:
// 判断是否大于最小滑动值。
@@ -245,6 +317,9 @@ public class PageView extends View {
// 如果滑动了,则进行翻页。
if (isMove) {
+ if (SysManager.getSetting().isCanSelectText()) {
+ removeCallbacks(mLongPressRunnable);
+ }
mPageAnim.onTouchEvent(event);
}
break;
@@ -255,16 +330,41 @@ public class PageView extends View {
mCenterRect = new RectF(mViewWidth / 5, mViewHeight / 3,
mViewWidth * 4 / 5, mViewHeight * 2 / 3);
}
-
+ if (SysManager.getSetting().isCanSelectText()) {
+ removeCallbacks(mLongPressRunnable);
+ }
//是否点击了中间
if (mCenterRect.contains(x, y)) {
- if (mTouchListener != null) {
- mTouchListener.center();
+ if (firstSelectTxtChar == null) {
+ if (mTouchListener != null) {
+ mTouchListener.center();
+ }
+ } else {
+ if (mSelectTextPath != null) {//长安选择删除选中状态
+ if (!isLongPress) {
+ firstSelectTxtChar = null;
+ mSelectTextPath.reset();
+ invalidate();
+ }
+ }
+ //清除移动选择状态
}
return true;
}
}
- mPageAnim.onTouchEvent(event);
+ if (firstSelectTxtChar == null || isMove) {//长安选择删除选中状态
+ mPageAnim.onTouchEvent(event);
+ } else {
+ if (!isLongPress) {
+ //释放了
+ if (LONG_PRESS_TIMEOUT != 0) {
+ removeCallbacks(mLongPressRunnable);
+ }
+ firstSelectTxtChar = null;
+ mSelectTextPath.reset();
+ invalidate();
+ }
+ }
break;
}
return true;
@@ -405,7 +505,171 @@ public class PageView extends View {
return mPageLoader;
}
+ private void DrawSelectText(Canvas canvas) {
+ if (selectMode == SelectMode.PressSelectText) {
+ drawPressSelectText(canvas);
+ } else if (selectMode == SelectMode.SelectMoveForward) {
+ drawMoveSelectText(canvas);
+ } else if (selectMode == SelectMode.SelectMoveBack) {
+ drawMoveSelectText(canvas);
+ }
+ }
+
+
+ private void drawPressSelectText(Canvas canvas) {
+ if (lastSelectTxtChar != null) {// 找到了选择的字符
+ mSelectTextPath.reset();
+ mSelectTextPath.moveTo(firstSelectTxtChar.getTopLeftPosition().x, firstSelectTxtChar.getTopLeftPosition().y);
+ mSelectTextPath.lineTo(firstSelectTxtChar.getTopRightPosition().x, firstSelectTxtChar.getTopRightPosition().y);
+ mSelectTextPath.lineTo(firstSelectTxtChar.getBottomRightPosition().x, firstSelectTxtChar.getBottomRightPosition().y);
+ mSelectTextPath.lineTo(firstSelectTxtChar.getBottomLeftPosition().x, firstSelectTxtChar.getBottomLeftPosition().y);
+ canvas.drawPath(mSelectTextPath, mTextSelectPaint);
+ getSelectData();
+ }
+ }
+
+
+ public String getSelectStr() {
+
+ if (mSelectLines.size() == 0) {
+ return String.valueOf(firstSelectTxtChar.getChardata());
+ }
+ StringBuilder sb = new StringBuilder();
+ for (TxtLine l : mSelectLines) {
+ //Log.e("selectline", l.getLineData() + "");
+ sb.append(l.getLineData());
+ }
+
+ return sb.toString();
+ }
+
+
+ private void drawMoveSelectText(Canvas canvas) {
+ if (firstSelectTxtChar == null || lastSelectTxtChar == null)
+ return;
+ getSelectData();
+ drawSelectLines(canvas);
+ }
+
+ List mLinseData = null;
+
+ private void getSelectData() {
+ TxtPage txtPage = mPageLoader.curPage();
+ if (txtPage != null) {
+ mLinseData = txtPage.txtLists;
+
+ Boolean Started = false;
+ Boolean Ended = false;
+
+ mSelectLines.clear();
+
+ // 找到选择的字符数据,转化为选择的行,然后将行选择背景画出来
+ for (TxtLine l : mLinseData) {
+
+ TxtLine selectline = new TxtLine();
+ selectline.setCharsData(new ArrayList<>());
+
+ for (TxtChar c : l.getCharsData()) {
+ if (!Started) {
+ if (c.getIndex() == firstSelectTxtChar.getIndex()) {
+ Started = true;
+ selectline.getCharsData().add(c);
+ if (c.getIndex() == lastSelectTxtChar.getIndex()) {
+ Ended = true;
+ break;
+ }
+ }
+ } else {
+ if (c.getIndex() == lastSelectTxtChar.getIndex()) {
+ Ended = true;
+ if (!selectline.getCharsData().contains(c)) {
+ selectline.getCharsData().add(c);
+ }
+ break;
+ } else {
+ selectline.getCharsData().add(c);
+ }
+ }
+ }
+
+ mSelectLines.add(selectline);
+
+ if (Started && Ended) {
+ break;
+ }
+ }
+ }
+ }
+
+ public SelectMode getSelectMode() {
+ return selectMode;
+ }
+
+ public void setSelectMode(SelectMode mCurrentMode) {
+ this.selectMode = mCurrentMode;
+ }
+
+ private void drawSelectLines(Canvas canvas) {
+ drawOaleSeletLinesBg(canvas);
+ }
+
+ public void clearSelect() {
+ firstSelectTxtChar = null;
+ lastSelectTxtChar = null;
+ selectMode = SelectMode.Normal;
+ mSelectTextPath.reset();
+ invalidate();
+
+ }
+
+ //根据当前坐标返回文字
+ public TxtChar getCurrentTxtChar(float x, float y) {
+ return mPageLoader.detectPressTxtChar(x, y);
+ }
+
+ private void drawOaleSeletLinesBg(Canvas canvas) {// 绘制选中背景
+ for (TxtLine l : mSelectLines) {
+ if (l.getCharsData() != null && l.getCharsData().size() > 0) {
+
+ TxtChar fistchar = l.getCharsData().get(0);
+ TxtChar lastchar = l.getCharsData().get(l.getCharsData().size() - 1);
+
+// float fw = fistchar.getCharWidth();
+// float lw = lastchar.getCharWidth();
+
+ RectF rect = new RectF(fistchar.getTopLeftPosition().x, fistchar.getTopLeftPosition().y,
+ lastchar.getTopRightPosition().x, lastchar.getBottomRightPosition().y);
+
+ /*canvas.drawRoundRect(rect, fw / 4,
+ textHeight /4, mTextSelectPaint);*/
+ canvas.drawRect(rect, mTextSelectPaint);
+ }
+ }
+ }
+
+ public TxtChar getFirstSelectTxtChar() {
+ return firstSelectTxtChar;
+ }
+ public void setFirstSelectTxtChar(TxtChar firstSelectTxtChar) {
+ this.firstSelectTxtChar = firstSelectTxtChar;
+ }
+
+ public TxtChar getLastSelectTxtChar() {
+ return lastSelectTxtChar;
+ }
+
+ public void setLastSelectTxtChar(TxtChar lastSelectTxtChar) {
+ this.lastSelectTxtChar = lastSelectTxtChar;
+ }
+
+ public float getTextHeight() {
+ return textHeight;
+ }
+
+ public enum SelectMode {
+ Normal, PressSelectText, SelectMoveForward, SelectMoveBack
+ }
public interface TouchListener {
boolean onTouch();
@@ -416,5 +680,9 @@ public class PageView extends View {
void nextPage(boolean hasNextChange);
void cancel();
+
+ void onTouchClearCursor();
+
+ void onLongPress();
}
}
diff --git a/app/src/main/java/xyz/fycz/myreader/widget/page/TxtChar.kt b/app/src/main/java/xyz/fycz/myreader/widget/page/TxtChar.kt
index f1cc31b..dd001fa 100644
--- a/app/src/main/java/xyz/fycz/myreader/widget/page/TxtChar.kt
+++ b/app/src/main/java/xyz/fycz/myreader/widget/page/TxtChar.kt
@@ -1,4 +1,4 @@
-package xyz.fycz.myreader.widget.page2
+package xyz.fycz.myreader.widget.page
import android.graphics.Point
diff --git a/app/src/main/java/xyz/fycz/myreader/widget/page/TxtLine.kt b/app/src/main/java/xyz/fycz/myreader/widget/page/TxtLine.kt
index 15c58c8..9667781 100644
--- a/app/src/main/java/xyz/fycz/myreader/widget/page/TxtLine.kt
+++ b/app/src/main/java/xyz/fycz/myreader/widget/page/TxtLine.kt
@@ -1,5 +1,7 @@
package xyz.fycz.myreader.widget.page2
+import xyz.fycz.myreader.widget.page.TxtChar
+
class TxtLine {
var charsData: List? = null
diff --git a/app/src/main/res/drawable/ic_change_source.xml b/app/src/main/res/drawable/ic_change.xml
similarity index 100%
rename from app/src/main/res/drawable/ic_change_source.xml
rename to app/src/main/res/drawable/ic_change.xml
diff --git a/app/src/main/res/drawable/ic_cursor_left.xml b/app/src/main/res/drawable/ic_cursor_left.xml
new file mode 100644
index 0000000..1656763
--- /dev/null
+++ b/app/src/main/res/drawable/ic_cursor_left.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_cursor_right.xml b/app/src/main/res/drawable/ic_cursor_right.xml
new file mode 100644
index 0000000..99734ea
--- /dev/null
+++ b/app/src/main/res/drawable/ic_cursor_right.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml
new file mode 100644
index 0000000..ac69ecc
--- /dev/null
+++ b/app/src/main/res/drawable/ic_delete.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_export.xml b/app/src/main/res/drawable/ic_export.xml
new file mode 100644
index 0000000..86ee47a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_export.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_import.xml b/app/src/main/res/drawable/ic_import.xml
new file mode 100644
index 0000000..1d13e07
--- /dev/null
+++ b/app/src/main/res/drawable/ic_import.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_replace.xml b/app/src/main/res/drawable/ic_replace.xml
new file mode 100644
index 0000000..dfd1451
--- /dev/null
+++ b/app/src/main/res/drawable/ic_replace.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_reverse.xml b/app/src/main/res/drawable/ic_reverse.xml
new file mode 100644
index 0000000..a70f878
--- /dev/null
+++ b/app/src/main/res/drawable/ic_reverse.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_swipe_left.xml b/app/src/main/res/drawable/ic_swipe_left.xml
new file mode 100644
index 0000000..5dc9220
--- /dev/null
+++ b/app/src/main/res/drawable/ic_swipe_left.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/app/src/main/res/layout/activity_more_setting.xml b/app/src/main/res/layout/activity_more_setting.xml
index b4d8ba8..668551a 100644
--- a/app/src/main/res/layout/activity_more_setting.xml
+++ b/app/src/main/res/layout/activity_more_setting.xml
@@ -174,7 +174,49 @@
android:clickable="false"
android:longClickable="false" />
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_replace.xml b/app/src/main/res/layout/dialog_replace.xml
new file mode 100644
index 0000000..43370c1
--- /dev/null
+++ b/app/src/main/res/layout/dialog_replace.xml
@@ -0,0 +1,221 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fingerprint_dialog.xml b/app/src/main/res/layout/fingerprint_dialog.xml
index 51d14be..9bd905b 100644
--- a/app/src/main/res/layout/fingerprint_dialog.xml
+++ b/app/src/main/res/layout/fingerprint_dialog.xml
@@ -57,11 +57,15 @@
android:layout_weight="1"
android:layout_height="50dp"
android:gravity="center"
- android:text="取消"
+ android:text="@string/cancel"
android:textColor="@color/textSecondary"
android:textSize="16sp"
/>
-
+
diff --git a/app/src/main/res/layout/gridview_book_detailed_item.xml b/app/src/main/res/layout/gridview_book_detailed_item.xml
index a1b7cae..656a08b 100644
--- a/app/src/main/res/layout/gridview_book_detailed_item.xml
+++ b/app/src/main/res/layout/gridview_book_detailed_item.xml
@@ -146,5 +146,4 @@
android:layout_alignParentBottom="true"
android:background="@color/sys_window_back"/>
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_replace_rule.xml b/app/src/main/res/layout/item_replace_rule.xml
new file mode 100644
index 0000000..a0c6688
--- /dev/null
+++ b/app/src/main/res/layout/item_replace_rule.xml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/main_bnv.xml b/app/src/main/res/menu/main_bnv.xml
index 2220570..4090abf 100644
--- a/app/src/main/res/menu/main_bnv.xml
+++ b/app/src/main/res/menu/main_bnv.xml
@@ -6,14 +6,14 @@
+ android:title="@string/bookcase"/>
+ android:title="@string/find"/>
+ android:title="@string/mine"/>
diff --git a/app/src/main/res/menu/menu_book.xml b/app/src/main/res/menu/menu_book.xml
index 7fee944..670d7ba 100644
--- a/app/src/main/res/menu/menu_book.xml
+++ b/app/src/main/res/menu/menu_book.xml
@@ -17,7 +17,7 @@
app:showAsAction="always"/>
diff --git a/app/src/main/res/menu/menu_read.xml b/app/src/main/res/menu/menu_read.xml
index 98d7960..e9f545c 100644
--- a/app/src/main/res/menu/menu_read.xml
+++ b/app/src/main/res/menu/menu_read.xml
@@ -5,7 +5,7 @@
+
-
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 38fabc8..f70cf96 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -296,6 +296,42 @@
此书架的书籍仅能在该分组内阅读、查看及其他操作,且阅读后不会保存上次阅读记录。\n\n2、如何进入私密书架?\n在书架页面长按左上角书架标题正确验证隐私密码即可进入私密书架。
\n\n3、如何退出私密书架?\n点击书架菜单栏切换分组按钮切换至其他分组即可退出私密书架。\n\n4、忘记隐私密码怎么办?\n隐私密码不支持找回,忘记密码后可强制关闭私密书架,并删除私密书架内所有书籍。\n\n
5、此功能处于测试阶段,如发现问题,请到我的-建议反馈里进行反馈。
+ 长按选择文字
+ 搜索
+ 发声
+ 替换
+ 拷贝
+ 内容替换
+ 概要
+ 说明信息
+ 替换前
+ 要替换的内容
+ 使用正则
+ 替换为
+ 替换后的内容
+ 应用于书源
+ 默认将应用于所有书源
+ 选择
+ 应用于书籍
+ 默认将应用于所有书籍
+ 取消
+ 确定
+ 使用密码
+ 本地书籍
+ 删除
+ 禁用
+ 置顶
+ 启用
+ 内容替换规则
+ 书架
+ 发现
+ 我
+ 新建规则
+ 导出所有
+ 反转规则可用性
+ 删除禁用规则
+ 导入规则
+ 帮助
- 常亮
@@ -360,4 +396,15 @@
- 下载后面全部章
- 下载本书所有章
+
+
+ - 使用[百度]
+ - 使用[谷歌]
+ - 使用[有道词典]
+
+
+
+ - 从剪切板导入
+ - 从文件导入
+
diff --git a/app/version_code.properties b/app/version_code.properties
index e10469d..7d82364 100644
--- a/app/version_code.properties
+++ b/app/version_code.properties
@@ -1,2 +1,2 @@
-#Sat Jan 09 20:56:54 CST 2021
-VERSION_CODE=178
+#Tue Jan 19 16:58:48 CST 2021
+VERSION_CODE=179