新增长按选择、内容替换

pull/5/head
fengyuecanzhu 4 years ago
parent 87070d52ca
commit d230c1029d
  1. 4
      .idea/assetWizardSettings.xml
  2. BIN
      .idea/caches/build_file_checksums.ser
  3. 2
      README.md
  4. 3
      app/build.gradle
  5. 1
      app/src/main/AndroidManifest.xml
  6. 9
      app/src/main/assets/updatelog.fy
  7. 3
      app/src/main/java/xyz/fycz/myreader/common/APPCONST.java
  8. 5
      app/src/main/java/xyz/fycz/myreader/common/URLCONST.java
  9. 20
      app/src/main/java/xyz/fycz/myreader/entity/Setting.java
  10. 6
      app/src/main/java/xyz/fycz/myreader/enums/BookSource.java
  11. 1
      app/src/main/java/xyz/fycz/myreader/greendao/entity/ReplaceRuleBean.java
  12. 97
      app/src/main/java/xyz/fycz/myreader/model/ReplaceRuleManager.java
  13. 23
      app/src/main/java/xyz/fycz/myreader/ui/activity/MoreSettingActivity.java
  14. 358
      app/src/main/java/xyz/fycz/myreader/ui/activity/ReadActivity.java
  15. 265
      app/src/main/java/xyz/fycz/myreader/ui/activity/RuleActivity.java
  16. 37
      app/src/main/java/xyz/fycz/myreader/ui/adapter/ReplaceRuleAdapter.java
  17. 144
      app/src/main/java/xyz/fycz/myreader/ui/adapter/holder/ReplaceRuleHolder.java
  18. 218
      app/src/main/java/xyz/fycz/myreader/ui/dialog/ReplaceDialog.java
  19. 22
      app/src/main/java/xyz/fycz/myreader/util/ShareUtils.java
  20. 1248
      app/src/main/java/xyz/fycz/myreader/util/help/ChapterContentHelp.java
  21. 68
      app/src/main/java/xyz/fycz/myreader/util/utils/ClipBoardUtil.java
  22. 16
      app/src/main/java/xyz/fycz/myreader/util/utils/FileUtils.java
  23. 10
      app/src/main/java/xyz/fycz/myreader/util/utils/StringUtils.java
  24. 13
      app/src/main/java/xyz/fycz/myreader/webapi/crawler/ReadCrawlerUtil.java
  25. 2
      app/src/main/java/xyz/fycz/myreader/webapi/crawler/read/TianLaiReadCrawler.java
  26. 642
      app/src/main/java/xyz/fycz/myreader/widget/BubblePopupView.java
  27. 158
      app/src/main/java/xyz/fycz/myreader/widget/page/PageLoader.java
  28. 276
      app/src/main/java/xyz/fycz/myreader/widget/page/PageView.java
  29. 2
      app/src/main/java/xyz/fycz/myreader/widget/page/TxtChar.kt
  30. 2
      app/src/main/java/xyz/fycz/myreader/widget/page/TxtLine.kt
  31. 0
      app/src/main/res/drawable/ic_change.xml
  32. 11
      app/src/main/res/drawable/ic_cursor_left.xml
  33. 11
      app/src/main/res/drawable/ic_cursor_right.xml
  34. 9
      app/src/main/res/drawable/ic_delete.xml
  35. 15
      app/src/main/res/drawable/ic_export.xml
  36. 15
      app/src/main/res/drawable/ic_import.xml
  37. 9
      app/src/main/res/drawable/ic_replace.xml
  38. 9
      app/src/main/res/drawable/ic_reverse.xml
  39. 12
      app/src/main/res/drawable/ic_swipe_left.xml
  40. 42
      app/src/main/res/layout/activity_more_setting.xml
  41. 20
      app/src/main/res/layout/activity_read.xml
  42. 16
      app/src/main/res/layout/activity_rule.xml
  43. 221
      app/src/main/res/layout/dialog_replace.xml
  44. 10
      app/src/main/res/layout/fingerprint_dialog.xml
  45. 1
      app/src/main/res/layout/gridview_book_detailed_item.xml
  46. 79
      app/src/main/res/layout/item_replace_rule.xml
  47. 6
      app/src/main/res/menu/main_bnv.xml
  48. 2
      app/src/main/res/menu/menu_book.xml
  49. 2
      app/src/main/res/menu/menu_book_detail.xml
  50. 7
      app/src/main/res/menu/menu_read.xml
  51. 29
      app/src/main/res/menu/menu_rule.xml
  52. 47
      app/src/main/res/values/strings.xml
  53. 4
      app/version_code.properties

