From d230c1029d438f05f696fb0b80a6fc9f165b8a4a Mon Sep 17 00:00:00 2001 From: fengyuecanzhu <1021300691@qq.com> Date: Tue, 19 Jan 2021 18:05:29 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=95=BF=E6=8C=89=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E3=80=81=E5=86=85=E5=AE=B9=E6=9B=BF=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/assetWizardSettings.xml | 4 +- .idea/caches/build_file_checksums.ser | Bin 537 -> 537 bytes README.md | 2 +- app/build.gradle | 3 +- app/src/main/AndroidManifest.xml | 1 + app/src/main/assets/updatelog.fy | 9 +- .../xyz/fycz/myreader/common/APPCONST.java | 3 +- .../xyz/fycz/myreader/common/URLCONST.java | 5 + .../xyz/fycz/myreader/entity/Setting.java | 20 + .../xyz/fycz/myreader/enums/BookSource.java | 6 +- .../greendao/entity/ReplaceRuleBean.java | 1 + .../myreader/model/ReplaceRuleManager.java | 97 ++ .../ui/activity/MoreSettingActivity.java | 23 + .../myreader/ui/activity/ReadActivity.java | 360 ++++- .../myreader/ui/activity/RuleActivity.java | 265 ++++ .../ui/adapter/ReplaceRuleAdapter.java | 37 + .../ui/adapter/holder/ReplaceRuleHolder.java | 144 ++ .../myreader/ui/dialog/ReplaceDialog.java | 218 +++ .../xyz/fycz/myreader/util/ShareUtils.java | 22 +- .../util/help/ChapterContentHelp.java | 1252 ++++++++++++++++- .../myreader/util/utils/ClipBoardUtil.java | 68 + .../fycz/myreader/util/utils/FileUtils.java | 16 + .../fycz/myreader/util/utils/StringUtils.java | 10 + .../webapi/crawler/ReadCrawlerUtil.java | 13 +- .../crawler/read/TianLaiReadCrawler.java | 2 +- .../fycz/myreader/widget/BubblePopupView.java | 642 +++++++++ .../fycz/myreader/widget/page/PageLoader.java | 158 ++- .../fycz/myreader/widget/page/PageView.java | 282 +++- .../xyz/fycz/myreader/widget/page/TxtChar.kt | 2 +- .../xyz/fycz/myreader/widget/page/TxtLine.kt | 2 + .../{ic_change_source.xml => ic_change.xml} | 0 app/src/main/res/drawable/ic_cursor_left.xml | 11 + app/src/main/res/drawable/ic_cursor_right.xml | 11 + app/src/main/res/drawable/ic_delete.xml | 9 + app/src/main/res/drawable/ic_export.xml | 15 + app/src/main/res/drawable/ic_import.xml | 15 + app/src/main/res/drawable/ic_replace.xml | 9 + app/src/main/res/drawable/ic_reverse.xml | 9 + app/src/main/res/drawable/ic_swipe_left.xml | 12 + .../main/res/layout/activity_more_setting.xml | 42 + app/src/main/res/layout/activity_read.xml | 20 +- app/src/main/res/layout/activity_rule.xml | 16 + app/src/main/res/layout/dialog_replace.xml | 221 +++ .../main/res/layout/fingerprint_dialog.xml | 10 +- .../layout/gridview_book_detailed_item.xml | 1 - app/src/main/res/layout/item_replace_rule.xml | 79 ++ app/src/main/res/menu/main_bnv.xml | 6 +- app/src/main/res/menu/menu_book.xml | 2 +- app/src/main/res/menu/menu_book_detail.xml | 2 +- app/src/main/res/menu/menu_read.xml | 7 +- app/src/main/res/menu/menu_rule.xml | 29 + app/src/main/res/values/strings.xml | 47 + app/version_code.properties | 4 +- 53 files changed, 4143 insertions(+), 101 deletions(-) create mode 100644 app/src/main/java/xyz/fycz/myreader/ui/activity/RuleActivity.java create mode 100644 app/src/main/java/xyz/fycz/myreader/ui/adapter/ReplaceRuleAdapter.java create mode 100644 app/src/main/java/xyz/fycz/myreader/ui/adapter/holder/ReplaceRuleHolder.java create mode 100644 app/src/main/java/xyz/fycz/myreader/ui/dialog/ReplaceDialog.java create mode 100644 app/src/main/java/xyz/fycz/myreader/util/utils/ClipBoardUtil.java create mode 100644 app/src/main/java/xyz/fycz/myreader/widget/BubblePopupView.java rename app/src/main/res/drawable/{ic_change_source.xml => ic_change.xml} (100%) create mode 100644 app/src/main/res/drawable/ic_cursor_left.xml create mode 100644 app/src/main/res/drawable/ic_cursor_right.xml create mode 100644 app/src/main/res/drawable/ic_delete.xml create mode 100644 app/src/main/res/drawable/ic_export.xml create mode 100644 app/src/main/res/drawable/ic_import.xml create mode 100644 app/src/main/res/drawable/ic_replace.xml create mode 100644 app/src/main/res/drawable/ic_reverse.xml create mode 100644 app/src/main/res/drawable/ic_swipe_left.xml create mode 100644 app/src/main/res/layout/activity_rule.xml create mode 100644 app/src/main/res/layout/dialog_replace.xml create mode 100644 app/src/main/res/layout/item_replace_rule.xml create mode 100644 app/src/main/res/menu/menu_rule.xml 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 a93c5b7d2299f655cb0e39b10152690b0ab58e63..310065277bde198e608cfa720ab68b5881e2407d 100644 GIT binary patch delta 36 ucmV+<0NekW1epYomj!niD+7CxoOcl4({re)_a5Re93o6$UnBC9BLVgZ#}4`c delta 36 ucmV+<0NekW1epYomj!m^QmBKGoOck%K$drx9rUA!;kJw3)+aENBLVg~ + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +