From 29cb92d7ee0527c8601398392c59dc636859826b Mon Sep 17 00:00:00 2001 From: fengyuecanzhu <1021300691@qq.com> Date: Sat, 15 May 2021 13:38:15 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=B9=A6=E6=BA=90=E7=BB=93?= =?UTF-8?q?=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/third2/analyzeRule/AnalyzeRule.java | 1 + .../fycz/myreader/util/utils/OkHttpUtils.java | 20 + .../xyz/fycz/myreader/webapi/BookApi.java | 137 ++--- .../fycz/myreader/webapi/ThirdSourceApi.java | 11 +- .../webapi/crawler/base/BaseReadCrawler.java | 18 +- .../crawler/base/BaseSourceCrawler.java | 4 +- .../webapi/crawler/base/BookInfoCrawler.java | 9 + .../scroller/FastScrollRecyclerView.java | 253 +++++++++ .../FastScrollStateChangeListener.java | 14 + .../widget/scroller/FastScroller.java | 531 ++++++++++++++++++ .../main/res/drawable/fastscroll_bubble.xml | 39 ++ .../main/res/drawable/fastscroll_handle.xml | 31 + .../main/res/drawable/fastscroll_track.xml | 27 + .../main/res/layout/activity_replace_rule.xml | 13 +- app/src/main/res/layout/fragment_catalog.xml | 67 +-- .../res/layout/fragment_import_source.xml | 15 +- .../main/res/layout/fragment_local_source.xml | 11 +- app/src/main/res/layout/view_fastscroller.xml | 50 ++ app/src/main/res/values/attrs.xml | 10 + app/src/main/res/values/dimens.xml | 16 + app/src/main/res/values/ids.xml | 3 + 21 files changed, 1155 insertions(+), 125 deletions(-) create mode 100644 app/src/main/java/xyz/fycz/myreader/widget/scroller/FastScrollRecyclerView.java create mode 100644 app/src/main/java/xyz/fycz/myreader/widget/scroller/FastScrollStateChangeListener.java create mode 100644 app/src/main/java/xyz/fycz/myreader/widget/scroller/FastScroller.java create mode 100644 app/src/main/res/drawable/fastscroll_bubble.xml create mode 100644 app/src/main/res/drawable/fastscroll_handle.xml create mode 100644 app/src/main/res/drawable/fastscroll_track.xml create mode 100644 app/src/main/res/layout/view_fastscroller.xml diff --git a/app/src/main/java/xyz/fycz/myreader/model/third2/analyzeRule/AnalyzeRule.java b/app/src/main/java/xyz/fycz/myreader/model/third2/analyzeRule/AnalyzeRule.java index 8add536..7417821 100644 --- a/app/src/main/java/xyz/fycz/myreader/model/third2/analyzeRule/AnalyzeRule.java +++ b/app/src/main/java/xyz/fycz/myreader/model/third2/analyzeRule/AnalyzeRule.java @@ -535,6 +535,7 @@ public class AnalyzeRule implements JsExtensions { bindings.put("java", this); bindings.put("result", result); bindings.put("baseUrl", baseUrl); + bindings.put("book", book); return SCRIPT_ENGINE.eval(jsStr, bindings); } diff --git a/app/src/main/java/xyz/fycz/myreader/util/utils/OkHttpUtils.java b/app/src/main/java/xyz/fycz/myreader/util/utils/OkHttpUtils.java index 94cd892..e204587 100644 --- a/app/src/main/java/xyz/fycz/myreader/util/utils/OkHttpUtils.java +++ b/app/src/main/java/xyz/fycz/myreader/util/utils/OkHttpUtils.java @@ -21,12 +21,14 @@ import xyz.fycz.myreader.application.App; import xyz.fycz.myreader.entity.StrResponse; import xyz.fycz.myreader.greendao.DbManager; import xyz.fycz.myreader.greendao.entity.CookieBean; +import xyz.fycz.myreader.greendao.entity.rule.BookSource; import xyz.fycz.myreader.model.third2.analyzeRule.AnalyzeUrl; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -287,6 +289,24 @@ public class OkHttpUtils { }); } + public static Map getCookies(BookSource bookSource) { + if (bookSource != null) { + return getCookies(bookSource.getSourceUrl()); + } + return new HashMap<>(); + } + + public static Map getCookies(String url) { + Map cookieMap = new HashMap<>(); + if (url != null) { + CookieBean cookie = DbManager.getDaoSession().getCookieBeanDao().load(url); + if (cookie != null) { + cookieMap.put("Cookie", cookie.getCookie()); + } + } + return cookieMap; + } + private static class Web { private String content; private String js = "document.documentElement.outerHTML"; diff --git a/app/src/main/java/xyz/fycz/myreader/webapi/BookApi.java b/app/src/main/java/xyz/fycz/myreader/webapi/BookApi.java index 6ddb06b..1f8a637 100644 --- a/app/src/main/java/xyz/fycz/myreader/webapi/BookApi.java +++ b/app/src/main/java/xyz/fycz/myreader/webapi/BookApi.java @@ -1,13 +1,21 @@ package xyz.fycz.myreader.webapi; +import org.jetbrains.annotations.NotNull; + import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.List; +import java.util.Map; import io.reactivex.Observable; +import io.reactivex.ObservableEmitter; +import io.reactivex.ObservableOnSubscribe; +import io.reactivex.ObservableSource; +import io.reactivex.functions.Function; import okhttp3.MediaType; import okhttp3.RequestBody; import xyz.fycz.myreader.entity.SearchBookBean; +import xyz.fycz.myreader.entity.StrResponse; import xyz.fycz.myreader.greendao.entity.Book; import xyz.fycz.myreader.greendao.entity.Chapter; import xyz.fycz.myreader.model.mulvalmap.ConMVMap; @@ -18,49 +26,10 @@ import xyz.fycz.myreader.webapi.crawler.base.ReadCrawler; import xyz.fycz.myreader.webapi.crawler.read.TianLaiReadCrawler; import xyz.fycz.myreader.webapi.crawler.source.ThirdCrawler; - -public class BookApi { - - /** - * 获取章节列表 - * - * @param book - */ - public static Observable> getBookChapters(Book book, final ReadCrawler rc) { - if (rc instanceof ThirdCrawler) { - return ThirdSourceApi.getBookChaptersByTC(book, (ThirdCrawler) rc); - } - String url = book.getChapterUrl(); - String charset = rc.getCharset(); - url = NetworkUtils.getAbsoluteURL(rc.getNameSpace(), url); - String finalUrl = url; - return Observable.create(emitter -> { - emitter.onNext(rc.getChaptersFromHtml(OkHttpUtils.getHtml(finalUrl, charset, rc.getHeaders()))); - emitter.onComplete(); - }); - } - - - /** - * 获取章节正文 - * - */ - - public static Observable getChapterContent(Chapter chapter, Book book, final ReadCrawler rc) { - if (rc instanceof ThirdCrawler) { - return ThirdSourceApi.getChapterContentByTC(chapter, book, (ThirdCrawler) rc); - } - String url = chapter.getUrl(); - String charset = rc.getCharset(); - url = NetworkUtils.getAbsoluteURL(rc.getNameSpace(), url); - String finalUrl = url; - return Observable.create(emitter -> { - emitter.onNext(rc.getContentFormHtml(OkHttpUtils.getHtml(finalUrl, charset, rc.getHeaders()))); - emitter.onComplete(); - }); - } +import static xyz.fycz.myreader.util.utils.OkHttpUtils.getCookies; +public class BookApi { /** * 搜索小说 * @@ -85,25 +54,22 @@ public class BookApi { } String finalCharset = charset; String finalKey = key; - return Observable.create(emitter -> { - try { - if (rc.isPost()) { - String url = rc.getSearchLink(); - String[] urlInfo = url.split(","); - url = urlInfo[0]; - String body = makeSearchUrl(urlInfo[1], finalKey); - MediaType mediaType = MediaType.parse("application/x-www-form-urlencoded"); - RequestBody requestBody = RequestBody.create(mediaType, body); - emitter.onNext(rc.getBooksFromSearchHtml(OkHttpUtils.getHtml(url, requestBody, finalCharset, rc.getHeaders()))); - } else { - emitter.onNext(rc.getBooksFromSearchHtml(OkHttpUtils.getHtml(makeSearchUrl(rc.getSearchLink(), finalKey), finalCharset, rc.getHeaders()))); - } - } catch (Exception e) { - e.printStackTrace(); - emitter.onError(e); + return Observable.create((ObservableOnSubscribe) emitter -> { + Map headers = rc.getHeaders(); + headers.putAll(getCookies(rc.getNameSpace())); + if (rc.isPost()) { + String url = rc.getSearchLink(); + String[] urlInfo = url.split(","); + url = urlInfo[0]; + String body = makeSearchUrl(urlInfo[1], finalKey); + MediaType mediaType = MediaType.parse("application/x-www-form-urlencoded"); + RequestBody requestBody = RequestBody.create(mediaType, body); + emitter.onNext(OkHttpUtils.getStrResponse(url, requestBody, finalCharset, headers)); + } else { + emitter.onNext(OkHttpUtils.getStrResponse(makeSearchUrl(rc.getSearchLink(), finalKey), finalCharset, headers)); } emitter.onComplete(); - }); + }).flatMap(response -> OkHttpUtils.setCookie(response, rc.getNameSpace())).flatMap(rc::getBooksFromStrResponse); } public static String makeSearchUrl(String url, String key) { @@ -111,24 +77,67 @@ public class BookApi { } - /** * 获取小说详细信息 * * @param book */ public static Observable getBookInfo(final Book book, final BookInfoCrawler bic) { - if (bic instanceof ThirdCrawler){ + if (bic instanceof ThirdCrawler) { return ThirdSourceApi.getBookInfoByTC(book, (ThirdCrawler) bic); } String url; url = book.getInfoUrl(); url = NetworkUtils.getAbsoluteURL(bic.getNameSpace(), url); String finalUrl = url; - return Observable.create(emitter -> { - emitter.onNext(bic.getBookInfo(OkHttpUtils.getHtml(finalUrl, bic.getCharset(), - ((ReadCrawler) bic).getHeaders()), book)); + return Observable.create((ObservableOnSubscribe) emitter -> { + Map headers = ((ReadCrawler) bic).getHeaders(); + headers.putAll(getCookies(bic.getNameSpace())); + emitter.onNext(OkHttpUtils.getStrResponse(finalUrl, bic.getCharset(), headers)); emitter.onComplete(); - }); + }).flatMap(response -> OkHttpUtils.setCookie(response, bic.getNameSpace())).flatMap(response -> bic.getBookInfo(response, book)); } + + /** + * 获取章节列表 + * + * @param book + */ + public static Observable> getBookChapters(Book book, final ReadCrawler rc) { + if (rc instanceof ThirdCrawler) { + return ThirdSourceApi.getBookChaptersByTC(book, (ThirdCrawler) rc); + } + String url = book.getChapterUrl(); + String charset = rc.getCharset(); + url = NetworkUtils.getAbsoluteURL(rc.getNameSpace(), url); + String finalUrl = url; + return Observable.create((ObservableOnSubscribe) emitter -> { + Map headers = rc.getHeaders(); + headers.putAll(getCookies(rc.getNameSpace())); + emitter.onNext(OkHttpUtils.getStrResponse(finalUrl, charset, headers)); + emitter.onComplete(); + }).flatMap(response -> OkHttpUtils.setCookie(response, rc.getNameSpace())).flatMap(rc::getChaptersFromStrResponse); + } + + + /** + * 获取章节正文 + */ + + public static Observable getChapterContent(Chapter chapter, Book book, final ReadCrawler rc) { + if (rc instanceof ThirdCrawler) { + return ThirdSourceApi.getChapterContentByTC(chapter, book, (ThirdCrawler) rc); + } + String url = chapter.getUrl(); + String charset = rc.getCharset(); + url = NetworkUtils.getAbsoluteURL(rc.getNameSpace(), url); + String finalUrl = url; + return Observable.create((ObservableOnSubscribe) emitter -> { + Map headers = rc.getHeaders(); + headers.putAll(getCookies(rc.getNameSpace())); + emitter.onNext(OkHttpUtils.getStrResponse(finalUrl, charset, headers)); + emitter.onComplete(); + }).flatMap(response -> OkHttpUtils.setCookie(response, rc.getNameSpace())).flatMap(rc::getContentFormStrResponse); + } + } diff --git a/app/src/main/java/xyz/fycz/myreader/webapi/ThirdSourceApi.java b/app/src/main/java/xyz/fycz/myreader/webapi/ThirdSourceApi.java index 491b854..3e9141e 100644 --- a/app/src/main/java/xyz/fycz/myreader/webapi/ThirdSourceApi.java +++ b/app/src/main/java/xyz/fycz/myreader/webapi/ThirdSourceApi.java @@ -25,6 +25,7 @@ import xyz.fycz.myreader.util.utils.OkHttpUtils; import xyz.fycz.myreader.webapi.crawler.source.ThirdCrawler; import static xyz.fycz.myreader.common.APPCONST.JS_PATTERN; +import static xyz.fycz.myreader.util.utils.OkHttpUtils.getCookies; /** * @author fengyue @@ -130,14 +131,4 @@ public class ThirdSourceApi { } } - private static Map getCookies(BookSource bookSource){ - Map cookieMap = new HashMap<>(); - if (bookSource != null){ - CookieBean cookie = DbManager.getDaoSession().getCookieBeanDao().load(bookSource.getSourceUrl()); - if (cookie != null) { - cookieMap.put("Cookie", cookie.getCookie()); - } - } - return cookieMap; - } } diff --git a/app/src/main/java/xyz/fycz/myreader/webapi/crawler/base/BaseReadCrawler.java b/app/src/main/java/xyz/fycz/myreader/webapi/crawler/base/BaseReadCrawler.java index 2f462b6..f385976 100644 --- a/app/src/main/java/xyz/fycz/myreader/webapi/crawler/base/BaseReadCrawler.java +++ b/app/src/main/java/xyz/fycz/myreader/webapi/crawler/base/BaseReadCrawler.java @@ -1,6 +1,7 @@ package xyz.fycz.myreader.webapi.crawler.base; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -19,7 +20,7 @@ import xyz.fycz.myreader.model.mulvalmap.ConMVMap; public abstract class BaseReadCrawler implements ReadCrawler { @Override public Map getHeaders() { - return null; + return new HashMap<>(); } @Override @@ -39,16 +40,25 @@ public abstract class BaseReadCrawler implements ReadCrawler { @Override public Observable> getBooksFromStrResponse(StrResponse response) { - return null; + return Observable.create(emitter -> { + emitter.onNext(getBooksFromSearchHtml(response.body())); + emitter.onComplete(); + }); } @Override public Observable> getChaptersFromStrResponse(StrResponse response) { - return null; + return Observable.create(emitter -> { + emitter.onNext(getChaptersFromHtml(response.body())); + emitter.onComplete(); + }); } @Override public Observable getContentFormStrResponse(StrResponse response) { - return null; + return Observable.create(emitter -> { + emitter.onNext(getContentFormHtml(response.body())); + emitter.onComplete(); + }); } } diff --git a/app/src/main/java/xyz/fycz/myreader/webapi/crawler/base/BaseSourceCrawler.java b/app/src/main/java/xyz/fycz/myreader/webapi/crawler/base/BaseSourceCrawler.java index a3673a6..bebb7bd 100644 --- a/app/src/main/java/xyz/fycz/myreader/webapi/crawler/base/BaseSourceCrawler.java +++ b/app/src/main/java/xyz/fycz/myreader/webapi/crawler/base/BaseSourceCrawler.java @@ -348,7 +348,7 @@ public abstract class BaseSourceCrawler extends BaseReadCrawler implements BookI @Override public Map getHeaders() { if (!StringUtils.isJsonObject(source.getSourceHeaders())) - return null; + return super.getHeaders(); try { JSONObject headersJson = new JSONObject(source.getSourceHeaders()); Map headers = new HashMap<>(); @@ -362,7 +362,7 @@ public abstract class BaseSourceCrawler extends BaseReadCrawler implements BookI } catch (JSONException e) { e.printStackTrace(); } - return null; + return super.getHeaders(); } /** diff --git a/app/src/main/java/xyz/fycz/myreader/webapi/crawler/base/BookInfoCrawler.java b/app/src/main/java/xyz/fycz/myreader/webapi/crawler/base/BookInfoCrawler.java index cf8c632..5ddd80a 100644 --- a/app/src/main/java/xyz/fycz/myreader/webapi/crawler/base/BookInfoCrawler.java +++ b/app/src/main/java/xyz/fycz/myreader/webapi/crawler/base/BookInfoCrawler.java @@ -1,5 +1,8 @@ package xyz.fycz.myreader.webapi.crawler.base; + +import io.reactivex.Observable; +import xyz.fycz.myreader.entity.StrResponse; import xyz.fycz.myreader.greendao.entity.Book; /** @@ -10,4 +13,10 @@ public interface BookInfoCrawler { String getNameSpace(); String getCharset(); Book getBookInfo(String html, Book book); + default Observable getBookInfo(StrResponse strResponse, Book book){ + return Observable.create(emitter -> { + emitter.onNext(getBookInfo(strResponse.body(), book)); + emitter.onComplete(); + }); + } } diff --git a/app/src/main/java/xyz/fycz/myreader/widget/scroller/FastScrollRecyclerView.java b/app/src/main/java/xyz/fycz/myreader/widget/scroller/FastScrollRecyclerView.java new file mode 100644 index 0000000..d5bba90 --- /dev/null +++ b/app/src/main/java/xyz/fycz/myreader/widget/scroller/FastScrollRecyclerView.java @@ -0,0 +1,253 @@ +package xyz.fycz.myreader.widget.scroller; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.ViewGroup; +import android.view.ViewParent; + +import androidx.annotation.ColorInt; +import androidx.recyclerview.widget.RecyclerView; + +import xyz.fycz.myreader.R; + + +@SuppressWarnings("unused") +public class FastScrollRecyclerView extends RecyclerView { + + + private FastScroller mFastScroller; + + + public FastScrollRecyclerView(Context context) { + + super(context); + + layout(context, null); + + setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + + } + + + public FastScrollRecyclerView(Context context, AttributeSet attrs) { + + this(context, attrs, 0); + + } + + + public FastScrollRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { + + super(context, attrs, defStyleAttr); + + layout(context, attrs); + + } + + + @Override + + public void setAdapter(Adapter adapter) { + + super.setAdapter(adapter); + + + if (adapter instanceof FastScroller.SectionIndexer) { + + setSectionIndexer((FastScroller.SectionIndexer) adapter); + + } else if (adapter == null) { + + setSectionIndexer(null); + + } + + } + + + @Override + + public void setVisibility(int visibility) { + + super.setVisibility(visibility); + + mFastScroller.setVisibility(visibility); + + } + + + /** + * Set the {@link FastScroller.SectionIndexer} for the {@link FastScroller}. + * + * @param sectionIndexer The SectionIndexer that provides section text for the FastScroller + */ + + public void setSectionIndexer(FastScroller.SectionIndexer sectionIndexer) { + + mFastScroller.setSectionIndexer(sectionIndexer); + + } + + + /** + * Set the enabled state of fast scrolling. + * + * @param enabled True to enable fast scrolling, false otherwise + */ + + public void setFastScrollEnabled(boolean enabled) { + + mFastScroller.setEnabled(enabled); + + } + + + /** + * Hide the scrollbar when not scrolling. + * + * @param hideScrollbar True to hide the scrollbar, false to show + */ + + public void setHideScrollbar(boolean hideScrollbar) { + + mFastScroller.setFadeScrollbar(hideScrollbar); + + } + + + /** + * Display a scroll track while scrolling. + * + * @param visible True to show scroll track, false to hide + */ + + public void setTrackVisible(boolean visible) { + + mFastScroller.setTrackVisible(visible); + + } + + + /** + * Set the color of the scroll track. + * + * @param color The color for the scroll track + */ + + public void setTrackColor(@ColorInt int color) { + + mFastScroller.setTrackColor(color); + + } + + + /** + * Set the color for the scroll handle. + * + * @param color The color for the scroll handle + */ + + public void setHandleColor(@ColorInt int color) { + + mFastScroller.setHandleColor(color); + + } + + + /** + * Show the section bubble while scrolling. + * + * @param visible True to show the bubble, false to hide + */ + + public void setBubbleVisible(boolean visible) { + + mFastScroller.setBubbleVisible(visible); + + } + + + /** + * Set the background color of the index bubble. + * + * @param color The background color for the index bubble + */ + + public void setBubbleColor(@ColorInt int color) { + + mFastScroller.setBubbleColor(color); + + } + + + /** + * Set the text color of the index bubble. + * + * @param color The text color for the index bubble + */ + + public void setBubbleTextColor(@ColorInt int color) { + + mFastScroller.setBubbleTextColor(color); + + } + + + /** + * Set the fast scroll state change listener. + * + * @param fastScrollStateChangeListener The interface that will listen to fastscroll state change events + */ + + public void setFastScrollStateChangeListener(FastScrollStateChangeListener fastScrollStateChangeListener) { + + mFastScroller.setFastScrollStateChangeListener(fastScrollStateChangeListener); + + } + + + @Override + + protected void onAttachedToWindow() { + + super.onAttachedToWindow(); + + mFastScroller.attachRecyclerView(this); + + + ViewParent parent = getParent(); + + + if (parent instanceof ViewGroup) { + + ViewGroup viewGroup = (ViewGroup) parent; + + viewGroup.addView(mFastScroller); + + mFastScroller.setLayoutParams(viewGroup); + + } + + } + + + @Override + + protected void onDetachedFromWindow() { + + mFastScroller.detachRecyclerView(); + + super.onDetachedFromWindow(); + + } + + + private void layout(Context context, AttributeSet attrs) { + + mFastScroller = new FastScroller(context, attrs); + + mFastScroller.setId(R.id.fast_scroller); + + } + +} \ No newline at end of file diff --git a/app/src/main/java/xyz/fycz/myreader/widget/scroller/FastScrollStateChangeListener.java b/app/src/main/java/xyz/fycz/myreader/widget/scroller/FastScrollStateChangeListener.java new file mode 100644 index 0000000..158500d --- /dev/null +++ b/app/src/main/java/xyz/fycz/myreader/widget/scroller/FastScrollStateChangeListener.java @@ -0,0 +1,14 @@ +package xyz.fycz.myreader.widget.scroller; + +public interface FastScrollStateChangeListener { + + /** + * Called when fast scrolling begins + */ + void onFastScrollStart(FastScroller fastScroller); + + /** + * Called when fast scrolling ends + */ + void onFastScrollStop(FastScroller fastScroller); +} \ No newline at end of file diff --git a/app/src/main/java/xyz/fycz/myreader/widget/scroller/FastScroller.java b/app/src/main/java/xyz/fycz/myreader/widget/scroller/FastScroller.java new file mode 100644 index 0000000..3f8fb1e --- /dev/null +++ b/app/src/main/java/xyz/fycz/myreader/widget/scroller/FastScroller.java @@ -0,0 +1,531 @@ +package xyz.fycz.myreader.widget.scroller; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewPropertyAnimator; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import androidx.annotation.ColorInt; +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.constraintlayout.widget.ConstraintSet; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.core.view.GravityCompat; +import androidx.core.view.ViewCompat; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.StaggeredGridLayoutManager; + +import xyz.fycz.myreader.R; +import xyz.fycz.myreader.util.utils.ColorUtil; + + +public class FastScroller extends LinearLayout { + private static final int sBubbleAnimDuration = 100; + private static final int sScrollbarAnimDuration = 300; + private static final int sScrollbarHideDelay = 1000; + private static final int sTrackSnapRange = 5; + @ColorInt + private int mBubbleColor; + @ColorInt + private int mHandleColor; + private int mBubbleHeight; + private int mHandleHeight; + private int mViewHeight; + private boolean mFadeScrollbar; + private boolean mShowBubble; + private SectionIndexer mSectionIndexer; + private ViewPropertyAnimator mScrollbarAnimator; + private ViewPropertyAnimator mBubbleAnimator; + private RecyclerView mRecyclerView; + private TextView mBubbleView; + private ImageView mHandleView; + private ImageView mTrackView; + private View mScrollbar; + private Drawable mBubbleImage; + private Drawable mHandleImage; + private Drawable mTrackImage; + private FastScrollStateChangeListener mFastScrollStateChangeListener; + private Runnable mScrollbarHider = this::hideScrollbar; + + private RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (!mHandleView.isSelected() && isEnabled()) { + setViewPositions(getScrollProportion(recyclerView)); + } + } + + @Override + public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { + super.onScrollStateChanged(recyclerView, newState); + if (isEnabled()) { + switch (newState) { + case RecyclerView.SCROLL_STATE_DRAGGING: + getHandler().removeCallbacks(mScrollbarHider); + cancelAnimation(mScrollbarAnimator); + if (!isViewVisible(mScrollbar)) { + showScrollbar(); + } + break; + case RecyclerView.SCROLL_STATE_IDLE: + if (mFadeScrollbar && !mHandleView.isSelected()) { + getHandler().postDelayed(mScrollbarHider, sScrollbarHideDelay); + } + break; + } + } + } + }; + + public FastScroller(Context context) { + super(context); + layout(context, null); + setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)); + } + + public FastScroller(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public FastScroller(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + layout(context, attrs); + setLayoutParams(generateLayoutParams(attrs)); + } + + @Override + public void setLayoutParams(@NonNull ViewGroup.LayoutParams params) { + params.width = LayoutParams.WRAP_CONTENT; + super.setLayoutParams(params); + } + + public void setLayoutParams(@NonNull ViewGroup viewGroup) { + @IdRes int recyclerViewId = mRecyclerView != null ? mRecyclerView.getId() : NO_ID; + int marginTop = getResources().getDimensionPixelSize(R.dimen.fastscroll_scrollbar_margin_top); + int marginBottom = getResources().getDimensionPixelSize(R.dimen.fastscroll_scrollbar_margin_bottom); + if (recyclerViewId == NO_ID) { + throw new IllegalArgumentException("RecyclerView must have a view ID"); + } + if (viewGroup instanceof ConstraintLayout) { + ConstraintSet constraintSet = new ConstraintSet(); + @IdRes int layoutId = getId(); + constraintSet.clone((ConstraintLayout) viewGroup); + constraintSet.connect(layoutId, ConstraintSet.TOP, recyclerViewId, ConstraintSet.TOP); + constraintSet.connect(layoutId, ConstraintSet.BOTTOM, recyclerViewId, ConstraintSet.BOTTOM); + constraintSet.connect(layoutId, ConstraintSet.END, recyclerViewId, ConstraintSet.END); + constraintSet.applyTo((ConstraintLayout) viewGroup); + ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) getLayoutParams(); + layoutParams.setMargins(0, marginTop, 0, marginBottom); + setLayoutParams(layoutParams); + } else if (viewGroup instanceof CoordinatorLayout) { + CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) getLayoutParams(); + layoutParams.setAnchorId(recyclerViewId); + layoutParams.anchorGravity = GravityCompat.END; + layoutParams.setMargins(0, marginTop, 0, marginBottom); + setLayoutParams(layoutParams); + } else if (viewGroup instanceof FrameLayout) { + FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) getLayoutParams(); + layoutParams.gravity = GravityCompat.END; + layoutParams.setMargins(0, marginTop, 0, marginBottom); + setLayoutParams(layoutParams); + } else if (viewGroup instanceof RelativeLayout) { + RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams(); + int endRule = RelativeLayout.ALIGN_END; + layoutParams.addRule(RelativeLayout.ALIGN_TOP, recyclerViewId); + layoutParams.addRule(RelativeLayout.ALIGN_BOTTOM, recyclerViewId); + layoutParams.addRule(endRule, recyclerViewId); + layoutParams.setMargins(0, marginTop, 0, marginBottom); + setLayoutParams(layoutParams); + } else { + throw new IllegalArgumentException("Parent ViewGroup must be a ConstraintLayout, CoordinatorLayout, FrameLayout, or RelativeLayout"); + } + updateViewHeights(); + } + + public void setSectionIndexer(SectionIndexer sectionIndexer) { + mSectionIndexer = sectionIndexer; + } + + public void attachRecyclerView(RecyclerView recyclerView) { + mRecyclerView = recyclerView; + if (mRecyclerView != null) { + mRecyclerView.addOnScrollListener(mScrollListener); + post(() -> { + // set initial positions for bubble and handle + setViewPositions(getScrollProportion(mRecyclerView)); + }); + } + } + + public void detachRecyclerView() { + if (mRecyclerView != null) { + mRecyclerView.removeOnScrollListener(mScrollListener); + mRecyclerView = null; + } + } + + /** + * Hide the scrollbar when not scrolling. + * + * @param fadeScrollbar True to hide the scrollbar, false to show + */ + public void setFadeScrollbar(boolean fadeScrollbar) { + mFadeScrollbar = fadeScrollbar; + mScrollbar.setVisibility(fadeScrollbar ? GONE : VISIBLE); + } + + /** + * Show the section bubble while scrolling. + * + * @param visible True to show the bubble, false to hide + */ + public void setBubbleVisible(boolean visible) { + mShowBubble = visible; + } + + /** + * Display a scroll track while scrolling. + * + * @param visible True to show scroll track, false to hide + */ + public void setTrackVisible(boolean visible) { + mTrackView.setVisibility(visible ? VISIBLE : GONE); + } + + /** + * Set the color of the scroll track. + * + * @param color The color for the scroll track + */ + public void setTrackColor(@ColorInt int color) { + @ColorInt int trackColor = color; + if (mTrackImage == null) { + Drawable drawable = ContextCompat.getDrawable(getContext(), R.drawable.fastscroll_track); + if (drawable != null) { + mTrackImage = DrawableCompat.wrap(drawable); + } + } + DrawableCompat.setTint(mTrackImage, trackColor); + mTrackView.setImageDrawable(mTrackImage); + } + + /** + * Set the color for the scroll handle. + * + * @param color The color for the scroll handle + */ + public void setHandleColor(@ColorInt int color) { + mHandleColor = color; + if (mHandleImage == null) { + Drawable drawable = ContextCompat.getDrawable(getContext(), R.drawable.fastscroll_handle); + if (drawable != null) { + mHandleImage = DrawableCompat.wrap(drawable); + } + } + DrawableCompat.setTint(mHandleImage, mHandleColor); + mHandleView.setImageDrawable(mHandleImage); + } + + /** + * Set the background color of the index bubble. + * + * @param color The background color for the index bubble + */ + public void setBubbleColor(@ColorInt int color) { + mBubbleColor = color; + if (mBubbleImage == null) { + Drawable drawable = ContextCompat.getDrawable(getContext(), R.drawable.fastscroll_bubble); + if (drawable != null) { + mBubbleImage = DrawableCompat.wrap(drawable); + } + } + DrawableCompat.setTint(mBubbleImage, mBubbleColor); + mBubbleView.setBackground(mBubbleImage); + } + + /** + * Set the text color of the index bubble. + * + * @param color The text color for the index bubble + */ + public void setBubbleTextColor(@ColorInt int color) { + mBubbleView.setTextColor(color); + } + + /** + * Set the fast scroll state change listener. + * + * @param fastScrollStateChangeListener The interface that will listen to fastscroll state change events + */ + public void setFastScrollStateChangeListener(FastScrollStateChangeListener fastScrollStateChangeListener) { + mFastScrollStateChangeListener = fastScrollStateChangeListener; + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + setVisibility(enabled ? VISIBLE : GONE); + } + + @Override + @SuppressLint("ClickableViewAccessibility") + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + if (event.getX() < mHandleView.getX() - ViewCompat.getPaddingStart(mHandleView)) { + return false; + } + requestDisallowInterceptTouchEvent(true); + setHandleSelected(true); + getHandler().removeCallbacks(mScrollbarHider); + cancelAnimation(mScrollbarAnimator); + cancelAnimation(mBubbleAnimator); + if (!isViewVisible(mScrollbar)) { + showScrollbar(); + } + if (mShowBubble && mSectionIndexer != null) { + showBubble(); + } + if (mFastScrollStateChangeListener != null) { + mFastScrollStateChangeListener.onFastScrollStart(this); + } + case MotionEvent.ACTION_MOVE: + final float y = event.getY(); + setViewPositions(y); + setRecyclerViewPosition(y); + return true; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + requestDisallowInterceptTouchEvent(false); + setHandleSelected(false); + if (mFadeScrollbar) { + getHandler().postDelayed(mScrollbarHider, sScrollbarHideDelay); + } + hideBubble(); + if (mFastScrollStateChangeListener != null) { + mFastScrollStateChangeListener.onFastScrollStop(this); + } + return true; + } + return super.onTouchEvent(event); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + mViewHeight = h; + } + + private void setRecyclerViewPosition(float y) { + if (mRecyclerView != null && mRecyclerView.getAdapter() != null) { + int itemCount = mRecyclerView.getAdapter().getItemCount(); + float proportion; + if (mHandleView.getY() == 0) { + proportion = 0f; + } else if (mHandleView.getY() + mHandleHeight >= mViewHeight - sTrackSnapRange) { + proportion = 1f; + } else { + proportion = y / (float) mViewHeight; + } + int scrolledItemCount = Math.round(proportion * itemCount); + if (isLayoutReversed(mRecyclerView.getLayoutManager())) { + scrolledItemCount = itemCount - scrolledItemCount; + } + int targetPos = getValueInRange(0, itemCount - 1, scrolledItemCount); + mRecyclerView.getLayoutManager().scrollToPosition(targetPos); + if (mShowBubble && mSectionIndexer != null) { + mBubbleView.setText(mSectionIndexer.getSectionText(targetPos)); + } + } + } + + private float getScrollProportion(RecyclerView recyclerView) { + if (recyclerView == null) { + return 0; + } + final int verticalScrollOffset = recyclerView.computeVerticalScrollOffset(); + final int verticalScrollRange = recyclerView.computeVerticalScrollRange(); + final float rangeDiff = verticalScrollRange - mViewHeight; + float proportion = (float) verticalScrollOffset / (rangeDiff > 0 ? rangeDiff : 1f); + return mViewHeight * proportion; + } + + @SuppressWarnings("SameParameterValue") + private int getValueInRange(int min, int max, int value) { + int minimum = Math.max(min, value); + return Math.min(minimum, max); + } + + private void setViewPositions(float y) { + mBubbleHeight = mBubbleView.getHeight(); + mHandleHeight = mHandleView.getHeight(); + int bubbleY = getValueInRange(0, mViewHeight - mBubbleHeight - mHandleHeight / 2, (int) (y - mBubbleHeight)); + int handleY = getValueInRange(0, mViewHeight - mHandleHeight, (int) (y - mHandleHeight / 2)); + if (mShowBubble) { + mBubbleView.setY(bubbleY); + } + mHandleView.setY(handleY); + } + + private void updateViewHeights() { + int measureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + mBubbleView.measure(measureSpec, measureSpec); + mBubbleHeight = mBubbleView.getMeasuredHeight(); + mHandleView.measure(measureSpec, measureSpec); + mHandleHeight = mHandleView.getMeasuredHeight(); + } + + private boolean isLayoutReversed(@NonNull final RecyclerView.LayoutManager layoutManager) { + if (layoutManager instanceof LinearLayoutManager) { + return ((LinearLayoutManager) layoutManager).getReverseLayout(); + } else if (layoutManager instanceof StaggeredGridLayoutManager) { + return ((StaggeredGridLayoutManager) layoutManager).getReverseLayout(); + } + return false; + } + + private boolean isViewVisible(View view) { + return view != null && view.getVisibility() == VISIBLE; + } + + private void cancelAnimation(ViewPropertyAnimator animator) { + if (animator != null) { + animator.cancel(); + } + } + + private void showBubble() { + if (!isViewVisible(mBubbleView)) { + mBubbleView.setVisibility(VISIBLE); + mBubbleAnimator = mBubbleView.animate().alpha(1f) + .setDuration(sBubbleAnimDuration) + .setListener(new AnimatorListenerAdapter() { + // adapter required for new alpha value to stick + }); + } + } + + private void hideBubble() { + if (isViewVisible(mBubbleView)) { + mBubbleAnimator = mBubbleView.animate().alpha(0f) + .setDuration(sBubbleAnimDuration) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + mBubbleView.setVisibility(GONE); + mBubbleAnimator = null; + } + + @Override + public void onAnimationCancel(Animator animation) { + super.onAnimationCancel(animation); + mBubbleView.setVisibility(GONE); + mBubbleAnimator = null; + } + }); + } + } + + private void showScrollbar() { + if (mRecyclerView.computeVerticalScrollRange() - mViewHeight > 0) { + float transX = getResources().getDimensionPixelSize(R.dimen.fastscroll_scrollbar_padding_end); + mScrollbar.setTranslationX(transX); + mScrollbar.setVisibility(VISIBLE); + mScrollbarAnimator = mScrollbar.animate().translationX(0f).alpha(1f) + .setDuration(sScrollbarAnimDuration) + .setListener(new AnimatorListenerAdapter() { + // adapter required for new alpha value to stick + }); + } + } + + private void hideScrollbar() { + float transX = getResources().getDimensionPixelSize(R.dimen.fastscroll_scrollbar_padding_end); + mScrollbarAnimator = mScrollbar.animate().translationX(transX).alpha(0f) + .setDuration(sScrollbarAnimDuration) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + mScrollbar.setVisibility(GONE); + mScrollbarAnimator = null; + } + + @Override + public void onAnimationCancel(Animator animation) { + super.onAnimationCancel(animation); + mScrollbar.setVisibility(GONE); + mScrollbarAnimator = null; + } + }); + } + + private void setHandleSelected(boolean selected) { + mHandleView.setSelected(selected); + DrawableCompat.setTint(mHandleImage, selected ? mBubbleColor : mHandleColor); + } + + @SuppressWarnings("ConstantConditions") + private void layout(Context context, AttributeSet attrs) { + inflate(context, R.layout.view_fastscroller, this); + setClipChildren(false); + setOrientation(HORIZONTAL); + mBubbleView = findViewById(R.id.fastscroll_bubble); + mHandleView = findViewById(R.id.fastscroll_handle); + mTrackView = findViewById(R.id.fastscroll_track); + mScrollbar = findViewById(R.id.fastscroll_scrollbar); + @ColorInt int bubbleColor = ColorUtil.adjustAlpha(context.getResources().getColor(R.color.colorAccent), 0.8f); + @ColorInt int handleColor = context.getResources().getColor(R.color.colorAccent); + @ColorInt int trackColor = context.getResources().getColor(R.color.transparent30); + @ColorInt int textColor = ColorUtil.isColorLight(bubbleColor) ? Color.BLACK : Color.WHITE; + boolean fadeScrollbar = true; + boolean showBubble = false; + boolean showTrack = true; + if (attrs != null) { + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.FastScroller, 0, 0); + if (typedArray != null) { + try { + bubbleColor = typedArray.getColor(R.styleable.FastScroller_bubbleColor, bubbleColor); + handleColor = typedArray.getColor(R.styleable.FastScroller_handleColor, handleColor); + trackColor = typedArray.getColor(R.styleable.FastScroller_trackColor, trackColor); + textColor = typedArray.getColor(R.styleable.FastScroller_bubbleTextColor, textColor); + fadeScrollbar = typedArray.getBoolean(R.styleable.FastScroller_fadeScrollbar, fadeScrollbar); + showBubble = typedArray.getBoolean(R.styleable.FastScroller_showBubble, showBubble); + showTrack = typedArray.getBoolean(R.styleable.FastScroller_showTrack, showTrack); + } finally { + typedArray.recycle(); + } + } + } + setTrackColor(trackColor); + setHandleColor(handleColor); + setBubbleColor(bubbleColor); + setBubbleTextColor(textColor); + setFadeScrollbar(fadeScrollbar); + setBubbleVisible(showBubble); + setTrackVisible(showTrack); + } + + public interface SectionIndexer { + String getSectionText(int position); + } + +} diff --git a/app/src/main/res/drawable/fastscroll_bubble.xml b/app/src/main/res/drawable/fastscroll_bubble.xml new file mode 100644 index 0000000..4448ec4 --- /dev/null +++ b/app/src/main/res/drawable/fastscroll_bubble.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/fastscroll_handle.xml b/app/src/main/res/drawable/fastscroll_handle.xml new file mode 100644 index 0000000..7a5f7e8 --- /dev/null +++ b/app/src/main/res/drawable/fastscroll_handle.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/fastscroll_track.xml b/app/src/main/res/drawable/fastscroll_track.xml new file mode 100644 index 0000000..fb9b16c --- /dev/null +++ b/app/src/main/res/drawable/fastscroll_track.xml @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_replace_rule.xml b/app/src/main/res/layout/activity_replace_rule.xml index 4c2d9d3..f2ace93 100644 --- a/app/src/main/res/layout/activity_replace_rule.xml +++ b/app/src/main/res/layout/activity_replace_rule.xml @@ -7,10 +7,15 @@ - + android:layout_height="match_parent"> + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_catalog.xml b/app/src/main/res/layout/fragment_catalog.xml index a5556b1..d8f6c06 100644 --- a/app/src/main/res/layout/fragment_catalog.xml +++ b/app/src/main/res/layout/fragment_catalog.xml @@ -1,42 +1,43 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/rl_catalog" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context="xyz.fycz.myreader.ui.fragment.CatalogFragment"> - + android:id="@+id/lv_chapter_list" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:divider="@color/nothing" + android:fadeScrollbars="true" + android:fadingEdge="none" + android:fastScrollEnabled="true" + android:listSelector="@color/nothing" + android:overScrollMode="never" + android:scrollbars="vertical" /> + android:id="@+id/change_sort" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:layout_centerHorizontal="true" + android:layout_marginBottom="60dp" + android:clickable="true" + android:contentDescription="倒序" + android:src="@mipmap/ic_order" + app:borderWidth="0dp" + app:elevation="6dp" + app:pressedTranslationZ="12dp" + android:focusable="true" /> + android:id="@+id/pb_loading" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerInParent="true" + android:visibility="gone" /> diff --git a/app/src/main/res/layout/fragment_import_source.xml b/app/src/main/res/layout/fragment_import_source.xml index ce29b24..a353c05 100644 --- a/app/src/main/res/layout/fragment_import_source.xml +++ b/app/src/main/res/layout/fragment_import_source.xml @@ -59,8 +59,8 @@ android:id="@+id/iv_group" android:layout_width="wrap_content" android:layout_height="match_parent" - android:background="?android:attr/selectableItemBackground" android:layout_toStartOf="@+id/iv_menu" + android:background="?android:attr/selectableItemBackground" android:paddingStart="5dp" android:paddingEnd="5dp" app:srcCompat="@drawable/ic_filter" @@ -70,9 +70,9 @@ android:id="@+id/iv_menu" android:layout_width="wrap_content" android:layout_height="match_parent" - android:background="?android:attr/selectableItemBackground" android:layout_alignParentEnd="true" android:layout_gravity="end" + android:background="?android:attr/selectableItemBackground" android:paddingStart="5dp" android:paddingEnd="5dp" app:srcCompat="@drawable/ic_menu" @@ -85,9 +85,14 @@ android:layout_height="1dp" android:background="@color/colorBackground" /> - + android:layout_height="match_parent"> + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_local_source.xml b/app/src/main/res/layout/fragment_local_source.xml index 7d33635..16e831e 100644 --- a/app/src/main/res/layout/fragment_local_source.xml +++ b/app/src/main/res/layout/fragment_local_source.xml @@ -63,9 +63,14 @@ android:background="@color/colorBackground"/> - + android:layout_height="match_parent"> + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_fastscroller.xml b/app/src/main/res/layout/view_fastscroller.xml new file mode 100644 index 0000000..9f15333 --- /dev/null +++ b/app/src/main/res/layout/view_fastscroller.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index ef55b4c..a196db6 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -54,4 +54,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 27fbaa5..25a4541 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -48,4 +48,20 @@ 10dp 15dp + + 44dp + 88dp + 48sp + 16dp + + 40dp + 8dp + 0dp + + 2dp + + 8dp + 8dp + 8dp + 8dp diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml index 5c63742..bd8d816 100644 --- a/app/src/main/res/values/ids.xml +++ b/app/src/main/res/values/ids.xml @@ -6,4 +6,7 @@ + + + \ No newline at end of file