@ -19,8 +19,8 @@
<option name="values">
<map>
<entry key="assetSourceType" value="FILE" />
<entry key="outputName" value="ic_share" />
<entry key="sourceFile" value="F:\SVG图标\分享.svg" />
<entry key="outputName" value="ic_delete" />
<entry key="sourceFile" value="F:\SVG图标\删 除.svg" />
</map>
</option>
</PersistentState>

@ -2,7 +2,7 @@
风月读书,一款开源、无广告的小说阅读软件。
成品下载(v1.7.8):[https://fycz.lanzous.com/ieSvwk7vucf](https://fycz.lanzous.com/ieSvwk7vucf)
成品下载(v1.7.9):[https://fycz.lanzous.com/icrU3kkqrud](https://fycz.lanzous.com/icrU3kkqrud)
#### 一、关于书源

@ -200,7 +200,8 @@ dependencies {
implementation "com.afollestad.material-dialogs:input:$dialog_version"
implementation "com.afollestad.material-dialogs:files:$dialog_version"
implementation "com.afollestad.material-dialogs:bottomsheets:$dialog_version"*/
//
implementation 'com.github.mcxtzhang:SwipeDelMenuLayout:V1.3.0'
}
greendao {

@ -101,6 +101,7 @@
<activity android:name=".ui.activity.FileSystemActivity"/>
<activity android:name=".ui.activity.FeedbackActivity"/>
<activity android:name=".ui.activity.RuleActivity"/>
<activity android:name=".ui.activity.MoreSettingActivity"/>
<activity android:name=".ui.activity.BookstoreActivity"/>

@ -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

@ -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;

@ -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}";
}

@ -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;
}
}

@ -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) {

@ -166,4 +166,5 @@ public class ReplaceRuleBean implements Parcelable {
public void setIsRegex(Boolean isRegex) {
this.isRegex = isRegex;
}
}

@ -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<Boolean> 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<ReplaceRuleBean> 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<Boolean>) 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^])([…,,::?。!?!~<>《》【】()()]+)(?![\\p{P}\n$])", "\n")
;
String[] lines = string.split("\n");
List<String> 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<List<ReplaceRuleBean>> getAll() {
return Single.create((SingleOnSubscribe<List<ReplaceRuleBean>>) emitter -> emitter.onSuccess(GreenDaoManager.getDaoSession()
.getReplaceRuleBeanDao().queryBuilder()
@ -42,6 +131,13 @@ public class ReplaceRuleManager {
.list())).compose(RxUtils::toSimpleSingle);
}
public static List<ReplaceRuleBean> getAllRules() {
return GreenDaoManager.getDaoSession()
.getReplaceRuleBeanDao().queryBuilder()
.orderAsc(ReplaceRuleBeanDao.Properties.SerialNumber)
.list();
}
public static Single<Boolean> saveData(ReplaceRuleBean replaceRuleBean) {
return Single.create((SingleOnSubscribe<Boolean>) emitter -> {
if (replaceRuleBean.getSerialNumber() == 0) {
@ -111,4 +207,5 @@ public class ReplaceRuleManager {
e.onComplete();
});
}
}

@ -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) {

@ -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;
@ -960,7 +1018,7 @@ public class ReadActivity extends BaseActivity implements ColorPickerDialogListe
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<String> 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 = "";
}
}

