Move GalleryListScene to paging3 (#1245)

* Initial WIP move GalleryListScene to paging3

* Refine

* Refine

* Refine

* Refine

* Refine

* Refine

* Typesafe State

* Refine

* Refine

* Refine

* Refine

* Cleanup code

* Cleanup code
This commit is contained in:
tarsin authored and GitHub committed 2023-07-11 10:40:03 +08:00
1 parent 7fd3361608
commit df8c70d8e6
15 files changed
+405 -1575

No files matched your search

@@ -15,8 +15,6 @@
*/
package com.hippo.ehviewer
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.yorozuya.IntIdGenerator
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -25,26 +23,7 @@ import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.launch
object FavouriteStatusRouter {
private val idGenerator = IntIdGenerator(Settings.dataMapNextId)
private val maps = HashMap<Int, MutableMap<Long, GalleryInfo>>()
fun saveDataMap(map: MutableMap<Long, GalleryInfo>): Int {
val id = idGenerator.nextId()
maps[id] = map
Settings.dataMapNextId = idGenerator.nextId()
return id
}
fun restoreDataMap(id: Int): MutableMap<Long, GalleryInfo>? {
return maps.remove(id)
}
fun modifyFavourites(gid: Long, slot: Int) {
for (map in maps.values) {
val info = map[gid]
if (info != null) {
info.favoriteSlot = slot
}
}
_globalFlow.tryEmit(gid to slot)
}
@@ -34,8 +34,8 @@ data class ListUrlBuilder(
@get:Mode @Mode
var mode: Int = MODE_NORMAL,
private var mPrev: String? = null,
private var mNext: String? = null,
private var mJumpTo: String? = null,
var mNext: String? = null,
var mJumpTo: String? = null,
var category: Int = EhUtils.NONE,
private var mKeyword: String? = null,
var hash: String? = null,
@@ -38,6 +38,7 @@ class AddDeleteDrawable(context: Context, color: Int) : Drawable() {
private var mProgress = 0f
private var mAutoUpdateMirror = false
private var mVerticalMirror = false
init {
val resources = context.resources
mSize = resources.getDimensionPixelSize(R.dimen.add_size)
@@ -78,7 +79,7 @@ class AddDeleteDrawable(context: Context, color: Int) : Drawable() {
}
override fun setColorFilter(cf: ColorFilter?) {
mPaint.setColorFilter(cf)
mPaint.colorFilter = cf
}
override fun getIntrinsicHeight(): Int {
@@ -131,7 +132,7 @@ class AddDeleteDrawable(context: Context, color: Int) : Drawable() {
progress = endProgress
} else {
val oa = ObjectAnimator.ofFloat(this, "progress", endProgress)
oa.setDuration(duration)
oa.duration = duration
oa.setAutoCancel(true)
oa.start()
}
@@ -18,8 +18,9 @@ package com.hippo.ehviewer.ui.legacy
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.view.View
import android.widget.FrameLayout
class BringOutTransition(contentLayout: ContentLayout, mSearchLayout: SearchLayout) :
class BringOutTransition(contentLayout: FrameLayout, mSearchLayout: SearchLayout) :
ViewTransition(contentLayout, mSearchLayout) {
override fun startAnimations(hiddenView: View, shownView: View) {
mAnimator1 = hiddenView.animate().alpha(0f).scaleY(0.7f).scaleX(0.7f).apply {
@@ -1,820 +0,0 @@
/*
* Copyright 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.legacy
import android.app.Activity
import android.content.Context
import android.os.Bundle
import android.os.Parcelable
import android.util.AttributeSet
import android.util.Log
import android.view.View
import android.widget.FrameLayout
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.databinding.WidgetContentLayoutBinding
import com.hippo.ehviewer.ui.legacy.LayoutManagerUtils.OnScrollToPositionListener
import com.hippo.ehviewer.ui.legacy.ViewTransition.OnShowViewListener
import com.hippo.ehviewer.util.ExceptionUtils
import com.hippo.ehviewer.util.getParcelableCompat
import com.hippo.ehviewer.yorozuya.IntIdGenerator
import com.hippo.ehviewer.yorozuya.collect.IntList
import com.hippo.ehviewer.yorozuya.dp2px
import rikka.core.res.resolveColor
class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout(context, attrs) {
private lateinit var mContentHelper: ContentHelper
private val mRecyclerViewOriginBottom: Int
private val mFastScrollerOriginBottom: Int
private val binding: WidgetContentLayoutBinding
init {
binding = WidgetContentLayoutBinding.inflate((context as Activity).layoutInflater, this)
clipChildren = false
clipToPadding = false
binding.fastScroller.attachToRecyclerView(binding.recyclerView)
val drawable = HandlerDrawable()
drawable.setColor(context.getTheme().resolveColor(androidx.appcompat.R.attr.colorPrimary))
binding.fastScroller.setHandlerDrawable(drawable)
mRecyclerViewOriginBottom = binding.recyclerView.paddingBottom
mFastScrollerOriginBottom = binding.fastScroller.paddingBottom
}
val recyclerView
get() = binding.recyclerView
val fastScroller
get() = binding.fastScroller
fun setHelper(helper: ContentHelper) {
mContentHelper = helper
helper.init(this)
}
fun hideFastScroll() {
binding.fastScroller.detachedFromRecyclerView()
}
override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
super.setPadding(left, top, right, 0)
setFitPaddingBottom(bottom)
}
fun setFitPaddingTop(fitPaddingTop: Int) {
// RefreshLayout
binding.refreshLayout.setProgressViewOffset(
true,
0,
fitPaddingTop + dp2px(context, 32f),
) // TODO
}
private fun setFitPaddingBottom(fitPaddingBottom: Int) {
// RecyclerView
binding.recyclerView.setPadding(
binding.recyclerView.paddingLeft,
binding.recyclerView.paddingTop,
binding.recyclerView.paddingRight,
mRecyclerViewOriginBottom + fitPaddingBottom,
)
binding.tip.setPadding(
binding.tip.paddingLeft,
binding.tip.paddingTop,
binding.tip.paddingRight,
fitPaddingBottom,
)
binding.progress.setPadding(
binding.progress.paddingLeft,
binding.progress.paddingTop,
binding.progress.paddingRight,
fitPaddingBottom,
)
binding.fastScroller.setPadding(
binding.fastScroller.paddingLeft,
binding.fastScroller.paddingTop,
binding.fastScroller.paddingRight,
mFastScrollerOriginBottom + fitPaddingBottom,
)
if (fitPaddingBottom > dp2px(context, 16f)) {
binding.bottomProgress.setPadding(0, 0, 0, fitPaddingBottom)
} else {
binding.bottomProgress.setPadding(0, 0, 0, 0)
}
}
override fun onSaveInstanceState(): Parcelable {
return mContentHelper.saveInstanceState(super.onSaveInstanceState())
}
override fun onRestoreInstanceState(state: Parcelable) {
super.onRestoreInstanceState(mContentHelper.restoreInstanceState(state))
}
abstract class ContentHelper : OnShowViewListener {
/**
* Generate task id
*/
private val mIdGenerator = IntIdGenerator()
private val mOnScrollToPositionListener =
OnScrollToPositionListener { position: Int -> onScrollToPosition(position) }
protected var mPrev: String? = null
protected var mNext: String? = null
private lateinit var mViewTransition: ViewTransition
private lateinit var binding: WidgetContentLayoutBinding
/**
* Store data
*/
private var mData = ArrayList<GalleryInfo>()
/**
* Store the page divider index
*
*
* For example, the data contain page 3, page 4, page 5,
* page 3 size is 7, page 4 size is 8, page 5 size is 9,
* so `mPageDivider` contain 7, 15, 24.
*/
private var mPageDivider = IntList()
/**
* The first page in `mData`
*/
private var mStartPage = 0
/**
* The last page + 1 in `mData`
*/
private var mEndPage = 0
/**
* The available page count.
*/
var pages = 0
private set
private var mNextPage = 0
private var mCurrentTaskId = 0
private var mCurrentTaskType = 0
private var mCurrentTaskPage = 0
private val mOnRefreshListener = OnRefreshListener {
if (mPrev != null || mStartPage > 0) {
mCurrentTaskId = mIdGenerator.nextId()
mCurrentTaskType = TYPE_PRE_PAGE_KEEP_POS
mCurrentTaskPage = mStartPage - 1
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage, mPrev, false)
} else {
doRefresh()
}
}
private var mNextPageScrollSize = 0
private var mEmptyString = "No hint"
private val mOnScrollListener: RecyclerView.OnScrollListener =
object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (!binding.refreshLayout.isRefreshing && !recyclerView.canScrollVertically(1)) {
if (mNext != null || mEndPage < pages) {
binding.bottomProgress.show()
// Get next page
// Fill pages before NextPage with empty list
while (mNextPage > mEndPage && mEndPage < pages) {
mCurrentTaskId = mIdGenerator.nextId()
mCurrentTaskType = TYPE_NEXT_PAGE_KEEP_POS
mCurrentTaskPage = mEndPage
onGetPageData(
mCurrentTaskId,
pages,
mNextPage,
null,
null,
emptyList(),
)
}
mCurrentTaskId = mIdGenerator.nextId()
mCurrentTaskType = TYPE_NEXT_PAGE_KEEP_POS
mCurrentTaskPage = mEndPage
getPageData(
mCurrentTaskId,
mCurrentTaskType,
mCurrentTaskPage,
mNext,
true,
)
} else if (mStartPage > 0 && mEndPage == pages) {
binding.bottomProgress.show()
// Refresh last page
mCurrentTaskId = mIdGenerator.nextId()
mCurrentTaskType = TYPE_REFRESH_PAGE
mCurrentTaskPage = mEndPage - 1
getPageData(
mCurrentTaskId,
mCurrentTaskType,
mCurrentTaskPage,
null,
true,
)
}
}
}
}
fun init(contentLayout: ContentLayout) {
binding = contentLayout.binding
mNextPageScrollSize = dp2px(contentLayout.context, 48f)
val drawable = ContextCompat.getDrawable(context, R.drawable.big_sad_pandroid)!!
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
binding.tip.setCompoundDrawables(null, drawable, null, null)
mViewTransition = ViewTransition(
binding.contentView,
binding.progress,
binding.tip,
)
mViewTransition.setOnShowViewListener(this)
binding.recyclerView.addOnScrollListener(mOnScrollListener)
binding.refreshLayout.setOnRefreshListener(mOnRefreshListener)
binding.tip.setOnClickListener { refresh() }
}
/**
* Call [.onGetPageData] when get data
*
* @param taskId task id
* @param page the page to get
* @param index the index to get
* @param isNext the index is next or prev
*/
protected abstract fun getPageData(
taskId: Int,
type: Int,
page: Int,
index: String?,
isNext: Boolean,
)
protected abstract val context: Context
protected open fun notifyDataSetChanged(callback: () -> Unit) {
callback()
}
protected abstract fun notifyDataSetChanged()
protected abstract fun notifyItemRangeInserted(positionStart: Int, itemCount: Int)
protected open fun onScrollToPosition(position: Int) {}
override fun onShowView(hiddenView: View, shownView: View) {}
val shownViewIndex: Int
get() = mViewTransition.shownViewIndex
fun setRefreshLayoutEnable(enable: Boolean) {
binding.refreshLayout.isEnabled = enable
}
fun setEmptyString(str: String) {
mEmptyString = str
}
val data: List<GalleryInfo>
get() = mData
fun getDataAtEx(location: Int): GalleryInfo? {
return if (location >= 0 && location < mData.size) {
mData[location]
} else {
null
}
}
val firstVisibleItem: GalleryInfo?
get() = getDataAtEx(LayoutManagerUtils.getFirstVisibleItemPosition(binding.recyclerView.layoutManager!!))
fun size(): Int {
return mData.size
}
fun isCurrentTask(taskId: Int): Boolean {
return mCurrentTaskId == taskId
}
protected abstract fun isDuplicate(d1: GalleryInfo, d2: GalleryInfo): Boolean
private fun removeDuplicateData(data: List<GalleryInfo>, start: Int, end: Int) {
val slicedData = mData.slice(start.coerceAtLeast(0) until end.coerceAtMost(mData.size))
data.dropWhile { d1 ->
slicedData.any { d2 ->
isDuplicate(d1, d2)
}
}
}
protected open fun onAddData(data: List<GalleryInfo>) {}
protected open fun onRemoveData(data: List<GalleryInfo>) {}
protected open fun onClearData() {}
fun onGetPageData(
taskId: Int,
pages: Int,
nextPage: Int,
prev: String?,
next: String?,
data: List<GalleryInfo>,
) {
if (mCurrentTaskId == taskId) {
val dataSize: Int
when (mCurrentTaskType) {
TYPE_REFRESH -> {
mStartPage = 0
mEndPage = 1
this.pages = pages
mNextPage = nextPage
mPrev = prev
mNext = next
mPageDivider.clear()
mPageDivider.add(data.size)
if (data.isEmpty()) {
mData.clear()
onClearData()
notifyDataSetChanged()
notifyDataSetChanged {
// Not found
// Ui change, show empty string
showEmptyString()
}
} else {
mData.clear()
onClearData()
mData.addAll(data)
onAddData(data)
notifyDataSetChanged()
notifyDataSetChanged {
// Ui change, show content
showContent()
// RecyclerView scroll
if (binding.recyclerView.isAttachedToWindow) {
scrollToPosition(0)
}
}
}
}
TYPE_PRE_PAGE, TYPE_PRE_PAGE_KEEP_POS -> {
removeDuplicateData(data, 0, CHECK_DUPLICATE_RANGE)
dataSize = data.size
var i = 0
val n = mPageDivider.size
while (i < n) {
mPageDivider[i] = mPageDivider[i] + dataSize
i++
}
mPageDivider.add(0, dataSize)
mStartPage--
this.pages = pages.coerceAtLeast(mEndPage)
mPrev = prev
// assert mStartPage >= 0
if (data.isEmpty()) {
// OK, that's all
if (mData.isEmpty()) {
// Ui change, show empty string
showEmptyString()
} else {
// Ui change, show content
showContent()
if (mCurrentTaskType == TYPE_PRE_PAGE && binding.recyclerView.isAttachedToWindow) {
// RecyclerView scroll, to top
scrollToPosition(0)
}
}
} else {
mData.addAll(0, data)
onAddData(data)
notifyItemRangeInserted(0, data.size)
notifyDataSetChanged {
// Ui change, show content
showContent()
if (binding.recyclerView.isAttachedToWindow) {
// RecyclerView scroll
if (mCurrentTaskType == TYPE_PRE_PAGE_KEEP_POS) {
binding.recyclerView.stopScroll()
LayoutManagerUtils.scrollToPositionProperly(
binding.recyclerView.layoutManager!!,
context,
dataSize - 1,
mOnScrollToPositionListener,
)
} else {
scrollToPosition(0)
}
}
}
}
}
TYPE_NEXT_PAGE, TYPE_NEXT_PAGE_KEEP_POS -> {
removeDuplicateData(data, mData.size - CHECK_DUPLICATE_RANGE, mData.size)
dataSize = data.size
val oldDataSize = mData.size
mPageDivider.add(oldDataSize + dataSize)
mEndPage++
mNextPage = nextPage
this.pages = pages.coerceAtLeast(mEndPage)
mNext = next
if (data.isEmpty()) {
// OK, that's all
if (mData.isEmpty()) {
// Ui change, show empty string
showEmptyString()
} else {
// Ui change, show content
showContent()
if (mCurrentTaskType == TYPE_NEXT_PAGE && binding.recyclerView.isAttachedToWindow) {
// RecyclerView scroll
scrollToPosition(oldDataSize)
}
}
} else {
mData.addAll(data)
onAddData(data)
notifyItemRangeInserted(oldDataSize, dataSize)
notifyDataSetChanged {
// Ui change, show content
showContent()
if (binding.recyclerView.isAttachedToWindow) {
if (mCurrentTaskType == TYPE_NEXT_PAGE_KEEP_POS) {
binding.recyclerView.stopScroll()
binding.recyclerView.smoothScrollBy(0, mNextPageScrollSize)
} else {
scrollToPosition(oldDataSize)
}
}
}
}
}
TYPE_SOMEWHERE -> {
mStartPage = mCurrentTaskPage
mEndPage = mCurrentTaskPage + 1
mNextPage = nextPage
this.pages = pages
mPrev = prev
mNext = next
mPageDivider.clear()
mPageDivider.add(data.size)
if (data.isEmpty()) {
mData.clear()
onClearData()
notifyDataSetChanged()
notifyDataSetChanged {
// Not found
// Ui change, show empty string
showEmptyString()
}
} else {
mData.clear()
onClearData()
mData.addAll(data)
onAddData(data)
notifyDataSetChanged()
notifyDataSetChanged {
// Ui change, show content
showContent()
if (binding.recyclerView.isAttachedToWindow) {
// RecyclerView scroll
scrollToPosition(0)
}
}
}
}
TYPE_REFRESH_PAGE -> {
if (mCurrentTaskPage < mStartPage || mCurrentTaskPage >= mEndPage) {
Log.e(
TAG,
"TYPE_REFRESH_PAGE, but mCurrentTaskPage = " + mCurrentTaskPage +
", mStartPage = " + mStartPage + ", mEndPage = " + mEndPage,
)
return
}
if (mCurrentTaskPage == mEndPage - 1) {
mNextPage = nextPage
}
this.pages = pages.coerceAtLeast(mEndPage)
val oldIndexStart =
if (mCurrentTaskPage == mStartPage) 0 else mPageDivider[mCurrentTaskPage - mStartPage - 1]
val oldIndexEnd = mPageDivider[mCurrentTaskPage - mStartPage]
val toRemove = mData.subList(oldIndexStart, oldIndexEnd)
onRemoveData(toRemove)
toRemove.clear()
removeDuplicateData(
data,
oldIndexStart - CHECK_DUPLICATE_RANGE,
oldIndexStart + CHECK_DUPLICATE_RANGE,
)
val newIndexEnd = oldIndexStart + data.size
mData.addAll(oldIndexStart, data)
onAddData(data)
notifyDataSetChanged()
notifyDataSetChanged {
var i = mCurrentTaskPage - mStartPage
val n = mPageDivider.size
while (i < n) {
mPageDivider[i] = mPageDivider[i] - oldIndexEnd + newIndexEnd
i++
}
if (mData.isEmpty()) {
// Ui change, show empty string
showEmptyString()
} else {
// Ui change, show content
showContent()
// RecyclerView scroll
if (newIndexEnd > oldIndexEnd && newIndexEnd > 0 && binding.recyclerView.isAttachedToWindow) {
scrollToPosition(newIndexEnd - 1)
}
}
}
}
}
}
}
fun onGetException(taskId: Int, e: Throwable) {
if (mCurrentTaskId == taskId) {
showProgress(PROGRESS_NONE)
val readableError = ExceptionUtils.getReadableString(e)
if (mViewTransition.shownViewIndex == 0) {
Toast.makeText(context, readableError, Toast.LENGTH_SHORT).show()
} else {
showText(readableError)
}
}
}
private fun scrollToPosition(position: Int) {
binding.recyclerView.stopScroll()
LayoutManagerUtils.scrollToPositionWithOffset(
binding.recyclerView.layoutManager!!,
position,
0,
)
onScrollToPosition(position)
}
private fun showProgress(type: Int) {
when (type) {
PROGRESS_NONE -> {
binding.refreshLayout.isRefreshing = false
binding.bottomProgress.hide()
}
PROGRESS_TOP -> {
binding.refreshLayout.isRefreshing = true
binding.bottomProgress.hide()
}
PROGRESS_BOTTOM -> {
binding.refreshLayout.isRefreshing = false
binding.bottomProgress.show()
}
}
}
private fun showContent() {
showProgress(PROGRESS_NONE)
mViewTransition.showView(0)
}
private val isContentShowing: Boolean
get() = mViewTransition.shownViewIndex == 0
fun showProgressBar(animation: Boolean = true) {
mViewTransition.showView(1, animation)
}
private fun showText(text: CharSequence?) {
binding.tip.text = text
mViewTransition.showView(2)
}
private fun showEmptyString() {
showProgress(PROGRESS_NONE)
showText(mEmptyString)
}
private fun doRefresh() {
mCurrentTaskId = mIdGenerator.nextId()
mCurrentTaskType = TYPE_REFRESH
mCurrentTaskPage = 0
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage, null, true)
}
/**
* Like [.refresh], but no animation when show progress bar
*/
fun firstRefresh() {
showProgressBar(false)
doRefresh()
}
/**
* Show progress bar first, than do refresh
*/
fun refresh() {
showProgressBar()
doRefresh()
}
private fun cancelCurrentTask() {
mCurrentTaskId = mIdGenerator.nextId()
showProgress(PROGRESS_NONE)
}
private fun getPageStart(page: Int): Int {
return if (mStartPage == page) {
0
} else {
mPageDivider[page - mStartPage - 1]
}
}
private fun getPageForPosition(position: Int): Int {
if (position < 0) {
return -1
}
val pageDivider = mPageDivider
var i = 0
val n = pageDivider.size
while (i < n) {
if (position < pageDivider[i]) {
return i + mStartPage
}
i++
}
return -1
}
val pageForTop: Int
get() = getPageForPosition(LayoutManagerUtils.getFirstVisibleItemPosition(binding.recyclerView.layoutManager!!))
val pageForBottom: Int
get() = getPageForPosition(LayoutManagerUtils.getLastVisibleItemPosition(binding.recyclerView.layoutManager!!))
fun canGoTo(): Boolean {
return isContentShowing
}
/**
* Check range first!
*
* @param page the target page
* @throws IndexOutOfBoundsException if page < 0 or page >= mPages
*/
fun goTo(page: Int) {
when (page) {
!in 0 until pages -> {
throw IndexOutOfBoundsException("Page count is $pages, page is $page")
}
in mStartPage until mEndPage -> {
cancelCurrentTask()
val position = getPageStart(page)
scrollToPosition(position)
}
mStartPage - 1 -> {
showProgress(PROGRESS_TOP)
mCurrentTaskId = mIdGenerator.nextId()
mCurrentTaskType = TYPE_PRE_PAGE
mCurrentTaskPage = page
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage, null, true)
}
mEndPage -> {
showProgress(PROGRESS_BOTTOM)
mCurrentTaskId = mIdGenerator.nextId()
mCurrentTaskType = TYPE_NEXT_PAGE
mCurrentTaskPage = page
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage, null, true)
}
else -> {
showProgress(PROGRESS_TOP)
mCurrentTaskId = mIdGenerator.nextId()
mCurrentTaskType = TYPE_SOMEWHERE
mCurrentTaskPage = page
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage, null, true)
}
}
}
fun goTo(index: String?, isNext: Boolean) {
showProgress(PROGRESS_TOP)
mCurrentTaskId = mIdGenerator.nextId()
mCurrentTaskType = TYPE_SOMEWHERE
mCurrentTaskPage = 0
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage, index, isNext)
}
open fun saveInstanceState(superState: Parcelable?): Parcelable {
if (mData.isNotEmpty()) cancelCurrentTask()
val bundle = Bundle()
bundle.putParcelable(KEY_SUPER, superState)
val shownView = mViewTransition.shownViewIndex
bundle.putInt(KEY_SHOWN_VIEW, shownView)
bundle.putString(KEY_TIP, binding.tip.text.toString())
synchronized(contentMap) {
val size = contentMap.keys.takeIf { it.isNotEmpty() }?.max()?.inc() ?: 0
contentMap[size] = mData
bundle.putInt(KEY_DATA, size)
}
bundle.putInt(KEY_NEXT_ID, mIdGenerator.nextId())
bundle.putParcelable(KEY_PAGE_DIVIDER, mPageDivider)
bundle.putInt(KEY_START_PAGE, mStartPage)
bundle.putInt(KEY_END_PAGE, mEndPage)
bundle.putInt(KEY_PAGES, pages)
bundle.putString(KEY_PREV, mPrev)
bundle.putString(KEY_NEXT, mNext)
return bundle
}
open fun restoreInstanceState(state: Parcelable): Parcelable? {
return if (state is Bundle) {
mViewTransition.showView(state.getInt(KEY_SHOWN_VIEW), false)
binding.tip.text = state.getString(KEY_TIP)
val data = state.getInt(KEY_DATA)
synchronized(contentMap) {
mData = contentMap.remove(data) ?: ArrayList()
}
mIdGenerator.setNextId(state.getInt(KEY_NEXT_ID))
mPageDivider = state.getParcelableCompat(KEY_PAGE_DIVIDER) ?: IntList()
mStartPage = state.getInt(KEY_START_PAGE)
mEndPage = state.getInt(KEY_END_PAGE)
pages = state.getInt(KEY_PAGES)
mPrev = state.getString(KEY_PREV)
mNext = state.getString(KEY_NEXT)
notifyDataSetChanged()
notifyDataSetChanged {}
if (mData.size == 0) {
mPageDivider.clear()
mStartPage = 0
mEndPage = 0
pages = 0
mPrev = null
mNext = null
firstRefresh()
}
state.getParcelableCompat(KEY_SUPER)
} else {
state
}
}
companion object {
const val TYPE_REFRESH = 0
const val TYPE_PRE_PAGE = 1
const val TYPE_PRE_PAGE_KEEP_POS = 2
const val TYPE_NEXT_PAGE = 3
const val TYPE_NEXT_PAGE_KEEP_POS = 4
const val TYPE_SOMEWHERE = 5
const val TYPE_REFRESH_PAGE = 6
private val TAG = ContentHelper::class.java.simpleName
private const val CHECK_DUPLICATE_RANGE = 50
private const val PROGRESS_NONE = 0
private const val PROGRESS_TOP = 1
private const val PROGRESS_BOTTOM = 2
private const val KEY_SUPER = "super"
private const val KEY_SHOWN_VIEW = "shown_view"
private const val KEY_TIP = "tip"
private const val KEY_DATA = "data"
private const val KEY_NEXT_ID = "next_id"
private const val KEY_PAGE_DIVIDER = "page_divider"
private const val KEY_START_PAGE = "start_page"
private const val KEY_END_PAGE = "end_page"
private const val KEY_PAGES = "pages"
private const val KEY_PREV = "prev"
private const val KEY_NEXT = "next"
}
}
}
// data "ArrayList<GalleryInfo>" is too large, we cannot put it as Parcelable otherwise TransactionTooLargeException would occur
// TODO: convert to paging3 source then keep it in a proper window
// @see PagingConfig.maxSize
private val contentMap = hashMapOf<Int, ArrayList<GalleryInfo>>()
@@ -1,91 +0,0 @@
/*
* Copyright 2019 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.legacy
import android.annotation.SuppressLint
import android.os.Bundle
import android.os.Parcelable
import com.hippo.ehviewer.FavouriteStatusRouter
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.ui.legacy.ContentLayout.ContentHelper
import com.hippo.ehviewer.yorozuya.IntIdGenerator
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.Locale
abstract class GalleryInfoContentHelper : ContentHelper() {
var jumpTo: String? = null
val scope = CoroutineScope(Dispatchers.IO).apply {
launch {
FavouriteStatusRouter.globalFlow.collect { (gid, slot) -> map[gid]?.favoriteSlot = slot }
}
}
@SuppressLint("UseSparseArrays")
private var map: MutableMap<Long, GalleryInfo> = HashMap()
fun destroy() = scope.cancel()
override fun onAddData(data: List<GalleryInfo>) {
data.forEach { map[it.gid] = it }
}
override fun onRemoveData(data: List<GalleryInfo>) {
data.forEach { map.remove(it.gid) }
}
override fun onClearData() {
map.clear()
}
override fun saveInstanceState(superState: Parcelable?): Parcelable {
val bundle = super.saveInstanceState(superState) as Bundle
// TODO It's a bad design
val id = FavouriteStatusRouter.saveDataMap(map)
bundle.putInt(KEY_DATA_MAP, id)
return bundle
}
override fun restoreInstanceState(state: Parcelable): Parcelable? {
val bundle = state as Bundle
val id = bundle.getInt(KEY_DATA_MAP, IntIdGenerator.INVALID_ID)
if (id != IntIdGenerator.INVALID_ID) {
FavouriteStatusRouter.restoreDataMap(id)?.let { this.map = it }
}
return super.restoreInstanceState(state)
}
fun goTo(time: Long, isNext: Boolean) {
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.US).withZone(ZoneOffset.UTC)
jumpTo = formatter.format(Instant.ofEpochMilli(time))
if (isNext) {
goTo(mNext ?: "2", true)
} else {
goTo(mPrev, false)
}
jumpTo = null
}
companion object {
private const val KEY_DATA_MAP = "data_map"
}
}
@@ -60,7 +60,7 @@ class HandlerDrawable : Drawable() {
}
override fun setColorFilter(colorFilter: ColorFilter?) {
mPaint.setColorFilter(colorFilter)
mPaint.colorFilter = colorFilter
}
@Deprecated("Deprecated in Java")
@@ -69,9 +69,11 @@ class HandlerDrawable : Drawable() {
0xff -> {
PixelFormat.OPAQUE
}
0x00 -> {
PixelFormat.TRANSPARENT
}
else -> {
PixelFormat.TRANSLUCENT
}
@@ -18,7 +18,7 @@ package com.hippo.ehviewer.ui.legacy
import android.content.Context
import android.graphics.PointF
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.LayoutManager
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.google.android.material.math.MathUtils
import com.hippo.ehviewer.yorozuya.SimpleHandler
@@ -42,7 +42,7 @@ object LayoutManagerUtils {
}
fun scrollToPositionWithOffset(
layoutManager: RecyclerView.LayoutManager,
layoutManager: LayoutManager,
position: Int,
offset: Int,
) {
@@ -57,7 +57,7 @@ object LayoutManagerUtils {
}
fun smoothScrollToPosition(
layoutManager: RecyclerView.LayoutManager,
layoutManager: LayoutManager,
context: Context,
position: Int,
millisecondsPerInch: Int = -1,
@@ -104,13 +104,13 @@ object LayoutManagerUtils {
}
fun scrollToPositionProperly(
layoutManager: RecyclerView.LayoutManager,
layoutManager: LayoutManager,
context: Context,
position: Int,
listener: OnScrollToPositionListener?,
) {
SimpleHandler.postDelayed({
val first = getFirstVisibleItemPosition(layoutManager)
val first = layoutManager.firstVisibleItemPosition
val last = getLastVisibleItemPosition(layoutManager)
val offset = abs(position - first)
val max = last - first
@@ -128,19 +128,14 @@ object LayoutManagerUtils {
}, 200)
}
fun getFirstVisibleItemPosition(layoutManager: RecyclerView.LayoutManager): Int {
return when (layoutManager) {
is LinearLayoutManager -> layoutManager.findFirstVisibleItemPosition()
is StaggeredGridLayoutManager -> {
val positions = layoutManager.findFirstVisibleItemPositions(null)
positions.min()
}
else -> throw IllegalStateException("Can't do getFirstVisibleItemPosition for " + layoutManager.javaClass.name)
val LayoutManager.firstVisibleItemPosition: Int
get() = when (this) {
is LinearLayoutManager -> findFirstVisibleItemPosition()
is StaggeredGridLayoutManager -> findFirstVisibleItemPositions(null).min()
else -> error("Can't do getFirstVisibleItemPosition for " + javaClass.name)
}
}
fun getLastVisibleItemPosition(layoutManager: RecyclerView.LayoutManager): Int {
fun getLastVisibleItemPosition(layoutManager: LayoutManager): Int {
return when (layoutManager) {
is LinearLayoutManager -> layoutManager.findLastVisibleItemPosition()
is StaggeredGridLayoutManager -> {
@@ -48,11 +48,9 @@ import androidx.paging.LoadState
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.PagingDataAdapter
import androidx.paging.PagingSource
import androidx.paging.PagingState
import androidx.paging.cachedIn
import androidx.recyclerview.widget.DiffUtil
import com.google.android.material.datepicker.CalendarConstraints
import com.google.android.material.datepicker.CalendarConstraints.DateValidator
import com.google.android.material.datepicker.CompositeDateValidator
@@ -104,10 +102,9 @@ import java.util.Locale
// Note that we do not really follow mvvm structure, just use it as ... storage
class VMStorage : ViewModel() {
var urlBuilder = FavListUrlBuilder(favCat = Settings.recentFavCat)
var initialKey: String? = null
private val cloudDataFlow = Pager(PagingConfig(25)) {
object : PagingSource<String, GalleryInfo>() {
override fun getRefreshKey(state: PagingState<String, GalleryInfo>): String? = initialKey
override fun getRefreshKey(state: PagingState<String, GalleryInfo>): String? = null
override suspend fun load(params: LoadParams<String>) = withIOContext {
when (params) {
is LoadParams.Prepend -> urlBuilder.setIndex(params.key, isNext = false)
@@ -151,11 +148,9 @@ class VMStorage : ViewModel() {
class FavoritesScene : SearchBarScene() {
private val vm: VMStorage by viewModels()
private var urlBuilder by lazyMut { vm::urlBuilder }
private var initialKey by lazyMut { vm::initialKey }
private var _binding: SceneFavoritesBinding? = null
private val binding get() = _binding!!
private var mAdapter: PagingDataAdapter<GalleryInfo, GalleryHolder>? = null
private var mAdapterDelegate: GalleryAdapter? = null
private var mAdapter: GalleryAdapter? = null
private val showNormalFabsRunnable = Runnable {
updateJumpFab() // index: 0, 2
binding.fabLayout.run {
@@ -164,7 +159,7 @@ class FavoritesScene : SearchBarScene() {
}
}
fun onItemClick(position: Int) {
private fun onItemClick(position: Int) {
if (isDrawerOpen(GravityCompat.END)) {
// Skip if in search mode
if (!binding.recyclerView.isInCustomChoice) {
@@ -172,20 +167,6 @@ class FavoritesScene : SearchBarScene() {
updateJumpFab()
closeDrawer(GravityCompat.END)
}
} else {
if (binding.recyclerView.isInCustomChoice) {
binding.recyclerView.toggleItemChecked(position)
} else {
mAdapter?.peek(position)?.let {
navAnimated(
R.id.galleryDetailScene,
bundleOf(
GalleryDetailScene.KEY_ACTION to GalleryDetailScene.ACTION_GALLERY_INFO,
GalleryDetailScene.KEY_GALLERY_INFO to it,
),
)
}
}
}
}
@@ -197,7 +178,6 @@ class FavoritesScene : SearchBarScene() {
urlBuilder.favCat = newCat
urlBuilder.jumpTo = null
urlBuilder.setIndex(null, true)
initialKey = null
collectJob?.cancel()
collectJob = viewLifecycleOwner.lifecycleScope.launchIO {
vm.dataflow().collectLatest {
@@ -222,7 +202,7 @@ class FavoritesScene : SearchBarScene() {
override fun onResume() {
super.onResume()
mAdapterDelegate?.type = Settings.listMode
mAdapter?.type = Settings.listMode
}
override fun onCreateViewWithToolbar(
@@ -265,7 +245,7 @@ class FavoritesScene : SearchBarScene() {
0 -> showGoToDialog()
1 -> switchFav(urlBuilder.favCat)
2 -> {
initialKey = "1-0"
urlBuilder.setIndex("1-0", false)
mAdapter?.refresh()
}
}
@@ -331,22 +311,29 @@ class FavoritesScene : SearchBarScene() {
}
}
binding.recyclerView.run {
val delegateAdapter = object : GalleryAdapter(resources, this@run, Settings.listMode, false, { onItemClick(it) }, { onItemLongClick(it) }) {
override fun getItemCount() = TODO()
override fun getDataAt(position: Int) = mAdapter?.peek(position)
}
val diffCallback = object : DiffUtil.ItemCallback<GalleryInfo>() {
override fun areItemsTheSame(oldItem: GalleryInfo, newItem: GalleryInfo) = oldItem.gid == newItem.gid
override fun areContentsTheSame(oldItem: GalleryInfo, newItem: GalleryInfo) = oldItem.gid == newItem.gid
}
mAdapter = object : PagingDataAdapter<GalleryInfo, GalleryHolder>(diffCallback) {
override fun onBindViewHolder(holder: GalleryHolder, position: Int) {
getItem(position)
delegateAdapter.onBindViewHolder(holder, position)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = delegateAdapter.onCreateViewHolder(parent, delegateAdapter.type)
}.also { adapter ->
binding.recyclerView.adapter = adapter
mAdapter = GalleryAdapter(
this@run,
false,
{ info, pos ->
if (binding.recyclerView.isInCustomChoice) {
binding.recyclerView.toggleItemChecked(pos)
} else {
navAnimated(
R.id.galleryDetailScene,
bundleOf(
GalleryDetailScene.KEY_ACTION to GalleryDetailScene.ACTION_GALLERY_INFO,
GalleryDetailScene.KEY_GALLERY_INFO to info,
),
)
}
},
{ _, pos ->
if (!binding.recyclerView.isInCustomChoice) {
binding.recyclerView.intoCustomChoiceMode()
}
binding.recyclerView.toggleItemChecked(pos)
},
).also { adapter ->
val drawable = ContextCompat.getDrawable(context, R.drawable.big_sad_pandroid)!!
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
binding.tip.setCompoundDrawables(null, drawable, null, null)
@@ -378,7 +365,6 @@ class FavoritesScene : SearchBarScene() {
}
}
switchFav(Settings.recentFavCat)
mAdapterDelegate = delegateAdapter
setChoiceMode(EasyRecyclerView.CHOICE_MODE_MULTIPLE_CUSTOM)
setCustomCheckedListener(object : CustomChoiceListener {
override fun onIntoCustomChoice(view: EasyRecyclerView) {
@@ -431,7 +417,6 @@ class FavoritesScene : SearchBarScene() {
(binding.fabLayout.parent as ViewGroup).removeView(binding.fabLayout)
removeAboveSnackView(binding.fabLayout)
mAdapter = null
mAdapterDelegate = null
_binding = null
}
@@ -467,15 +452,6 @@ class FavoritesScene : SearchBarScene() {
}
}
fun onItemLongClick(position: Int): Boolean {
// Can not into
if (!binding.recyclerView.isInCustomChoice) {
binding.recyclerView.intoCustomChoiceMode()
}
binding.recyclerView.toggleItemChecked(position)
return true
}
private fun showGoToDialog() {
context ?: return
val local = LocalDateTime.of(2007, 3, 21, 0, 0)
@@ -15,15 +15,17 @@
*/
package com.hippo.ehviewer.ui.scene
import android.content.res.Resources
import android.graphics.Rect
import android.view.View
import android.view.ViewGroup
import androidx.annotation.IntDef
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ItemDecoration
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.Settings.detailSize
import com.hippo.ehviewer.Settings.thumbSizeDp
import com.hippo.ehviewer.client.data.GalleryInfo
@@ -34,14 +36,18 @@ import com.hippo.ehviewer.ui.legacy.STRATEGY_SUITABLE_SIZE
import com.hippo.ehviewer.yorozuya.dp2px
import splitties.init.appCtx
abstract class GalleryAdapter(
private val resources: Resources,
private val diffCallback = object : DiffUtil.ItemCallback<GalleryInfo>() {
override fun areItemsTheSame(oldItem: GalleryInfo, newItem: GalleryInfo) = oldItem.gid == newItem.gid
override fun areContentsTheSame(oldItem: GalleryInfo, newItem: GalleryInfo) = oldItem.gid == newItem.gid
}
class GalleryAdapter(
private val recyclerView: RecyclerView,
type: Int,
private val showFavorite: Boolean,
private val onItemClick: (Int) -> Unit,
private val onItemLongClick: (Int) -> Unit,
) : RecyclerView.Adapter<GalleryHolder>() {
private val onItemClick: (GalleryInfo, Int) -> Unit,
private val onItemLongClick: (GalleryInfo, Int) -> Unit,
) : PagingDataAdapter<GalleryInfo, GalleryHolder>(diffCallback) {
private val resources = recyclerView.context.resources
private val layoutManager: AutoStaggeredGridLayoutManager = AutoStaggeredGridLayoutManager(0, StaggeredGridLayoutManager.VERTICAL)
private val mPaddingTopSB: Int = resources.getDimensionPixelOffset(R.dimen.gallery_padding_top_search_bar)
private var mListDecoration: ItemDecoration? = null
@@ -126,12 +132,13 @@ abstract class GalleryAdapter(
}
}
}
notifyDataSetChanged()
refresh()
}
init {
recyclerView.adapter = this
recyclerView.layoutManager = layoutManager
this.type = type
type = Settings.listMode
adjustPaddings()
}
@@ -150,18 +157,14 @@ abstract class GalleryAdapter(
else -> error("Unexpected value: $viewType")
}
override fun getItemViewType(position: Int): Int {
return mType
}
abstract fun getDataAt(position: Int): GalleryInfo?
override fun getItemViewType(position: Int) = mType
override fun onBindViewHolder(holder: GalleryHolder, position: Int) {
val gi = getDataAt(position) ?: return
val gi = getItem(position) ?: return
holder.bind(
gi,
{ onItemClick(position) },
{ onItemLongClick(position) },
{ onItemClick(gi, position) },
{ onItemLongClick(gi, position) },
)
}
File diff suppressed because it is too large. Load diff
@@ -1,46 +0,0 @@
/*
* Copyright 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.yorozuya
import java.util.concurrent.atomic.AtomicInteger
class IntIdGenerator {
private val mId = AtomicInteger()
constructor()
constructor(init: Int) {
setNextId(init)
}
fun nextId(): Int {
var id: Int
while (mId.getAndIncrement().also { id = it } == INVALID_ID);
return id
}
fun setNextId(id: Int) {
checkInValidId(id)
mId.set(id)
}
private fun checkInValidId(id: Int) {
check(INVALID_ID != id) { "Can't set INVALID_ID" }
}
companion object {
const val INVALID_ID = -1
}
}
@@ -1,21 +0,0 @@
/*
* Copyright 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.yorozuya
import android.view.View
import android.view.ViewGroup
fun View.removeFromParent() = (parent as? ViewGroup)?.removeView(this)
+45 -2
View File
@@ -22,11 +22,54 @@
android:paddingTop="16dp"
app:layout_behavior="@string/searchbar_scrolling_view_behavior">
<com.hippo.ehviewer.ui.legacy.ContentLayout
<FrameLayout
android:id="@+id/content_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:fitsSystemWindowsInsets="top|bottom" />
android:clipChildren="false">
<TextView
android:id="@+id/tip"
style="@style/TextAppearance.AppCompat.Medium"
android:layout_width="228dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawablePadding="8dp"
android:gravity="center_horizontal" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/progress"
style="@style/ProgressView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<androidx.swiperefreshlayout.widget.ThemedSwipeRefreshLayout
android:id="@+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
app:fitsSystemWindowsInsets="top">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false" />
</androidx.swiperefreshlayout.widget.ThemedSwipeRefreshLayout>
<com.hippo.ehviewer.ui.legacy.FastScroller
android:id="@+id/fast_scroller"
android:layout_width="30dp"
android:layout_height="match_parent"
android:layout_gravity="end"
android:paddingStart="20dp"
android:paddingTop="8dp"
android:paddingEnd="4dp"
android:paddingBottom="8dp" />
</FrameLayout>
<com.hippo.ehviewer.ui.legacy.SearchLayout
android:id="@+id/search_layout"
@@ -1,79 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2015 Hippo Seven
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/progress"
style="@style/ProgressView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<TextView
android:id="@+id/tip"
style="@style/TextAppearance.AppCompat.Medium"
android:layout_width="228dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawablePadding="8dp"
android:gravity="center_horizontal" />
<FrameLayout
android:id="@+id/content_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false">
<androidx.swiperefreshlayout.widget.ThemedSwipeRefreshLayout
android:id="@+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false">
<com.hippo.ehviewer.ui.legacy.EasyRecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false" />
</androidx.swiperefreshlayout.widget.ThemedSwipeRefreshLayout>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/bottom_progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:indeterminate="true"
android:visibility="gone"
app:fitsSystemWindowsInsets="end"
app:hideAnimationBehavior="inward"
app:showAnimationBehavior="outward" />
<com.hippo.ehviewer.ui.legacy.FastScroller
android:id="@+id/fast_scroller"
android:layout_width="30dp"
android:layout_height="match_parent"
android:layout_gravity="end"
android:paddingStart="20dp"
android:paddingTop="8dp"
android:paddingEnd="4dp"
android:paddingBottom="8dp" />
</FrameLayout>
</merge>