Tablet UI for manga details

It returns for some reason... even I'm trying to figure out why

enjoy my 8 tablet users

Also kinda fix the view scrolling down a bit when tapping on the expanded summary when the summary is extremely long
This commit is contained in:
Jays2Kings 2021-05-08 22:48:43 -04:00
parent 0c2262ccba
commit 4999db33f4
7 changed files with 243 additions and 89 deletions

View File

@ -11,6 +11,7 @@ import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.Rect
import android.graphics.drawable.BitmapDrawable
@ -30,6 +31,7 @@ import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.SearchView
import androidx.core.graphics.ColorUtils
import androidx.core.view.isVisible
import androidx.palette.graphics.Palette
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
@ -95,15 +97,18 @@ import eu.kanade.tachiyomi.util.system.getPrefTheme
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.isInNightMode
import eu.kanade.tachiyomi.util.system.isOnline
import eu.kanade.tachiyomi.util.system.isTablet
import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.activityBinding
import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets
import eu.kanade.tachiyomi.util.view.getText
import eu.kanade.tachiyomi.util.view.requestPermissionsSafe
import eu.kanade.tachiyomi.util.view.scrollViewWith
import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener
import eu.kanade.tachiyomi.util.view.setStyle
import eu.kanade.tachiyomi.util.view.snack
import eu.kanade.tachiyomi.util.view.toolbarHeight
import eu.kanade.tachiyomi.util.view.updateLayoutParams
import eu.kanade.tachiyomi.util.view.updatePaddingRelative
import eu.kanade.tachiyomi.util.view.withFadeTransaction
@ -172,6 +177,10 @@ class MangaDetailsController :
var refreshTracker: Int? = null
var chapterPopupMenu: Pair<Int, PopupMenu>? = null
// Tablet Layout
var isTablet = false
private var tabletAdapter: MangaDetailsAdapter? = null
private var query = ""
private var adapter: MangaDetailsAdapter? = null
@ -195,6 +204,7 @@ class MangaDetailsController :
coverColor = null
fullCoverActive = false
setTabletMode(view)
setRecycler(view)
setPaletteColor()
adapter?.fastScroller = binding.fastScroller
@ -208,6 +218,21 @@ class MangaDetailsController :
requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
}
/** Check if device is tablet, and use a second recycler to hold the details header if so */
private fun setTabletMode(view: View) {
isTablet = view.context.isTablet() &&
view.context.resources.configuration?.orientation == Configuration.ORIENTATION_LANDSCAPE
binding.tabletOverlay.isVisible = isTablet
binding.tabletRecycler.isVisible = isTablet
binding.tabletDivider.isVisible = isTablet
if (isTablet) {
binding.recycler.updateLayoutParams<ViewGroup.LayoutParams> { width = 0 }
tabletAdapter = MangaDetailsAdapter(this)
binding.tabletRecycler.adapter = tabletAdapter
binding.tabletRecycler.layoutManager = LinearLayoutManager(view.context)
}
}
override fun onDestroyView(view: View) {
snack?.dismiss()
presenter.onDestroy()
@ -236,26 +261,37 @@ class MangaDetailsController :
binding.swipeRefresh.setDistanceToTriggerSync(70.dpToPx)
activityBinding!!.appBar.elevation = 0f
scrollViewWith(
binding.recycler,
padBottom = true,
customPadding = true,
afterInsets = { insets ->
if (isTablet) {
val tHeight = toolbarHeight.takeIf { it ?: 0 > 0 } ?: appbarHeight
headerHeight = tHeight + (activityBinding?.root?.rootWindowInsets?.systemWindowInsetTop ?: 0)
binding.recycler.updatePaddingRelative(top = headerHeight + 4.dpToPx)
binding.recycler.doOnApplyWindowInsets { _, insets, _ ->
setInsets(insets, appbarHeight, offset)
},
liftOnScroll = {
colorToolbar(it)
}
)
} else {
scrollViewWith(
binding.recycler,
padBottom = true,
customPadding = true,
afterInsets = { insets ->
setInsets(insets, appbarHeight, offset)
},
liftOnScroll = {
colorToolbar(it)
}
)
}
binding.recycler.addOnScrollListener(
object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val atTop = !recyclerView.canScrollVertically(-1)
val tY = getHeader()?.binding?.backdrop?.translationY ?: 0f
getHeader()?.binding?.backdrop?.translationY = max(0f, tY + dy * 0.25f)
if (atTop) getHeader()?.binding?.backdrop?.translationY = 0f
if (!isTablet) {
val atTop = !recyclerView.canScrollVertically(-1)
val tY = getHeader()?.binding?.backdrop?.translationY ?: 0f
getHeader()?.binding?.backdrop?.translationY = max(0f, tY + dy * 0.25f)
if (atTop) getHeader()?.binding?.backdrop?.translationY = 0f
}
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
@ -268,9 +304,15 @@ class MangaDetailsController :
private fun setInsets(insets: WindowInsets, appbarHeight: Int, offset: Int) {
binding.recycler.updatePaddingRelative(bottom = insets.systemWindowInsetBottom)
headerHeight = appbarHeight + insets.systemWindowInsetTop
binding.tabletRecycler.updatePaddingRelative(bottom = insets.systemWindowInsetBottom)
val tHeight = toolbarHeight.takeIf { it ?: 0 > 0 } ?: appbarHeight
headerHeight = tHeight + insets.systemWindowInsetTop
binding.swipeRefresh.setProgressViewOffset(false, (-40).dpToPx, headerHeight + offset)
// 1dp extra to line up chapter header and manga header
if (isTablet) {
binding.tabletOverlay.updateLayoutParams<ViewGroup.LayoutParams> { height = headerHeight }
// 4dp extra to line up chapter header and manga header
binding.recycler.updatePaddingRelative(top = headerHeight + 4.dpToPx)
}
getHeader()?.setTopHeight(headerHeight)
binding.fastScroller.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = headerHeight
@ -280,8 +322,8 @@ class MangaDetailsController :
}
/** Set the toolbar to fully transparent or colored and translucent */
fun colorToolbar(isColor: Boolean, animate: Boolean = true) {
if (isColor == toolbarIsColored) return
private fun colorToolbar(isColor: Boolean, animate: Boolean = true) {
if (isColor == toolbarIsColored || (isTablet && isColor)) return
toolbarIsColored = isColor
val isCurrentController =
router?.backstack?.lastOrNull()?.controller == this@MangaDetailsController
@ -538,12 +580,14 @@ class MangaDetailsController :
}
private fun getHeader(): MangaHeaderHolder? {
return binding.recycler.findViewHolderForAdapterPosition(0) as? MangaHeaderHolder
return if (isTablet) binding.tabletRecycler.findViewHolderForAdapterPosition(0) as? MangaHeaderHolder
else binding.recycler.findViewHolderForAdapterPosition(0) as? MangaHeaderHolder
}
fun updateHeader() {
binding.swipeRefresh.isRefreshing = presenter.isLoading
adapter?.setChapters(presenter.chapters)
tabletAdapter?.notifyDataSetChanged()
addMangaHeader()
activity?.invalidateOptionsMenu()
}
@ -555,6 +599,7 @@ class MangaDetailsController :
launchUI { binding.swipeRefresh.isRefreshing = true }
presenter.fetchChaptersFromSource()
}
tabletAdapter?.notifyDataSetChanged()
adapter?.setChapters(chapters)
addMangaHeader()
colorToolbar(binding.recycler.canScrollVertically(-1))
@ -562,7 +607,12 @@ class MangaDetailsController :
}
private fun addMangaHeader() {
if (adapter?.scrollableHeaders?.isEmpty() == true) {
if (tabletAdapter?.scrollableHeaders?.isEmpty() == true) {
tabletAdapter?.removeAllScrollableHeaders()
tabletAdapter?.addScrollableHeader(presenter.headerItem)
adapter?.removeAllScrollableHeaders()
adapter?.addScrollableHeader(presenter.tabletChapterHeaderItem!!)
} else if (!isTablet && adapter?.scrollableHeaders?.isEmpty() == true) {
adapter?.removeAllScrollableHeaders()
adapter?.addScrollableHeader(presenter.headerItem)
}
@ -819,8 +869,10 @@ class MangaDetailsController :
setOnQueryTextChangeListener(searchView) {
query = it ?: ""
if (query.isNotEmpty()) getHeader()?.collapse()
else getHeader()?.expand()
if (!isTablet) {
if (query.isNotEmpty()) getHeader()?.collapse()
else getHeader()?.expand()
}
adapter?.setFilter(query)
adapter?.performFilter()

View File

@ -46,6 +46,7 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.manga.MangaShortcutManager
import eu.kanade.tachiyomi.util.system.executeOnIO
import eu.kanade.tachiyomi.util.system.isTablet
import eu.kanade.tachiyomi.util.system.launchIO
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -97,8 +98,14 @@ class MangaDetailsPresenter(
private set
var headerItem = MangaHeaderItem(manga, controller.fromCatalogue)
var tabletChapterHeaderItem: MangaHeaderItem? = null
fun onCreate() {
headerItem.isTablet = controller.isTablet
if (controller.isTablet) {
tabletChapterHeaderItem = MangaHeaderItem(manga, false)
tabletChapterHeaderItem?.isChapterHeader = true
}
isLockedFromSearch = SecureActivityDelegate.shouldBeLocked()
headerItem.isLocked = isLockedFromSearch
downloadManager.addListener(this)

View File

@ -15,6 +15,7 @@ import com.google.android.material.button.MaterialButton
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.image.coil.loadManga
import eu.kanade.tachiyomi.databinding.ChapterHeaderItemBinding
import eu.kanade.tachiyomi.databinding.MangaHeaderItemBinding
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.model.SManga
@ -28,81 +29,110 @@ import eu.kanade.tachiyomi.util.view.updateLayoutParams
class MangaHeaderHolder(
private val view: View,
private val adapter: MangaDetailsAdapter,
startExpanded: Boolean
startExpanded: Boolean,
isTablet: Boolean = false
) : BaseFlexibleViewHolder(view, adapter) {
val binding = MangaHeaderItemBinding.bind(view)
val binding: MangaHeaderItemBinding? = try {
MangaHeaderItemBinding.bind(view)
} catch (e: Exception) {
null
}
private val chapterBinding: ChapterHeaderItemBinding? = try {
ChapterHeaderItemBinding.bind(view)
} catch (e: Exception) {
null
}
private var showReadingButton = true
private var showMoreButton = true
var hadSelection = false
init {
binding.chapterLayout.setOnClickListener { adapter.delegate.showChapterFilter() }
binding.startReadingButton.setOnClickListener { adapter.delegate.readNextChapter() }
binding.topView.updateLayoutParams<ConstraintLayout.LayoutParams> {
height = adapter.delegate.topCoverHeight()
}
binding.moreButton.setOnClickListener { expandDesc() }
binding.mangaSummary.setOnClickListener {
if (binding.moreButton.isVisible) {
expandDesc()
} else if (!hadSelection) {
collapseDesc()
} else {
hadSelection = false
if (binding == null) {
with(chapterBinding) {
this ?: return@with
chapterLayout.setOnClickListener { adapter.delegate.showChapterFilter() }
}
}
binding.mangaSummary.setOnLongClickListener {
if (binding.mangaSummary.isTextSelectable && !adapter.recyclerView.canScrollVertically(-1)) {
(adapter.delegate as MangaDetailsController).binding.swipeRefresh.isEnabled = false
with(binding) {
this ?: return@with
chapterLayout.setOnClickListener { adapter.delegate.showChapterFilter() }
startReadingButton.setOnClickListener { adapter.delegate.readNextChapter() }
topView.updateLayoutParams<ConstraintLayout.LayoutParams> {
height = adapter.delegate.topCoverHeight()
}
false
}
binding.mangaSummary.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
view.requestFocus()
moreButton.setOnClickListener { expandDesc() }
mangaSummary.setOnClickListener {
if (moreButton.isVisible) {
expandDesc()
} else if (!hadSelection) {
collapseDesc()
} else {
hadSelection = false
}
}
if (event.actionMasked == MotionEvent.ACTION_UP) {
hadSelection = binding.mangaSummary.hasSelection()
(adapter.delegate as MangaDetailsController).binding.swipeRefresh.isEnabled =
true
mangaSummary.setOnLongClickListener {
if (mangaSummary.isTextSelectable && !adapter.recyclerView.canScrollVertically(
-1
)
) {
(adapter.delegate as MangaDetailsController).binding.swipeRefresh.isEnabled =
false
}
false
}
false
mangaSummary.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
view.requestFocus()
}
if (event.actionMasked == MotionEvent.ACTION_UP) {
hadSelection = mangaSummary.hasSelection()
(adapter.delegate as MangaDetailsController).binding.swipeRefresh.isEnabled =
true
}
false
}
if (!itemView.resources.isLTR) {
moreBgGradient.rotation = 180f
}
lessButton.setOnClickListener { collapseDesc() }
mangaGenresTags.setOnTagClickListener {
adapter.delegate.tagClicked(it)
}
webviewButton.setOnClickListener { adapter.delegate.openInWebView() }
shareButton.setOnClickListener { adapter.delegate.prepareToShareManga() }
favoriteButton.setOnClickListener {
adapter.delegate.favoriteManga(false)
}
title.setOnClickListener {
title.text?.let { adapter.delegate.globalSearch(it.toString()) }
}
title.setOnLongClickListener {
adapter.delegate.copyToClipboard(title.text.toString(), R.string.title)
true
}
mangaAuthor.setOnClickListener {
mangaAuthor.text?.let { adapter.delegate.globalSearch(it.toString()) }
}
mangaAuthor.setOnLongClickListener {
adapter.delegate.copyToClipboard(
mangaAuthor.text.toString(),
R.string.author
)
true
}
mangaCover.setOnClickListener { adapter.delegate.zoomImageFromThumb(coverCard) }
trackButton.setOnClickListener { adapter.delegate.showTrackingSheet() }
if (startExpanded) expandDesc()
else collapseDesc()
if (isTablet) chapterLayout.isVisible = false
}
if (!itemView.resources.isLTR) {
binding.moreBgGradient.rotation = 180f
}
binding.lessButton.setOnClickListener { collapseDesc() }
binding.mangaGenresTags.setOnTagClickListener {
adapter.delegate.tagClicked(it)
}
binding.webviewButton.setOnClickListener { adapter.delegate.openInWebView() }
binding.shareButton.setOnClickListener { adapter.delegate.prepareToShareManga() }
binding.favoriteButton.setOnClickListener {
adapter.delegate.favoriteManga(false)
}
binding.title.setOnClickListener {
binding.title.text?.let { adapter.delegate.globalSearch(it.toString()) }
}
binding.title.setOnLongClickListener {
adapter.delegate.copyToClipboard(binding.title.text.toString(), R.string.title)
true
}
binding.mangaAuthor.setOnClickListener {
binding.mangaAuthor.text?.let { adapter.delegate.globalSearch(it.toString()) }
}
binding.mangaAuthor.setOnLongClickListener {
adapter.delegate.copyToClipboard(binding.mangaAuthor.text.toString(), R.string.author)
true
}
binding.mangaCover.setOnClickListener { adapter.delegate.zoomImageFromThumb(binding.coverCard) }
binding.trackButton.setOnClickListener { adapter.delegate.showTrackingSheet() }
if (startExpanded) expandDesc()
else collapseDesc()
}
private fun expandDesc() {
binding ?: return
if (binding.moreButton.visibility == View.VISIBLE) {
binding.mangaSummary.maxLines = Integer.MAX_VALUE
binding.mangaSummary.setTextIsSelectable(true)
@ -110,10 +140,12 @@ class MangaHeaderHolder(
binding.lessButton.isVisible = true
binding.moreButtonGroup.isVisible = false
binding.title.maxLines = Integer.MAX_VALUE
binding.mangaSummary.requestFocus()
}
}
private fun collapseDesc() {
binding ?: return
binding.mangaSummary.setTextIsSelectable(false)
binding.mangaSummary.isClickable = true
binding.mangaSummary.maxLines = 3
@ -129,13 +161,29 @@ class MangaHeaderHolder(
fun bindChapters() {
val presenter = adapter.delegate.mangaPresenter()
val count = presenter.chapters.size
binding.chaptersTitle.text = itemView.resources.getQuantityString(R.plurals.chapters_plural, count, count)
binding.filtersText.text = presenter.currentFilters()
if (binding != null) {
binding.chaptersTitle.text =
itemView.resources.getQuantityString(R.plurals.chapters_plural, count, count)
binding.filtersText.text = presenter.currentFilters()
} else if (chapterBinding != null) {
chapterBinding.chaptersTitle.text =
itemView.resources.getQuantityString(R.plurals.chapters_plural, count, count)
chapterBinding.filtersText.text = presenter.currentFilters()
}
}
@SuppressLint("SetTextI18n")
fun bind(item: MangaHeaderItem, manga: Manga) {
val presenter = adapter.delegate.mangaPresenter()
if (binding == null) {
if (chapterBinding != null) {
val count = presenter.chapters.size
chapterBinding.chaptersTitle.text =
itemView.resources.getQuantityString(R.plurals.chapters_plural, count, count)
chapterBinding.filtersText.text = presenter.currentFilters()
}
return
}
binding.title.text = manga.title
if (manga.genre.isNullOrBlank().not()) binding.mangaGenresTags.setTags(
@ -281,16 +329,20 @@ class MangaHeaderHolder(
}
fun setTopHeight(newHeight: Int) {
binding ?: return
if (newHeight == binding.topView.height) return
binding.topView.updateLayoutParams<ConstraintLayout.LayoutParams> {
height = newHeight
}
}
fun setBackDrop(color: Int) {
binding ?: return
binding.trueBackdrop.setBackgroundColor(color)
}
fun updateTracking() {
binding ?: return
val presenter = adapter.delegate.mangaPresenter()
val tracked = presenter.isTracked()
with(binding.trackButton) {
@ -309,6 +361,7 @@ class MangaHeaderHolder(
}
fun collapse() {
binding ?: return
binding.subItemGroup.isVisible = false
binding.startReadingButton.isVisible = false
if (binding.moreButton.isVisible || binding.moreButton.isInvisible) {
@ -320,6 +373,7 @@ class MangaHeaderHolder(
}
fun updateCover(manga: Manga) {
binding ?: return
if (!manga.initialized) return
val drawable = adapter.controller.binding.mangaCoverFull.drawable
binding.mangaCover.loadManga(
@ -343,6 +397,7 @@ class MangaHeaderHolder(
}
fun expand() {
binding ?: return
binding.subItemGroup.isVisible = true
if (!showMoreButton) binding.moreButtonGroup.isVisible = false
else {

View File

@ -13,6 +13,7 @@ class MangaHeaderItem(val manga: Manga, var startExpanded: Boolean) :
var isChapterHeader = false
var isLocked = false
var isTablet = false
override fun getLayoutRes(): Int {
return if (isChapterHeader) R.layout.chapter_header_item else R.layout.manga_header_item
@ -27,7 +28,7 @@ class MangaHeaderItem(val manga: Manga, var startExpanded: Boolean) :
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): MangaHeaderHolder {
return MangaHeaderHolder(view, adapter as MangaDetailsAdapter, startExpanded)
return MangaHeaderHolder(view, adapter as MangaDetailsAdapter, startExpanded, isTablet)
}
override fun bindViewHolder(

View File

@ -31,11 +31,11 @@
android:id="@+id/webview_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:layout_marginEnd="12dp"
android:background="@null"
android:padding="5dp"
android:src="@drawable/ic_filter_list_24dp"
android:tint="?colorAccent"
app:tint="?colorAccent"
app:layout_constraintBottom_toBottomOf="@id/chapters_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/chapters_title" />

View File

@ -13,19 +13,58 @@
android:layout_height="match_parent"
android:background="?android:colorBackground">
<FrameLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/linear_recycler_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:orientation="horizontal">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/tablet_recycler"
android:layout_width="0dp"
android:layout_height="match_parent"
android:clipToPadding="false"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/recycler"
app:layout_constraintWidth_percent="0.4"
tools:itemCount="1"
tools:listitem="@layout/manga_header_item" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/tablet_recycler"
tools:listitem="@layout/chapters_item" />
</FrameLayout>
<View
android:id="@+id/tablet_overlay"
android:visibility="gone"
android:background="?android:attr/colorBackground"
android:alpha=".80"
android:layout_width="0dp"
android:layout_height="20dp"
app:layout_constraintWidth_percent=".6"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<View
android:id="@+id/tablet_divider"
android:visibility="gone"
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@color/divider"
app:layout_constraintEnd_toStartOf="@id/tablet_overlay"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<eu.kanade.tachiyomi.ui.base.MaterialFastScroll
@ -45,7 +84,6 @@
android:visibility="invisible"
tools:background="@color/md_black_1000" />
<ImageView
android:id="@+id/manga_cover_full"
android:layout_width="match_parent"

View File

@ -356,6 +356,7 @@
android:tooltipText="@string/sort_and_filter"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/start_reading_button">
<com.google.android.material.textview.MaterialTextView