@ -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<ReplaceRuleBean> 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<List<ReplaceRuleBean>>() {
@Override
public void onSuccess(@NonNull List<ReplaceRuleBean> 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<ReplaceRuleBean> 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<Boolean> observable = ReplaceRuleManager.importReplaceRule(text);
if (observable != null) {
observable.subscribe(new MyObserver<Boolean>() {
@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);
}
}

@ -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<ReplaceRuleBean> {
private AppCompatActivity activity;
private OnDeleteListener onDeleteListener;
public ReplaceRuleAdapter(AppCompatActivity activity, OnDeleteListener onDeleteListener) {
this.activity = activity;
this.onDeleteListener = onDeleteListener;
}
@Override
protected IViewHolder<ReplaceRuleBean> 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);
}
}

@ -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<ReplaceRuleBean> {
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<Boolean>() {
@Override
public void onSuccess(@NonNull Boolean aBoolean) {
if (aBoolean) {
banOrUse(data);
refreshUI();
}
}
});
});
btShare.setOnClickListener(v -> {
((SwipeMenuLayout) getItemView()).smoothClose();
List<ReplaceRuleBean> shareRuleBean = new ArrayList<>();
shareRuleBean.add(data);
ShareUtils.share(activity, GsonExtensionsKt.getGSON().toJson(shareRuleBean));
});
btDelete.setOnClickListener(v -> {
((SwipeMenuLayout) getItemView()).smoothClose();
Observable.create((ObservableOnSubscribe<Boolean>) e -> {
ReplaceRuleManager.delData(data);
e.onNext(true);
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new MyObserver<Boolean>() {
@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);
}
}

@ -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<Boolean>() {
@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<String> mSources = ReadCrawlerUtil.getAllSources();
CharSequence[] mSourcesName = new CharSequence[mSources.size()];
HashMap<CharSequence, Boolean> 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<Book> mBooks = BookService.getInstance().getAllBooksNoHide();
HashMap<CharSequence, Boolean> 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();
}
}

@ -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));
}
}

@ -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();
}
}
}
}

@ -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,

@ -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;

@ -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<String> getAllSources(){
List<String> sources = new ArrayList<>();
for (BookSource bookSource : BookSource.values()) {
if (bookSource.equals(BookSource.fynovel))
continue;
sources.add(bookSource.text);
}
return sources;
}
public static HashMap<CharSequence, Boolean> 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();

@ -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 "";

@ -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<String> 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<String> 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);
}
}

@ -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<TxtChar>());
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<TxtLine> txtLines = txtPage.txtLists;
if (txtLines == null) return null;
for (TxtLine l : txtLines) {
List<TxtChar> 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 {

@ -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<TxtLine> 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 (firstSelectTxtChar == null) {
if (mTouchListener != null) {
mTouchListener.center();
}
} else {
if (mSelectTextPath != null) {//长安选择删除选中状态
if (!isLongPress) {
firstSelectTxtChar = null;
mSelectTextPath.reset();
invalidate();
}
}
//清除移动选择状态
}
return true;
}
}
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<TxtLine> 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();
}
}

@ -1,4 +1,4 @@
package xyz.fycz.myreader.widget.page2
package xyz.fycz.myreader.widget.page
import android.graphics.Point

@ -1,5 +1,7 @@
package xyz.fycz.myreader.widget.page2
import xyz.fycz.myreader.widget.page.TxtChar
class TxtLine {
var charsData: List<TxtChar>? = null

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#595757"
android:pathData="M12,4h8v8c0,4.418-3.582,8-8,8s-8-3.582-8-8S7.582,4,12,4z" />
</vector>

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#595757"
android:pathData="M12,4.208H4v8c0,4.418,3.582,8,8,8s8-3.582,8-8S16.418,4.208,12,4.208z" />
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M733.86,791.95c0,26.57 -21.63,48.2 -48.2,48.2L338.35,840.15c-26.57,0 -48.2,-21.63 -48.2,-48.2L290.14,280.26h443.71v511.69zM386.55,193.74c0,-5.56 4.33,-9.89 9.89,-9.89h231.74c5.56,0 9.89,4.33 9.89,9.89v29.05L386.55,222.78L386.55,193.74zM878.46,222.78L695.54,222.78L695.54,193.74c0,-37.08 -30.28,-67.36 -67.36,-67.36h-231.74c-37.08,0 -67.36,30.28 -67.36,67.36v29.05L145.54,222.78C129.47,222.78 116.49,235.76 116.49,251.83s12.98,29.05 29.05,29.05h86.52v511.07c0,58.71 47.58,106.29 106.29,106.29h347.31c58.71,0 106.29,-47.58 106.29,-106.29L791.95,280.26h86.52c16.07,0 29.05,-12.98 29.05,-29.05s-12.98,-28.43 -29.05,-28.43zM512,753.01c16.07,0 29.05,-12.98 29.05,-29.05v-308.99c0,-16.07 -12.98,-29.05 -29.05,-29.05s-29.05,12.98 -29.05,29.05v308.99c0,16.07 12.98,29.05 29.05,29.05m-135.34,0c16.07,0 29.05,-12.98 29.05,-29.05v-308.99c0,-16.07 -12.98,-29.05 -29.05,-29.05s-29.05,12.98 -29.05,29.05v308.99c0.62,16.07 13.6,29.05 29.05,29.05m270.68,0c16.07,0 29.05,-12.98 29.05,-29.05v-308.99c0,-16.07 -12.98,-29.05 -29.05,-29.05s-29.05,12.98 -29.05,29.05v308.99c0,16.07 12.98,29.05 29.05,29.05"
android:fillColor="#FFFFFF"/>
</vector>

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M778.75,845.31H245.25c-53.76,0 -97.28,-43.52 -97.28,-97.28V275.46c0,-53.76 43.52,-97.28 97.28,-97.28h131.07c17.92,0 35.33,8.19 46.59,22.02l57.34,69.12h303.62c50.69,0 91.65,40.96 91.65,91.65v387.07c0.51,53.76 -43.52,97.28 -96.77,97.28zM245.25,239.1c-19.97,0 -36.35,16.38 -36.35,36.35v472.58c0,19.97 16.38,36.35 36.35,36.35h533.5c19.97,0 36.35,-16.38 36.35,-36.35V360.96c0,-16.9 -13.82,-30.72 -31.23,-30.72H480.77c-17.92,0 -35.33,-8.19 -46.59,-22.02L376.83,239.1H245.25z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M614.4,584.4c-8.7,0 -17.4,-3.4 -24.2,-10L512,496.2l-78.2,78.2c-13.3,13.4 -35,13.4 -48.2,0 -13.4,-13.4 -13.4,-35 0,-48.2L488,423.8c13.3,-13.4 35,-13.4 48.2,0l102.4,102.4c13.4,13.4 13.4,35 0,48.2 -6.8,6.8 -15.5,10 -24.2,10zM614.4,584.4"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M512.51,424.45c16.9,0 30.21,13.82 30.21,30.21v211.97c0,16.9 -13.82,30.21 -30.21,30.21 -16.9,0 -30.21,-13.82 -30.21,-30.21V454.66c0,-16.9 13.82,-30.21 30.21,-30.21z"
android:fillColor="#FFFFFF"/>
</vector>

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M778.75,845.31H245.25c-53.76,0 -97.28,-43.52 -97.28,-97.28V275.46c0,-53.76 43.52,-97.28 97.28,-97.28h131.07c17.92,0 35.33,8.19 46.59,22.02l57.34,69.12h303.62c50.69,0 91.65,40.96 91.65,91.65v387.07c0.51,53.76 -43.52,97.28 -96.77,97.28zM245.25,239.1c-19.97,0 -36.35,16.38 -36.35,36.35v472.58c0,19.97 16.38,36.35 36.35,36.35h533.5c19.97,0 36.35,-16.38 36.35,-36.35V360.96c0,-16.9 -13.82,-30.72 -31.23,-30.72H480.77c-17.92,0 -35.33,-8.19 -46.59,-22.02L376.83,239.1H245.25z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M421.89,545.79c7.68,0 15.36,3.07 21.5,8.7l69.63,69.63 69.63,-69.63c11.78,-11.78 31.23,-11.78 43.01,0 11.78,11.78 11.78,31.23 0,43.01l-91.14,91.14c-11.78,11.78 -31.23,11.78 -43.01,0l-91.14,-91.14c-11.78,-11.78 -11.78,-31.23 0,-43.01 5.63,-6.14 13.82,-8.7 21.5,-8.7z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M512.51,424.45c16.9,0 30.21,13.82 30.21,30.21v211.97c0,16.9 -13.82,30.21 -30.21,30.21 -16.9,0 -30.21,-13.82 -30.21,-30.21V454.66c0,-16.9 13.82,-30.21 30.21,-30.21z"
android:fillColor="#FFFFFF"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M410.11,449.31h-55.92l-39.94,-107.18L151.1,342.13l-37.55,107.18h-55.98l149.22,-391.68h54.1L410.17,449.31zM298.61,297.81l-59.16,-162.76a182.39,182.39 0,0 1,-6.14 -27.31L232.11,107.75a146.66,146.66 0,0 1,-6.26 27.31L167.03,297.81h131.47zM662.93,967.11v-395.95h137.56c41.07,0 73.96,8.31 98.42,24.92 24.63,16.67 36.92,38.68 36.92,66.1 0,21.79 -7.34,41.02 -21.96,57.8 -14.62,16.73 -34.7,28.44 -60.3,35.56v1.02c31.86,3.02 57.29,13.2 76.34,30.55 19.06,17.35 28.5,39.25 28.5,65.82 0,34.13 -14.45,61.67 -43.35,82.72 -28.79,20.99 -65.82,31.46 -110.93,31.46h-141.2zM723.46,615.88v124.02h55.01c29.18,0 52.17,-6.09 68.72,-18.03 16.33,-11.32 25.71,-30.15 25.03,-49.95 0,-37.32 -29.41,-55.92 -88.23,-55.92h-60.53v-0.11zM723.46,784.38v138.01h72.48c31.57,0 56.04,-6.14 73.39,-18.55a59.73,59.73 0,0 0,25.71 -51.48c0,-45.34 -36.41,-67.98 -109.11,-67.98h-62.46zM440.15,864.2l6.49,-38.17a353.62,353.62 0,0 1,-265.39 -311.98h64.68a291.9,291.9 0,0 0,212.31 248.55l9.73,-54.39 121.06,102.29 -148.88,53.7z"
android:fillColor="#FFFFFF"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M298.24,469.33L128,640l170.24,170.67v-128h299.09v-85.33H298.24v-128zM896,384L725.76,213.33v128H426.67v85.33H725.76v128L896,384z"
android:fillColor="#FFFFFF"/>
</vector>

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M618.92,469.52c-22.9,23.46 -22.9,61.5 0,84.97l116.95,128.1c22.91,23.46 60.05,23.46 82.96,0 22.9,-23.46 22.9,-61.5 0,-84.97l-75.57,-85.62 75.57,-85.62c22.9,-23.47 22.9,-61.51 0,-84.97 -22.91,-23.47 -60.05,-23.47 -82.96,0L618.92,469.52v-0z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M705.45,857.03L329.51,512l375.94,-345.03c22.91,-23.47 22.91,-61.51 0,-84.97 -22.9,-23.47 -60.04,-23.47 -82.96,0L205.17,469.52c-22.91,23.46 -22.91,61.5 0,84.97L622.49,942c22.91,23.46 60.05,23.46 82.95,0 22.91,-23.46 22.91,-61.5 0,-84.97z"
android:fillColor="#FFFFFF"/>
</vector>

@ -174,7 +174,49 @@
android:clickable="false"
android:longClickable="false" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/rl_long_press"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@drawable/selector_common_bg"
android:gravity="center"
android:paddingLeft="20dp"
android:paddingRight="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:text="@string/long_press"
android:textColor="@color/textSecondary"
android:textSize="@dimen/text_normal_size" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/sc_long_press"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:clickable="false"
android:longClickable="false" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/rl_content_replace"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@drawable/selector_common_bg"
android:paddingLeft="20dp"
android:paddingRight="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:text="@string/content_replace"
android:textColor="@color/textSecondary"
android:textSize="@dimen/text_normal_size" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/rl_read_aloud_volume_turn_page"
android:layout_width="match_parent"

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<RelativeLayout
android:id="@+id/rl_content"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
@ -86,7 +88,23 @@
android:visibility="gone"
tools:visibility="visible" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/cursor_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
app:tint="@color/colorAccent"
app:srcCompat="@drawable/ic_cursor_left"
android:visibility="invisible" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/cursor_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
app:tint="@color/colorAccent"
app:srcCompat="@drawable/ic_cursor_right"
android:visibility="invisible" />
<!--底部页面-->
<LinearLayout
android:id="@+id/read_ll_bottom_menu"

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/toolbar"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_rule_list"
android:background="@color/colorForeground"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>

@ -0,0 +1,221 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorForeground"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="5dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="10dp"
android:text="@string/content_replace"
android:textColor="@color/textPrimary"
android:textSize="@dimen/text_medium_size" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="5dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/summary"
android:textColor="@color/textPrimary"
android:textSize="@dimen/text_normal_size" />
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/et_rule_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/summary_tip"
android:maxLines="3"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:textSize="@dimen/text_small_size" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="5dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/replace_rule_old"
android:textColor="@color/textPrimary"
android:textSize="@dimen/text_normal_size" />
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/et_rule_old"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/replace_rule_old_tip"
android:maxLines="3"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:textSize="@dimen/text_small_size" />
</LinearLayout>
<CheckBox
android:id="@+id/cb_use_regex"
android:layout_width="match_parent"
android:layout_height="20dp"
android:text="@string/replace_use_regex"
android:textColor="@color/textPrimary"
android:theme="@style/MyCheckBox" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="5dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/replace_rule_new"
android:textColor="@color/textPrimary"
android:textSize="@dimen/text_normal_size" />
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/et_rule_new"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/replace_rule_new_tip"
android:maxLines="3"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:textSize="@dimen/text_small_size" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="5dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/use_to_source"
android:textColor="@color/textPrimary"
android:textSize="@dimen/text_normal_size" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/et_rule_source"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:hint="@string/use_to_source_tip"
android:maxLines="3"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:textSize="@dimen/text_small_size" />
<Button
android:id="@+id/bt_select_source"
android:layout_width="48dp"
android:layout_height="35dp"
android:layout_alignParentEnd="true"
android:text="@string/select"
android:textSize="@dimen/text_default_size" />
</RelativeLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="5dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/use_to_book"
android:textColor="@color/textPrimary"
android:textSize="@dimen/text_normal_size" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/et_rule_book"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:hint="@string/use_to_book_tip"
android:maxLines="3"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:textSize="@dimen/text_small_size" />
<Button
android:id="@+id/bt_select_book"
android:layout_width="48dp"
android:layout_height="35dp"
android:layout_alignParentEnd="true"
android:text="@string/select"
android:textSize="@dimen/text_default_size" />
</RelativeLayout>
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="@color/colorBackground" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_cancel"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_weight="1"
android:background="@drawable/selector_common_bg"
android:gravity="center"
android:text="@string/cancel"
android:textColor="@color/textPrimary"
android:textSize="16sp" />
<View
android:layout_width="0.5dp"
android:layout_height="match_parent"
android:background="@color/colorBackground" />
<TextView
android:id="@+id/tv_confirm"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_weight="1"
android:background="@drawable/selector_common_bg"
android:gravity="center"
android:text="@string/confirm"
android:textColor="@color/textPrimary"
android:textSize="16sp" />
</LinearLayout>
</LinearLayout>

@ -57,11 +57,15 @@
android:layout_weight="1"
android:layout_height="50dp"
android:gravity="center"
android:text="取消"
android:text="@string/cancel"
android:textColor="@color/textSecondary"
android:textSize="16sp"
/>
<View
android:layout_width="0.5dp"
android:layout_height="match_parent"
android:background="@color/colorBackground"
/>
<TextView
android:id="@+id/use_pwd"
android:background="@drawable/selector_common_bg"
@ -69,7 +73,7 @@
android:layout_weight="1"
android:layout_height="50dp"
android:gravity="center"
android:text="使用密码"
android:text="@string/use_pwd"
android:textColor="@color/textSecondary"
android:textSize="16sp"
/>

@ -146,5 +146,4 @@
android:layout_alignParentBottom="true"
android:background="@color/sys_window_back"/>
</RelativeLayout>

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<com.mcxtzhang.swipemenulib.SwipeMenuLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="50dp"
android:clickable="true"
android:paddingBottom="1dp"
app:ios="false"
app:leftSwipe="true"
app:swipeEnable="true"
android:focusable="true">
<RelativeLayout
android:id="@+id/rl_content"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/selectableItemBackground">
<TextView
android:id="@+id/tv_rule_summary"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_toStartOf="@+id/iv_swipe_left"
android:textColor="@color/textPrimary"
android:maxLines="1"
android:ellipsize="end"
android:layout_alignParentStart="true"
android:gravity="center_vertical"
android:text="@string/summary" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_swipe_left"
android:layout_width="13dp"
android:layout_height="13dp"
android:layout_centerVertical="true"
android:layout_alignParentEnd="true"
app:tint="@color/textAssist"
app:srcCompat="@drawable/ic_swipe_left"/>
</RelativeLayout>
<!-- 以下都是侧滑菜单的内容依序排列 -->
<!--<Button
android:id="@+id/bt_top"
android:layout_width="65dp"
android:layout_height="match_parent"
android:background="@color/md_purple_500"
android:text="@string/top"
android:textColor="@android:color/white"/>-->
<Button
android:id="@+id/bt_ban"
android:layout_width="65dp"
android:layout_height="match_parent"
android:background="@color/md_blue_500"
android:text="@string/ban"
android:textColor="@android:color/white"/>
<Button
android:id="@+id/bt_share"
android:layout_width="65dp"
android:layout_height="match_parent"
android:background="@color/md_yellow_800"
android:clickable="true"
android:text="@string/share"
android:textColor="@android:color/white"
android:focusable="true" />
<Button
android:id="@+id/btnDelete"
android:layout_width="65dp"
android:layout_height="match_parent"
android:background="@color/md_red_500"
android:text="@string/delete"
android:textColor="@android:color/white"/>
</com.mcxtzhang.swipemenulib.SwipeMenuLayout>

@ -6,14 +6,14 @@
<item
android:id="@+id/menu_bookshelf"
android:icon="@drawable/ic_bookshelf"
android:title="书架"/>
android:title="@string/bookcase"/>
<item
android:id="@+id/menu_find_book"
android:icon="@drawable/ic_dicovery"
android:title="发现"/>
android:title="@string/find"/>
<item
android:id="@+id/menu_my_config"
android:icon="@drawable/ic_mine"
android:title=""/>
android:title="@string/mine"/>
</menu>

@ -17,7 +17,7 @@
app:showAsAction="always"/>
<item
android:id="@+id/action_change_group"
android:icon="@drawable/ic_change_source"
android:icon="@drawable/ic_change"
android:title="@string/menu_change_group"
app:showAsAction="always"/>
<item

@ -5,7 +5,7 @@
<item
android:id="@+id/action_change_source"
android:icon="@drawable/ic_change_source"
android:icon="@drawable/ic_change"
android:title="@string/menu_change_source"
app:showAsAction="ifRoom"/>

@ -5,7 +5,7 @@
<item
android:id="@+id/action_change_source"
android:icon="@drawable/ic_change_source"
android:icon="@drawable/ic_change"
android:title="@string/menu_change_source"
app:showAsAction="ifRoom"/>
<item
@ -25,6 +25,11 @@
android:icon="@drawable/ic_bookmark"
android:title="@string/menu_add_bookmark"
app:showAsAction="never"/>
<item
android:id="@+id/action_replace_content"
android:icon="@drawable/ic_replace"
android:title="@string/content_replace"
app:showAsAction="never"/>
<item
android:id="@+id/action_copy_content"
android:icon="@drawable/ic_copy"

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:showIn="bottom_navigation_view">
<item
android:id="@+id/action_add_rule"
android:icon="@drawable/ic_add"
android:title="@string/add_rule"/>
<item
android:id="@+id/action_import"
android:icon="@drawable/ic_import"
android:title="@string/import_rule"/>
<item
android:id="@+id/action_export"
android:icon="@drawable/ic_export"
android:title="@string/export_all"/>
<item
android:id="@+id/action_reverse"
android:icon="@drawable/ic_reverse"
android:title="@string/reverse_rule"/>
<item
android:id="@+id/action_delete"
android:icon="@drawable/ic_delete"
android:title="@string/delete_ban_rule"/>
</menu>

@ -296,6 +296,42 @@
此书架的书籍仅能在该分组内阅读、查看及其他操作,且阅读后不会保存上次阅读记录。\n\n2、如何进入私密书架?\n在书架页面长按左上角书架标题正确验证隐私密码即可进入私密书架。
\n\n3、如何退出私密书架?\n点击书架菜单栏切换分组按钮切换至其他分组即可退出私密书架。\n\n4、忘记隐私密码怎么办?\n隐私密码不支持找回,忘记密码后可强制关闭私密书架,并删除私密书架内所有书籍。\n\n
5、此功能处于测试阶段,如发现问题,请到我的-建议反馈里进行反馈。</string>
<string name="long_press">长按选择文字</string>
<string name="search">搜索</string>
<string name="speak">发声</string>
<string name="replace">替换</string>
<string name="copy">拷贝</string>
<string name="content_replace">内容替换</string>
<string name="summary">概要</string>
<string name="summary_tip">说明信息</string>
<string name="replace_rule_old">替换前</string>
<string name="replace_rule_old_tip">要替换的内容</string>
<string name="replace_use_regex">使用正则</string>
<string name="replace_rule_new">替换为</string>
<string name="replace_rule_new_tip">替换后的内容</string>
<string name="use_to_source">应用于书源</string>
<string name="use_to_source_tip">默认将应用于所有书源</string>
<string name="select">选择</string>
<string name="use_to_book">应用于书籍</string>
<string name="use_to_book_tip">默认将应用于所有书籍</string>
<string name="cancel">取消</string>
<string name="confirm">确定</string>
<string name="use_pwd">使用密码</string>
<string name="local_book">本地书籍</string>
<string name="delete">删除</string>
<string name="ban">禁用</string>
<string name="top">置顶</string>
<string name="enable_use">启用</string>
<string name="replace_rule">内容替换规则</string>
<string name="bookcase">书架</string>
<string name="find">发现</string>
<string name="mine"></string>
<string name="add_rule">新建规则</string>
<string name="export_all">导出所有</string>
<string name="reverse_rule">反转规则可用性</string>
<string name="delete_ban_rule">删除禁用规则</string>
<string name="import_rule">导入规则</string>
<string name="help">帮助</string>
<string-array name="reset_screen_time">
<item>常亮</item>
@ -360,4 +396,15 @@
<item>下载后面全部章</item>
<item>下载本书所有章</item>
</string-array>
<string-array name="search_way">
<item>使用[百度]</item>
<item>使用[谷歌]</item>
<item>使用[有道词典]</item>
</string-array>
<string-array name="import_rule">
<item>从剪切板导入</item>
<item>从文件导入</item>
</string-array>
</resources>

@ -1,2 +1,2 @@
#Sat Jan 09 20:56:54 CST 2021
VERSION_CODE=178
#Tue Jan 19 16:58:48 CST 2021
VERSION_CODE=179

Loading…
Cancel
Save